Category Archives: Software

PHP, Reflection et Annotations

J’ai récemment eu à coder à mon taf des jeux de tests pour valider le comportement de mon serveur de pub, et pour ça, j’ai codé des petits scripts tout bête pour créer/modifier les données dans une base postgresql. J’aurai pu prendre un ORM tout fait (propel, doctrine…), mais j’ai juste codé mes classes tout bêtement et le plus simplement du monde. Un exemple:

class Voiture
{
    public $id;
    public $marque;
    public $couleur;
};

Pour les requêtes d’insert, au lieu d’avoir des méthodes spécialisées dans chaque classe, j’ai codé juste une fonction save() qui prend l’objet à sauver, et qui parse grâce aux fonctions get_class_* ses propriétés pour générer les requêtes d’INSERT ou d’UPDATE. Cette fonction ressembait dans un premier temps à:

function save($obj)
{
    $class_name = get_class($obj);
    $class_vars = get_class_vars($class_name);

    $fields_names = '';
    $fields_values = '';

    $fields_count = 0;

    foreach($class_vars as $var_name => $var_value)
    {
        if(isset($obj->$var_name))
        {
            $fields_names .= $var_name . ', ';
            if(is_string($obj->$var_name))
                $fields_values .= '\'' . $obj->$var_name . '\', ';
            else
                $fields_values .= $obj->$var_name . ', ';

            $fields_count ++;
        }
    }

    if(0 == $fields_count)
    {
        return -1;
    }

    // Remove trailing ', ';
    $fields_names = substr($fields_names, 0, strlen($fields_names) - 2);
    $fields_values = substr($fields_values, 0, strlen($fields_values) - 2);

    return "INSERT INTO " . $class_name . "(" . $fields_names . ") VALUES (" . $fields_values . ");";
}

Ca me contentait dans pas mal de cas simples, mais très rapidement, cela à montré ses limites. J’avais besoin de connaître plus précisement les types des champs dans la base. Et la solution a été d’utiliser les annotations, pour ne pas avoir de longues et lourdes modifications à faire dans toutes mes classes d’objet. PHP ne propose pas out of the box un système complet d’annotation, mais incorpore les outils pour développer un comportement analogue: les classes Reflection.

Ces classes permettent de faire du « reverse engineering » sur les classes, les interfaces etc pour retrouver les différentes composantes de ces objets, qu’ils soient instanciés ou non. Et de plus, elles permettent de récupérer les commentaires des classes, des méthodes et des propriétés s’ils existent. Il n’y a qu’à les mettre en application:

<?php

/**
 * Commentaire classe voiture
 */

class Voiture
{
    /**
     * Commentaire propriete id
     */

    public $id;
}

$voiture = new Voiture;

$reflection = new ReflectionClass($voiture);
echo "Classe:\n";
echo $reflection->getDocComment() . "\n";

foreach($reflection->getProperties() as $property)
{
    echo $property->name . ":\n";
    echo $property->getDocComment() . "\n";
}

Ce qui donne:

$ php test_2.php
/**
 * Commentaire classe voiture
 */
id:
/**
     * Commentaire propriete id
     */
$

Il ne resterait plus maintenant qu’à définir ses keywords, parser les commentaires propement pis à coder son propre mini-orm sans modifier les objets finaux.

Et voilà un début:

<?php

class DbObject
{
    public function save()
    {
        $schema = $this->buildSchema($this);
        /* Ici y a du code */

        return TRUE;
    }

    private function buildSchema()
    {
        $class_name = get_class($this);
        $class_vars = get_class_vars($class_name);

        $class_reflection = new ReflectionClass($this);
        echo $class_reflection->getDocComment();

        $schema = array();

        foreach($class_vars as $var_name => $var_value)
        {
            $prop_reflection = $class_reflection->getProperty($var_name);
            $comment = $prop_reflection->getDocComment();

            $comment = preg_replace(',\/\*\*(.*)\*\/,', '$1', $comment);
            $comments = preg_split(',\n,', $comment);

            $key = $val = NULL;
            $schema[$var_name] = array();

            foreach($comments as $comment_line)
            {
                if(preg_match(',@(.*?): (.*),i', $comment_line, $matches))
                {
                    $key = $matches[1];
                    $val = $matches[2];

                    $schema[$var_name][trim($key)] = trim($val);
                }
            }
        }
        var_dump($schema);

        return $schema;
    }
};

