lundi 16 novembre 2009

Quelle librairie utilisez vous pour generer vos flux Atom et RSS ?

Jusqu'à présent, j'utilisais Zend_Feed pour générer mes flux RSS et Atom. Tout allait très bien jusqu'à ce que je veuille publier un flux sur un hub PubSubHubbub.

Pour rappel, voici quelle tête doit avoir un flux Atom pour cette utilisation :

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
...
<link rel="self" href="http://example.com/mon-flux.rss" />
<link rel="hub" href="http://pubsubhubbub.appspot.com" />
<link rel="hub" href="http://superfeedr.com/hubbub" />
...
</feed>

Comme vous pouvez le constater, il est nécessaire de définir un élément <link> supplémentaire avec l'attribut rel="hub" pour chaque hub que vous voulez utiliser.

En RSS, c'est le même principe en utilisant le namespace atom (cf le wiki pubsubhubbub) :

<?xml version="1.0"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<link>http://example.com/mon-flux.atom</link>
<atom:link rel="hub" href="http://pubsubhubbub.appspot.com"/>
<atom:link rel="hub" href="http://superfeedr.com/hubbub"/>
...
</channel>
</rss>

Le problème, c'est que je n'ai trouvé aucune librairie de génération de flux me permettant de faire cela. J'ai donc finalement utilisé la classe native php DOMDocument pour générer mes flux Atom et RSS. Je trouve cela dommage... d'où mon article.

Et vous, quelle librairie utilisez vous ? En connaissez vous une qui puisse gérer ce cas ?

jeudi 12 novembre 2009

Présentation de PubSubHubbub

J'ai récemment découvert PubSubHubbub : un protocole simple et redoutable permettant d'apporter du temps réel à la publication de flux RSS / Atom.

Brad Fitzpatrick, l'un des concepteurs de ce protocole, propose une description assez imagée du principe, dont voici l'adaptation en français :

Imaginez vos enfants à l'arrière de la voiture en train de demander sans cesse "Quand est-ce qu'on arrive ? Quand est-ce qu'on arrive ?". La ferme les enfants ! je vous dirais quand on y sera !

Ce phénomène, on l'appelle Hubbub en anglais (a confused noise of many voices). Sauf que dans notre cas, les enfants, ce sont en fait des sites Web en train de se parler. La façon dont les sites se parlent de nos jours ressemble à ça : "Du nouveau ? Non. Du nouveau ? Non. Du nouveau ? Non. Du nouveau ? Non".

PubSubHubbub corrige ce problème : les sites se parlent uniquement lorsqu'il y a du changement.

Dans ce système, on peut distinguer trois type d'acteurs :
  • Les "publishers" : ils publient du contenu (sous forme de flux Atom ou RSS)
  • Les "subscriber" : ils s'abonnent à des flux et sont informés à chaque mise à jour (sous forme de web hooks)
  • Les "hub" : c'est l'intermédiaire, qui s'occupe de transmettre aux subscriber les notifications de mise à jour des flux auxquels ils sont abonnés

Comme je le disais en introduction, cela permet d'ajouter une touche de temps réel à ce monde de flux ! Voyons voir maintenant comment cela fonctionne dans la vraie vie ;-)

On va commencer par une bonne nouvelle : vous n'êtes pas obligés d'héberger votre propre Hub pour jouer avec PubSubHubbub. Voici une liste de hubs publiques. Les deux plus populaires sont :

En tant que publisher, c'est extrêmement simple.
  1. Selectionnez un ou plusieurs hubs auxquels vous allez déléguer la distribution de votre flux. Je vous conseille d'en prendre plusieurs, en cas d'indisponibilité de l'un d'entre eux.
  2. Indiquez dans votre flux RSS ou Atom l'url de votre hub. Si vous avez un flux RSS, modifiez le de cette façon pour ajouter le namespace atom et un element <atom:link> :
    <?xml version="1.0"?>
    <rss xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
    <atom:link rel="hub" href="http://pubsubhubbub.appspot.com"/>
    ...
    </channel>
    </rss>
    
    Si vous avez un flux Atom, il suffit de rajouter un element link a votre feed :
    <?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
    <link rel="hub" href="http://myhub.example.com/endpoint" />
    ...
    </feed>
    
  3. A chaque modification de votre flux, informez votre hub avec la librairie PubSubHubbub de votre language préféré. Voyons à quel point c'est difficile en PHP :
    $p = new Publisher('http://superfeedr.com/hubbub');
    $p->publish_update('http://example.com/url-de-mon-flux.rss');
    