class Voiture extends DbObject
{
    /**-
     * @DbType: integer
     * @DbValue: 0
     */

    public $id;

    /** @DbType: string
     *
     * Blabla.
     *
     */

    public $marque;

    /** @DbType: string */
    public $couleur;
};

$voiture = new Voiture;
$voiture->id = 1;
$voiture->marque = 'renault';
$voiture->couleur = 'bleu';
$voiture->save();

buildSchema retournera dans cet exemple:

$ php test_annotations.php
array(3) {
  ["id"]=>
  array(2) {
    ["DbType"]=>
    string(7) "integer"
    ["DbValue"]=>
    string(1) "0"
  }
  ["marque"]=>
  array(1) {
    ["DbType"]=>
    string(6) "string"
  }
  ["couleur"]=>
  array(1) {
    ["DbType"]=>
    string(6) "string"
  }
}

Pour finir, quelques petites références sur le sujet:

Et au final, au lieu de réinventer une roue bien vieille, je suis parti utiliser Addendum:

Se connecter proprement à ssh via un proxy web

Cet article est issu d’une précédente version de mon weblog, mais perdu dans l’oubli. Il remonte à Avril 2008. Je le remets ici à jour.

Un problème au taf, si ce n’est le prix du café au distributeur, est la non possibilité de se connecter sur sa box avec son ssh. Et bien, Dag Wiers propose une solution propre (qui n’oblige pas à installer son sshd sur le 443 et d’utiliser corkcrew), mais d’utiliser proxytunnel qui marche plutot bien.

Pour résumer la chose, cela implique d’avoir un serveur avec un apache où on a la main dessus, mod_proxy/mod_proxy_connect et mod_proxy_http d’installés, possiblement mod_ssl (mais ça crée un petit problème de taille à cause d’une non-feature d’apache2), et un peu de configuration.

N’ayant pas la main sur mon serveur au taf, c’est pas facile pour tester. J’ai donc aussi installé privoxy en local pour faire mes tests. Par contre, après utilisation, j’ai pas réussi à faire passer un CONNECT sur un port de destination 80, privoxy ne voulait pas, et le limit-connect semblait pas vouloir être changé. En même temps il était 5h du mat, j’ai peut être oublié un truc.

Alors, voilà la conf que j’ai faite sur ma debian (serveur ssh):

Server ssh sur le 22 et 2222;
Apache2 de base installé (bon, avec php mais là on s’en fiche);

On configure le bouzin:

remote# a2enmod ssl
remote# a2enmod proxy
remote# a2enmod proxy_connect

Il faut générer un certificat (.pem) pour notre nouveau serveur https. Pour çà, y a un super outil dans debian, et c’est make-ssl-cert:

remote# make-ssl-cert /usr/share/ssl-cert/ssleay.cnf /etc/apache2/ssl/apache.pem
remote# chmod a+r /etc/apache2/ssl/apache.pem

Et un virtualhost sur le 443, qui inclue directement les directives pour mod_ssl et mod_proxy:

<VirtualHost spine.minithins.net:443>
  SSLEngine On
  SSLCertificateFile /etc/apache2/ssl/apache.pem

  ServerAdmin mycroft@minithins.net
  DocumentRoot /home/web/minithins.net
  ServerName cns.minithins.net

  HostnameLookups On

  ProxyRequests on
  AllowCONNECT 2222
  ProxyVia on

  <proxy *>
    Order deny,allow
    Deny from all
    Allow from client.monkeyz.eu
  </proxy>
</VirtualHost>

Un redémarrage d’apache, et on va tester avec stunnel:

remote# service apache2 restart
Restarting web server: apache2 ... waiting .
client# apt-get install stunnel
...
client# stunnel -f -c -d 12345 -r spine.minithins.net:443
2011.04.02 12:22:22 LOG5[11844:140613013587712]: stunnel 4.29 on x86_64-pc-linux-gnu with OpenSSL 0.9.8o 01 Jun 2010
2011.04.02 12:22:22 LOG5[11844:140613013587712]: Threading:PTHREAD SSL:ENGINE Sockets:POLL,IPv6 Auth:LIBWRAP
2011.04.02 12:22:22 LOG5[11844:140613013587712]: 500 clients allowed
...

Sur une autre console:

client# telnet localhost 12345
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
CONNECT spine.minithins.net:2222 HTTP/1.0

Connection closed by foreign host.

Hum. Ca ne fonctionne pas.

Un peu de recherche (ou de triche, car Dag Wieers donne la réponse), Apache, ou plus précisement le mod_proxy_connect est buggé. Mais un patch existe. Il faut appliquer un des patchs proposés (surement le dernier en date, dans mon exemple celui pour 2.2.16), et lancer un coup de rebuild des packages apache via les scripts debian:

remote# cd /tmp
remote# apt-get install devscripts fakeroot gcc bzip2 dpkg-dev
...
remote# apt-get build-dep apache2.2-common
...
remote# apt-get source apache2.2-common
...
remote# cd apache2-2.2.16/modules/proxy
remote# wget -O mod_proxy_connect.diff "https://issues.apache.org/bugzilla/attachment.cgi?id=26225"
remote# patch -p0 < mod_proxy_connect.diff

… et c’est parti pour tout rebuilder: …

remote# debuild -us -uc
remote# cd /tmp/apache2-2.2.16
... (long) ...

… et finalement on met à jour le package apache2.2-bin:

remote# dpkg -i ../apache2.2-bin_2.2.16-6+squeeze1_amd64.deb
(Reading database ... 48179 files and directories currently installed.)
Preparing to replace apache2.2-bin 2.2.16-6+squeeze1 (using .../apache2.2-bin_2.2.16-6+squeeze1_amd64.deb) ...
Unpacking replacement apache2.2-bin ...
Setting up apache2.2-bin (2.2.16-6+squeeze1) ...
Processing triggers for man-db ...
remote # service apache2 restart
Restarting web server: apache2 ... waiting .

On reteste avec stunnel:

client# telnet localhost 12345
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
CONNECT spine.minithins.net:2222 HTTP/1.0

HTTP/1.0 200 Connection Established
Proxy-agent: Apache/2.2.16 (Debian)

SSH-2.0-OpenSSH_5.5p1 Debian-6

Yay ! First step donne. Plus qu’à tester via un proxy http et utiliser le client ssh.

Pour tester, j’installe en local un privoxy:

client# apt-get install privoxy

On pourra modifier l’adresse d’écoute de privoxy (localhost:8118), et le modifier (par exemple, 127.0.0.1:8080 pour éviter qu’il prenne l’adresse ipv6 au lieu de la v4).

On crée un profil ssh en accord, dans ~/.ssh/config:

Host spine-proxy
    DynamicForward 1080
    ServerAliveInterval 30
    # proxy.dev pointe vers localhost car mon proxy est sur localhost.
    ProxyCommand proxytunnel -v -X -p proxy.dev:8118 -r spine.minithins.net:443 -d %h:%p -H "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Win32)\n"

On notera dans mon exemple que j’utilise le flag -X. Il permet de préciser que j’utilise une connection sécurisée ssl entre les deux proxy (entre mon proxy et le mod_proxy de mon apache). Il est possible que le proxy que vous utiliserez ne l’aime pas. Dans ce cas, n’hésitez pas à joeur avec -X, -e et -E).

Et si on restestait:

client# ssh spine-proxy
...
Tunneling to spine-proxy:2222 (destination)
Communication with remote proxy:
 -> CONNECT spine-proxy:2222 HTTP/1.0
 -> Proxy-Connection: Keep-Alive
 -> User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Win32)\n
 <- HTTP/1.1 502 Proxy Error
HTTP return code: 502 Proxy Error
...

What ?!
Dans les logs, on voit:

[error] [client 78.229.134.1] proxy: DNS lookup failure for: spine-proxy returned by spine-proxy:2222

Il n’aime pas trop que j’utilise « spine-proxy » comme alias. Solution, retirer le -d %h:%p (et le remplacer par le vrai host de la machine), ou changer l’alias. Après correction:

client# ssh -v spine
OpenSSH_5.5p1 Debian-4ubuntu5, OpenSSL 0.9.8o 01 Jun 2010
...
debug1: Executing proxy command: exec proxytunnel -v -X -p proxy.dev:8118 -r spine.minithins.net:443 -d spine:2222 -H "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Win32)\\n"
...
SSL local to remote proxy enabled
Local proxy proxy.dev resolves to 127.0.0.1
Connected to proxy.dev:8118 (local proxy)