Pour la partie subscriber que je n'ai pas encore abordé, je vous invite à consulter un exemple d'implémentation en PHP sur le repository de PubSubHubbub. Pour cette partie, méfiez vous car ce n'est pas si simple qu'il n'y parait : touts souscription à un flux possède une date d'expiration. Il faut relancer une requête de souscription avant expiration afin de renouveller son abonnement. Je vous invite à consulter les spécifications du protocole pour plus de détails.

Prochainement (dans moins d'un an cette fois-ci, promis !) j'aborderais un problème que j'ai rencontré avec PHP pour mettre ce système en place.

Edit (11/02/2009) : Bonne nouvelle : Google Buzz, le nouveau service de Google, est un subscriber PubSubHubbub ! Si vos flux sont publiés sur un hub, leur contenu pourra donc apparaitre instantanément dans Google Buzz !

dimanche 9 novembre 2008

Contrôler le temps d'execution maximum d'un traitement PHP

Il est parfois nécessaire d'exécuter un traitement PHP avec une contrainte de temps maximum à ne pas dépasser. Dans cette article je vais vous proposer une solution à ce problème.

Voici un petit exemple impossible que nous allons tenter d'implémenter : "Faites ce que vous voulez dans une boucle infinie. Si au bout de 5 secondes vous n'avez pas terminé, stoppez le traitement".

Voici une classe qui va s'occuper de ce travail :

class   MissionImpossible
{
  public function start()
  {
    while (42)
      {
        echo "processing nothing...\n";
        sleep(1);
      }
  }

  public function abort()
  {
    throw new Exception("Time limit ! job aborted");
  }
}

Maintenant, passons aux choses intéressantes : comment s'y prendre pour appeler la fonction abort() au bout de 5 secondes, alors que la fonction start() est en train de s'executer ?

Nous allons utiliser le mécanisme de signaux POSIX pour interrompre le processus et l'informer que le temps qui lui était alloué arrive à terme. Le signal SIGALRM est parfaitement adapté à ce cas d'utilisation.

Deux fonctions vont nous être utiles en PHP :

  • pcntl_signal qui permet de definir quelle fonction va etre appelée lorsqu'un signal particulier est reçu.
  • pcntl_alarm qui permet de mettre en place un compte a rebours, qui lorsqu'il est terminé envoie un signal SIGALRM au processus courant.

Voici donc comment résoudre notre problème :
declare(ticks = 1);
require('MissionImpossible.php');

$job = new MissionImpossible();
pcntl_signal(SIGALRM, array($job, "abort"), true);
pcntl_alarm(5);
try {
  $job->start();
}
catch (Exception $e)
{
  echo "Exception caught. Stopping execution\n";
}

Quelques commentaires :

  • Lignes 1 et 2 : l'appel à la fonction declare est indispensable pour que le mécanisme de signaux puisse fonctionner en PHP. Il doit être fait impérativement avant la déclaration de la classe MissionImpossible.
  • Ligne 5 : mise en place du gestionnaire de signaux : la fonction abort() de l'instance $job de la classe MissionImpossible sera appelée dès qu'un signal SIGALRM sera reçu
  • Ligne 6 : mise en place du compte à rebours. Dans 5 secondes, un signal SIGALRM sera envoyé au processus courant
  • Dans la fonction abort(), une exception est lancée. Sans elle, le traitement de la fonction start() aurait continué dès la fin de la fonction abort().

Voilà, c'est tout simple. Mais sachez que les fonctions de l'extension pcntl ont quelques inconvénients : (cf doc officielle)

  • "Cette extension ne doit pas être activée pour une utilisation en serveur web, car les résultats pourraient être inattendus"
  • "Cette extension n'est pas disponible sur les plates-formes Windows."
Si cela vous pose problème, regardez du coté de register_tick_function (qui a lui aussi quelques inconvenients : " [...] ne doit pas être utilisé avec les modules de serveur web threadé. Les ticks ne fonctionnent pas en mode ZTS et peuvent interrompre votre serveur web.")

Enfin, cet exemple était vraiment simpliste mais sachez que c'est applicable pour tout traitement lourd et "réversible" (par exemple une grosse transaction MySQL).

vendredi 26 septembre 2008

Parlons d'ergonomie

L'une des règles d'ergonomie à appliquer lorsque l'on fait un site web est d'utiliser un vocabulaire standard pour que les visiteurs ne soient pas perdus. Mais... comment choisir correctement ses mots ?

La méthode classique : constituer une liste de mots "éligibles", parcourir le web pour trouver lesquels sont les plus populaires, puis enfin faire son choix. Au fur et à mesure, on accumule de l'expérience et se fait son propre dictionnaire de mots à utiliser.

Ne serait-il pas intéressant de cumuler nos expériences sur un site qui servirait de dictionnaire de référence en la matière ? Fonctionnant de la même manière qu'un dictionnaire de synonymes, il pourrait pour chaque mot donner les alternatives possibles et leur popularité, ainsi qu'une liste de sites qui les utilise. Ce serait un gain de temps pour nous tous.

Exemple:

Deconnexion
Utilisation recommandée
Alternatives: Quitter, Va-voir-ailleurs-si-j'y-suis!

Si ce service existe déjà, merci de me donner l'URL ;-) Sinon je trouve que ce serait un bon projet... Je n'ai malheureusement pas le temps de me lancer dessus. Si ça tente quelqu'un, l'idée attend au chaud !

samedi 5 juillet 2008

CSRF : Sea, Surf and Zend

Note: Si vous n'utilisez pas Zend Framework et que le titre vous a fait peur, ne fuyez pas, je ne parle de ZF qu'à la fin.

Au programme d'aujourd'hui, un peu de prévention ! Comme vous l'avez remarqué, l'été pointe enfin le bout de son nez et il devient difficile de rester concentré devant son écran. On parle souvent d'insolations, de déshydratations, mais n'oubliez pas non plus qu'une seule seconde d'inattention peut mener à une faille de sécurité dans votre code !

C'est pourquoi j'aimerais vous parler des failles de type CSRF. Si cela ne vous évoque rien ou pas grand chose, je vous conseille d'aller vous renseigner sur le sujet, de nombreux articles expliquent le problème.

Comment s'en protéger ? Tout d'abord je vous conseillerais de boire de l'eau (sans aucun additif anisé) lorsque vous codez. Mais ça ne suffit pas ! Les techniques suivantes sont souvent utilisées pour se protéger contre les CSRF :

  • Utiliser la méthode HTTP POST (ou UPDATE ou DELETE) pour toutes les actions qui ne sont pas de la consultation de ressource (insertion, mise à jour ou suppression). D'une part, vous protégera légèrement contre le CSRF, d'autre part ça sera le premier pas vers une architecture RESTful
  • Utiliser des token de sécurité (aussi appelés nonce ou encore bordel pour paranoïaque) Kézako ? En gros lorsqu'un utilisateur affiche un formulaire, on lui génère une clé. Cette clé sera valide pour un certain temps (5 min en général), sera liée uniquement au couple utilisateur/formulaire et devra automatiquement être transmise avec le formulaire pour que celui ci puisse être validé !

En général, cela évite les dégats. Par contre méfiez vous car si vous êtes également vulnérable à une faille de type XSS, ces protections ne changeront absolument rien ! Ce serait aussi efficace que d'installer une porte blindée à la place de la fermeture d'une tente de camping en toile. Un simple cure dent permettrait de désintégrer votre site web.

Donc techniquement, pour se protéger, il faut agir à deux niveaux :

  1. Affichage du formulaire
    • Générer un token valide pour cet utilisateur et ce formulaire [1]
    • Stocker ce token quelque part [2]
    • Ajouter la valeur du token dans un champ caché du formulaire
  2. Validation du formulaire
    • Ne traiter que les données qui ont ete envoyées par POST (ou UPDATE ou DELETE)
    • Récupérer le token valide quelque part [2] et vérifier qu'il n'est pas expiré
    • Comparer ce token à celui qui a été POSTé

Simple, non ? Étudions maintenant deux points :

  1. Le token ne doit pas être prédictible. Je vous propose d'utiliser la formule suivante :

    MD5( identifiant_formulaire . server_secret . assaisonnement)
    identifiant_formulaire est une chaîne représentant le formulaire concerné
    server_secret est une clé connue uniquement par le serveur
    assaisonnement est un nombre aléatoire (entre 1 et beaucoup, par exemple)

  2. Tout est possible, tout est imaginable...
    • Sessions natives PHP
    • Memcache
    • Base de données (MySQL, Postgresql, sqlite, Berkeley DB, Memcachedb)
    • Cookie sécurisé (c'était juste pour les citer mais je vous les déconseille pour cette utilisation. Si vous avez beaucoup de formulaires ça va créer beaucoup de données qui transiteront entre l'utilisateur et le serveur à chaque requête)
    ... à condition bien sur que personne ne puisse altérer les données que vous y avez stocké.

Si sauvegarder les token en session vous convient, le Zend Framework propose une solution qui fait exactement tout ça : Zend_Form_Element_Hash.

Moi je n'aime toujours pas les sessions, donc je vais préférer Memcache pour y stocker les token. Memcache fournit nativement le système d'expiration, c'est rapide, et c'est pratique a utiliser dans un environnement multi-serveur. Note: Il y a un risque que certains token soient perdu, compte tenu du mode de fonctionnement de Memcache. Mais je vais prendre ce risque compte tenu de leur faible durée de vie. De plus, je vais surveiller l'utilisation de mémoire de memcache pour éviter le problème.

J'ai donc créé une classe qui ressemble beaucoup à Zend_Form_Element_Hash, sauf qu'elle implémente un mécanisme de "Storage Adapter" qui permet de stocker les tokens n'importe où. J'ai également créé un Adapter qui permet de stocker les données dans Memcache, mais libre à vous de créer les Adapter de vos rêves (j'étudie actuellement une solution de stockage des token dans un bigorneaux fossilisé, mais c'est pas très scalable).

C'est ici pour télécharger les classes BigOrNot_Form_Element_Token*

Exemple d'utilisation :

// Initialisation du formulaire
$form = new Zend_Form ...

// Initialisation de Memcache
$memcache = new Memcache ...

// Recuperation de l'identifiant de l'utilisateur
$uniqueUserID = Zend_Auth::getIdentity();

// Mise en place du token de protection
$storage = new BigOrNot_Form_Element_Token_Storage_Memcache($memcache, $uniqueUserID);
$tokenElement = new BigOrNot_Form_Element_Token('formulaire_ajout_bigorneaux', array('adapter' => $storage));
$form->addElement($tokenElement);

Enjoy !

Pour conclure, une petite quote d'un papier (Advanced CRSF) de l'Epitech Security Lab :

Tant que le développement web sera fait par des gens non sensibilisés aux plus évidentes règles de sécurité, que des langages à pièges comme PHP seront “maitrisés” entre deux cours de géographie, il va falloir se faire à l’idée de surfer avec netcat et lire ses mails avec gnus.

Une solution ? Faites circuler cet article dans la boîte au lettre de tous vos voisins (sinon votre serveur Web va perdre ses dents et votre système va mourir d'un kill -9 -1)

;-)

lundi 23 juin 2008

Une classe de protection contre le flood

Il est très courant d'être victime de flood pour certaines fonctionnalités proposées par votre site.

Exemples :

Que penseriez vous d'une classe qui permettrait de bloquer (ou plutôt ralentir) les tentatives de flood sur les fonctionnalités sensibles de votre site ? (ex: pas plus d'1 post sur le forum toutes les 30 secondes).