Tunneling to spine.minithins.net:443 (remote proxy)
Communication with local proxy:
 -> CONNECT spine.minithins.net:443 HTTP/1.0
 -> Proxy-Connection: Keep-Alive
 -> User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Win32)\n
 <- HTTP/1.0 200 Connection established
 <- Proxy-Agent: Privoxy/3.0.16


Tunneling to spine:2222 (destination)
Communication with remote proxy:
 -> CONNECT spine:2222 HTTP/1.0
 -> Proxy-Connection: Keep-Alive
 -> User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Win32)\n
 <- HTTP/1.0 200 Connection Established
 <- Proxy-agent: Apache/2.2.16 (Debian)

Tunnel established.
debug1: Remote protocol version 2.0, remote software version OpenSSH_5.5p1 Debian-6
...
remote#

Et ça marche ! Rock’n'Roll ! :°)

Python: Chargement de fichiers sources en live

Pour BUTT j’ai voulu un mini système de plugins/modules afin de rajouter ou modifier du code sans redémarrer mon process. Et Python propose nativement cela grâce au module imp. On va utiliser spécifiquement la procédure load_source qui permet de charger un fichier source, de le parser, et de retourner son objet Module.

Illustrons. Voici le code du très simple module à importer:

#!/usr/bin/env python

class TestModule:
    def __init__(self):
        pass

    def myfunction(self):
        print "Calling myfunction"

Procédure de chargement du module:

#!/usr/bin/env python

import os, shutil
import imp

def load_module(module_name):
    print "Loading module %s" % module_name

    modfile = os.path.join(os.getcwd(), module_name + '.py')

    module = imp.load_source(module_name, modfile)

    print module
    # Returns <module 'test' from '/home/mycroft/imp/test.pyc'>

    print dir(module)
    # Permet de lister les differents attributs de l'objet importé.
    # Returns ['TestModule', '__builtins__', '__doc__', '__file__', '__name__', '__package__']

    for objname in dir(module):
        # Je veux specifiquement les modules suffixés "Module":
        if objname.endswith('Module'):
            modclass = getattr(module, objname)
            print modclass
            # Returns test.TestModule
            print dir(modlass)
            # Returns ['__doc__', '__init__', '__module__', 'myfunction']
            return modclass

    return None

Il ne reste plus qu’à montrer par l’exemple le fonctionnement et le reload d’un module dans le main:

if __name__ == '__main__':

    modclass = load_module('test')
    instance = modclass()
    instance.myfunction()

    # We move away test.py, and copy test_new.py to test.py.

    print

    shutil.move('test.py', 'test_old.py')
    shutil.move('test_new.py', 'test.py')

    modclass = load_module('test')
    instance = modclass()
    instance.myfunction()

    shutil.move('test.py', 'test_new.py')
    shutil.move('test_old.py', 'test.py')

Qui nous donnera:

$ ./modules.py
Loading module test
<module 'test' from '/home/mycroft/imp/test.py'>
['TestModule', '__builtins__', '__doc__', '__file__', '__name__', '__package__']
test.TestModule
['__doc__', '__init__', '__module__', 'myfunction']
Calling myfunction

Loading module test
<module 'test' from '/home/mycroft/imp/test.py'>
['TestModule', '__builtins__', '__doc__', '__file__', '__name__', '__package__']
test.TestModule
['__doc__', '__init__', '__module__', 'myfunction']
Calling (new) myfunction

Le code modifié (ici test.py a été remplacé par test_new.py, qui contient du code modifié) a bien été relu par python et est à présent utilisé.

Ecrire un bot IRC grâce à Python et Twisted

Quand il faut écrire une application réseau, les développeurs Python n’ont qu’un mot à la bouche: Twisted. Il s’agit d’un moteur réseau « event-driven », et comprend un grand nombre de protocoles de niveau 3 (TCP, UDP, socket unix) et 7 (HTTP, SSH, IMAP, IRC…).

On va donc simplement voir ici comment coder un bot irc (quasi) complet. On prend la doc « Writing Clients » et celle de la classe irc.IRCClient.

On a donc besoin d’écrire une classe de base pour le bot, et sa factory utilisable dans le reactor de twisted.

#!/usr/bin/env python

import sys

from twisted.words.protocols import irc
from twisted.internet import protocol
from twisted.internet import reactor

class QuickBot(irc.IRCClient):
    def _get_nickname(self):
        return self.factory.nickname
    nickname = property(_get_nickname)

    def signedOn(self):
        self.join(self.factory.channel)
        pass

    def privmsg(self, user, channel, msg):
        pass