Pour cela, nous allons utiliser memcache. Si vous ne connaissez pas memcache, c'est un système de cache distribué extrèmement rapide. Comme tout système de cache, il offre un mécanisme de stockage, de récupération et d'expiration des données.

Pourquoi memcache ?
C'est un outil pratique aussi bien dans un environnement multiserveur que sur un seul serveur, donc il est accessible à un grand nombre de personnes (désolé ca n'inclu pas les comptes de type "mutualisés" grand public).
Il propose également un mécanisme d'expiration qui correspond parfaitement à nos besoins pour ce système de flood-control.

Principe :
l'extension memcache pour PHP propose une fonction Memcache::add() qui permet d'ajouter une donnee au cache si et seulement si celle-ci n'existe pas déjà.
A chaque action d'un utilisateur que nous voulons limiter, il suffit donc d'ajouter une information dans le cache avec une date d'expiration correspondant au temps d'attente necessaire avant la prochaine action. Si la fonction add() renvoie une erreur, c'est qu'une action a déjà été effectuée durant cette intervalle de temps.

Scénario type : (flood control sur un forum, 1 post toutes les 30 sec)
11:00:00 : jean-craoude much poste un message sur le forum. Le forum ajoute une entrée au cache qui a pour ID "ForumPost:JeanCraoudeMuch" et pour date d'expiration 30 secondes.
11:00:15 : jean-craoude Much s'endort malencontreusement sur la touche F5 de son clavier. Toutes les nanosecondes, une requête est donc envoyée au serveur pour poster à nouveau le même message. Le forum va donc pour chaque requête tenter de remettre en cache une donnée ayant pour ID "ForumPost:JeanCraoudeMuch", mais celle-ci existant déjà, la fonction Memcache::add() va renvoyer une erreur. Le post sera donc refusé.
11:00:29 : jean-craoude Much se réveille et se regarde dans le mirroir. Voyant un F5 rouge se dessiner sur son front, il se précipite sur le forum pour voir l'etendue de ses dégats. Fort heureusement, tout va bien, il est rassuré. Il se rendort donc consciencieusement, mais cette fois-ci à côté du clavier

Bref, si vous êtes séduit par ce système, voici une petite classe qui vous permettra de le mettre en place très facilement : BigOrNot_FloodControl

Exemple d'utilisation :

/* Initialisation d'un objet memcache */
$cache = new Memcache;
$cache->addServer('127.0.0.1');

/* Initialisation de la classe flood control */
$floodControl = new BigOrNot_FloodControl($cache);

/* ... */

/* Utilisation de la classe pour limiter les posts dans un forum */
$userId = Forum::getUserId();
if ($floodControl->isAllowed('posterUnMessageDansLeForum', $userId, 30))
{
    posterMessage();
}
else
{
    messageErreur();
}

Voila, c'est simple et efficace. Inutile de s'armer d'une base de données pour ce genre de choses.

Notes :

  • Avec cette solution, il n'y a pas la possibilité de faire des restrictions avec une intervalle inférieure à 1 seconde. Pour les amateurs de nanosecondes, cela nécessitera quelques modifications.
  • Je n'ai pas utilisé Zend_Cache car je n'ai pas trouvé de mécanisme équivalent au add() de Memcache. Il y a probablement moyen de mettre en place un système de lock qui y ressemblerait, mais ca ne m'enchantait pas trop... Alors si l'envie vous prend, n'hesitez pas ;) Méfiez vous des appels simultanés à votre système