class QuickBotFactory(protocol.ClientFactory):
    protocol = QuickBot

    def __init__(self, channel, nickname):
        self.channel = channel
        self.nickname = nickname

    def clientConnectionLost(self, connector, reason):
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        connector.connect()

Et le point d’entrée:

if __name__ == "__main__":
    reactor.connectTCP('localhost',
                       6667,
                       QuickBotFactory('#bot', 'quicky'))
    reactor.run()

Et du coup, j’ai un peu étendu le truc et j’ai créé Another Python Bot Using The Twisted Framework, aka Butt, avec un système de modules chargeables dynamiquement (avec imp) et l’utilisation de python-yaml pour les fichiers de conf. Un bon exemple pour commencer un bot IRC ! Les sources sont disponibles sur le projet Butt sur mon compte Github.

Mise en commun d’un serveur git via ssh et mise à disposition à tous

Je suis en train de développer sygit, une interface web light pour naviguer dans un dépôt git. J’ai alors décidé de mettre au clair mes dépots et de monter ma propre architecture d’hosting git, sous debian.

Emplacement des dépôts

Les dépôts vont être accessible via ssh (lecture+écriture) et via git-daemon (par dessus d’inetd, en lecture seule). On décide de créer un groupe « git-users » pour ceux qui auront accès aux dépots. On édite donc /etc/group:

git-users:x:199:mycroft,patrick

Le dépôt sera physiquement dans /git. On le crée (et on n’oublie pas le –bare !):

# mkdir -p /git/newproject
# cd !-1:2
# git --bare init
Initialized empty Git repository in /git/newproject/

On pourra également réutiliser un projet déjà existant dont on créerait un dépôt bare:

# cd /git
# git clone --bare git://github.com/mycroft/sygit.git
Cloning into bare repository sygit.git...
remote: Counting objects: 48, done.
remote: Compressing objects: 100% (47/47), done.
remote: Total 48 (delta 21), reused 0 (delta 0)
Receiving objects: 100% (48/48), 8.77 KiB, done.
Resolving deltas: 100% (21/21), done.

Pour ces projets, il faudra changer la configuration et leur indiquer qu’il s’agit d’un dépôt partagé sur la base du groupe. Pour cela, il faut modifier la propriété core.sharedRepository:

# cd sygit.git
# git repo-config core.sharedRepository group

On n’a plus qu’à fixer les permissions manuellement:

# chgrp -R git-users /git/sygit.git
# find /git/sygit.git -type d -exec chmod g+ws {} \;

Mise en place de git-daemon

On utilise inetd pour partager les dépôts. On édite donc /etc/inetd.conf

git stream tcp nowait nobody /usr/bin/git git daemon --inetd --verbose --export-all --base-path=/git /git

… et on redémarre le daemon:

/etc/init.d/openbsd-inetd restart

Testons !

Sur ma machine de travail, on clone le dépôt en read-only:

$ git clone git://mkz.me/sygit.git
Initialized empty Git repository in /tmp/testgit/sygit/.git/
remote: Counting objects: 48, done.
remote: Compressing objects: 100% (26/26), done.
remote: Total 48 (delta 21), reused 48 (delta 21)
Receiving objects: 100% (48/48), 8.77 KiB, done.
Resolving deltas: 100% (21/21), done.

$ cd sygit
$ git remote -v
origin  git://mkz.me/sygit.git (fetch)
origin  git://mkz.me/sygit.git (push)

Pour le moment, le seul remote configuré est le serveur git. Celui là étant en read-only, il va faloir le modifier pour pouvoir push:

$ git remote rm origin
$ git remote add origin mycroft@mkz.me:/git/sygit.git
$ git remote -v
origin  mycroft@mkz.me:/git/sygit.git (fetch)
origin  mycroft@mkz.me:/git/sygit.git (push)

Il ne reste plus qu’à modifier un fichier, le commiter et le pusher:

$ vim README
$ git add README
$ git commit -m "Update README"
[master 2d98128] Update README
 1 files changed, 6 insertions(+), 0 deletions(-)
$ git push origin master
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 375 bytes, done.
Total 3 (delta 2), reused 0 (delta 0)
To mycroft@mkz.me:/git/sygit.git
   826d9b0..2d98128  master -> master

Et voilà !