dimanche 15 juin 2008

Zend_Auth et le cookie masqué

Je vais encore vous parler de cookies, mais cette fois-ci à la sauce Zend_Auth.

Je vous ai présenté dans un précédent article une classe permettant de gérer des cookies sécurisés. Maintenant nous allons voir comment l'utiliser dans un cas très courant : l'authentification avec Zend_Auth.

Rappelons les grandes lignes de Zend_Auth :

Si comme moi vous n'utilisez les sessions que pour stocker l'identité des utilisateurs authentifiés (leur identifiant, un simple nombre entier en général), vous allez peut être trouver que c'est du gachi. Les sessions c'est lourd et ennuyeux à gérer quand l'application en question tourne sur une ferme de serveurs web (qui font meuh meuh en core et en core).

Bref, ne serait-il pas tentant de déléguer cette tâche au client ? Qu'il présente lui même son identité ? C'est risqué vous allez dire... et vous avez raison !
On peut déjà imaginer les pires scénarios :

Serveur> Bien le bonjour ! A qui ai-je l'honneur ?
Client (de type malicieux)> Je suis l'administrateur du site ! Regarde j'ai... ma bonne parole pour te le prouver !
Serveur> Salut chef je t'avais pas reconnu. Voici les clefs de ton royaume.

Techniquement, ca reviendrait a stocker l'user_id dans un cookie et de faire complètement confiance à ce cookie.

En général ce genre de scénarios est très peu apprécié par le public (en particulier celui de votre site). Donc on va tenter d'utiliser quelques ruses cryptographiques pour confier à l'utilisateur le soin de présenter son identité, sans qu'il puisse la modifier : on va la stocker dans un cookie sécurisé grâce à la classe BigOrNot_CookieManager.

J'ai donc écrit une classe qui implémente l'interface Zend_Auth_Storage_Interface pour stocker l'identité des utilisateurs dans un cookie sécurisé.

Vous pouvez la télécharger ici (l'archive contient également la classe BigOrNot_CookieManager).

Voici comment l'utiliser :

$cookieManager = new BigOrNot_CookieManager('SECRET_KEY');
$authStorage = new BigOrNot_Auth_Storage_Cookie($cookieManager);
$auth = Zend_Auth::getInstance();
$auth->setStorage($authStorage);

[...]

Par defaut, le cookie s'appelle "auth" et les paramètres par défaut sont envoyes a setcookie().

Si vous souhaitez modifier ces paramètres, il est possible d'envoyer au constructeur de la classe BigOrNot_Auth_Storage_Cookie un deuxième paramètre : un tableau (ou un objet Zend_Config) de configuration.

Les paramètres de configuration supportés sont : cookieName, cookieExpire, cookiePath, cookieDomain, cookieSecure, cookieHttpOnly. (Les noms sont assez explicites, voir la doc de setcookie() si vous avez des doutes).

Exemple d'utilisation avec configuration :

$cookieManager = new BigOrNot_CookieManager('SECRET_KEY');

$storageConfig = array(
    'cookieName' => 'BigOrNauth',
    'cookieExpire' => (time() + 3600),
    'cookiePath' => '/',
    'cookieDomain' => 'bigornot-fr.blogspot.com'
);

$authStorage = new BigOrNot_Auth_Storage_Cookie($cookieManager, $storageConfig);
$auth = Zend_Auth::getInstance();
$auth->setStorage($authStorage);

L'identité est stockée "serialisée" donc vous pouvez y stocker toute valeur serialisable. Evitez tout de même les gros objets, les cookies trop gros sont vite écoeurants. N'oubliez pas qu'ils sont transmis à chaque requête.

L'identité est également stockée chiffrée, donc vous ne donnez aucune information confidentielle à l'utilisateur en utilisant cette technique.

Pour information :
Comme nous l'avons vu dans le précédent article, pour la methode BigOrNot_CookieManager::setCookie(), il est necessaire d'envoyer en paramètre un nom d'utilisateur (ou n'importe quel identifiant unique).
Dans la classe BigOrNot_Auth_Storage_Cookie, j'envoie un hash md5 de l'identite serialisee.