29 juillet 2009

Séparer les entêtes et le contenu d'une réponse HTTP

De nos jours, il est rare d'avoir à traiter directement un flux HTTP, tant il existe de surcouche d'APIs, de classe, de langage pour masquer ce protocole. Si par hasard, on se retrouve nez à nez avec une chaine HTTP voici une petite fonction qui découpera la réponse HTTP en un tableau contenant les entêtes et une chaine de caractères avec le contenu de la réponse (généralement du HTML)


    function parse_http_response ($string) 
    {

        $headers = array();
        $content = '';
        $str = strtok($string, "\n");
        $h = null;
        while ($str !== false) {
            if ($h and trim($str) === '') {                
                $h = false;
                continue;
            }
            if ($h !== false and false !== strpos($str, ':')) {
                $h = true;
                list($headername, $headervalue) = explode(':', trim($str), 2);
                $headername = strtolower($headername);
                $headervalue = ltrim($headervalue);
                if (isset($headers[$headername])) 
                    $headers[$headername] .= ',' . $headervalue;
                else 
                    $headers[$headername] = $headervalue;
            }
            if ($h === false) {
                $content .= $str."\n";
            }
            $str = strtok("\n");
        }
        return array($headers, trim($content));
    } 

15 juillet 2009

Serveur Webdav en PHP avec la classe HTTP_WebDAV_Server

Le protocole Webdav permet d'accéder en lecture/écriture à une arborescence de fichiers au travers du protocole HTTP.
Généralement on utilise ce protocole pour accéder un système de fichier distant.

De la même manière que l'on peut produire une page HTML sans qu'elle existe réellement sur le serveur HTTP, on peut simuler un système de fichier complet en Webdav. Comme pour générer du HTML on peut utiliser PHP pour produire le XML spécifique à Webdav. Pour éviter d'apprendre la norme Webdav, on peut utiliser des APIs. A ma connaissance, il existe 2 en PHP :

Si on regarde de plus près la première, elle semble obsolète (numéro de version, date de version, etc...) Au delà des apparences cette classe est parfaitement utilisable et elle fonctionne très bien. Elle est d'ailleurs utiliser dans de nombreux projets PHP qui proposent une interface Webdav.

Installation

Package PEAR non à jour

Bien que emballée sous forme d'un package PEAR, il est préférable de ne pas utiliser cette version. Le code source de la classe a évolué mais le package PEAR, lui, n'a jamais été régénéré. Il donc préférable d'aller chercher directement le code source dans SVN.

Depuis le SVN

Cette classe est très légère (3 fichiers). Donc pour l'utiliser, on peut récupérer à la main ces 3 fichiers (sans même passer par les commandes subversion) :

% mkdir -p HTTP/WebDAV/Tools/
% cd HTTP/WebDAV/
% wget http://svn.php.net/viewvc/pear/packages/HTTP_WebDAV_Server/trunk/Server.php?view=co -O Server.php
% cd Tools
% wget http://svn.php.net/viewvc/pear/packages/HTTP_WebDAV_Server/trunk/Tools/_parse_lockinfo.php?view=co -O _parse_lockinfo.php
% wget http://svn.php.net/viewvc/pear/packages/HTTP_WebDAV_Server/trunk/Tools/_parse_propfind.php?view=co -O _parse_propfind.php
% wget http://svn.php.net/viewvc/pear/packages/HTTP_WebDAV_Server/trunk/Tools/_parse_proppatch.php?view=co -O _parse_proppatch.php 

Environnement de Test

Serveur HTTP

Pour utiliser notre classe, il faut un serveur HTTP. Pour plus de souplesse dans la configuration, on peut utiliser Pkgi
Pour notre exemple on utilisera le fichier .htaccess, donc dans PKGI on indiquera htaccess dans le champ APACHE_OPTIONS

Fichiers

En plus des fichiers téléchargés, il nous faut 3 autres fichiers :

  • .htaccess
  • index.php
  • Test.php

Ce qui donne

`-----.htaccess
`-----index.php
`-----HTTP
       `-----WebDAV
              `-----Server.php
              `-----Server
              |     `-----Test.php
              `-----Tools
                    `-----_parse_lockinfo.php
                    `-----_parse_propfind.php
                    `-----_parse_proppatch.php

Configuration

Un serveur Webdav s'attend à répondre à des URLs pointant vers des répertoires et des fichiers sans aucun paramètre supplémentaire. Donc pour concentrer les accès (toutes les URLS) vers un point unique on utilisera le module Apache mod_rewrite que l'on configura avec ces instructions dans le fichier .htaccess :


<IfModule mod_rewrite.c>
RewriteEngine on 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php
</IfModule>

Le fichier index.php lance le serveur webdav.

<?php

_SERVER['SCRIPT_NAME'] = '';

$server->ServeRequest();

La classe HTTP_WebDAV_Server se base sur les variables systèmes SCRIPT_NAME et REQUEST_URI pour calculer le chemin webdav demandé, il est extrêmement important de modifier ces variables afin d'obtenir pour la suite un chemin correct. Dans le cas contraire, on risque d'obtenir pour toutes requêtes le chemin par défaut à savoir "/". Dans notre cas, il suffit de mettre à blanc la variable SCRIPT_NAME

La classe HTTP_WebDAV_Server_Test

La classe HTTP_WebDAV_Server s'utilise par dérivation, il faut donc la surcharger et coder les méthodes abstraites. A chaque méthode correspond (en gros) une action webdav :

  • GET() get a resource from the server
  • HEAD() get resource headers only from the server
  • PUT() create or modify a resource on the server
  • COPY() copy a resource on the server
  • MOVE() move a resource on the server
  • DELETE() delete a resource on the server
  • MKCOL() create a new collection
  • PROPFIND() get property data for a resource
  • PROPPATCH() modify property data for a resource
  • LOCK() lock a resource
  • UNLOCK() unlock a locked resource
  • checklock() check whether a resource is locked
  • check_auth() check authentication

Voici un exemple qui permet de décrire une arborescence composée récursivement d'un fichier (MyFile) et d'un répertoire (MyDir). Le ficher contient son nom et son chemin.


require_once "HTTP/WebDAV/Server.php";

class HTTP_WebDAV_Server_Test extends HTTP_WebDAV_Server
{
    var $myfile = '%s%s';

    function check_auth($type, $user, $pass)
    {
        return true;
    }

    function HEAD(&$options)
    {
        $options['mimetype'] = 'text/xml';
        $options['mtime']    = filemtime(__FILE__);
        $options['size']     = strlen(sprintf($this->myfile, basename($options['path']), $options['path']));

        return true;
    }

    function GET(&$options)
    {
        if (!$this->HEAD($options)) {
            return false;
        }
        $options['data'] = sprintf($this->myfile, basename($options['path']), $options['path']);

        return true;
    }

    function PROPFIND(&$options, &$files)
    {
        $i = 0;
        $files["files"] = array();

        // On décrit la ressource demandée

        $path = $options['path'];
        $name = basename($path);

        $files["files"][$i] = array();
        $files["files"][$i]["path"]  = $path;
        $files["files"][$i]["props"] = array();
        $files["files"][$i]["props"][] = $this->mkprop("displayname",      $name);
        $files["files"][$i]["props"][] = $this->mkprop("creationdate",     filectime(__FILE__));
        $files["files"][$i]["props"][] = $this->mkprop("getlastmodified",  filemtime(__FILE__));
        $files["files"][$i]["props"][] = $this->mkprop("lastaccessed",     fileatime(__FILE__));
        $files["files"][$i]["props"][] = $this->mkprop("ishidden",         false);

        // La ressource demandée est-elle un repertoire ou un fichier ?
        $testdir = trim($options['path'], '/');

        if ($testdir === '' or preg_match(',MyDir$,', $testdir)) {
            $files["files"][$i]["props"][] = $this->mkprop("resourcetype",     "collection");
            $files["files"][$i]["props"][] = $this->mkprop("getcontenttype",   "httpd/unix-directory");
        }
        else {
            $files["files"][$i]["props"][] = $this->mkprop("getcontenttype",   'text/xml');
            $files["files"][$i]["props"][] = $this->mkprop("resourcetype",     "");
            $files["files"][$i]["props"][] = $this->mkprop("getcontentlength", strlen(sprintf($this->myfile, $name, $path)));
        }
        ++$i;

        // On décrit le contenu de la ressource
        if ($options['depth'] == 1) {

            $path = rtrim($options['path'], '/').'/MyDir';
            $name = basename($path);

            $files["files"][$i] = array();
            $files["files"][$i]["path"]  = $path;
            $files["files"][$i]["props"] = array();
            $files["files"][$i]["props"][] = $this->mkprop("displayname",      $name);
            $files["files"][$i]["props"][] = $this->mkprop("creationdate",     filectime(__FILE__));
            $files["files"][$i]["props"][] = $this->mkprop("getlastmodified",  filemtime(__FILE__));
            $files["files"][$i]["props"][] = $this->mkprop("lastaccessed",     fileatime(__FILE__));
            $files["files"][$i]["props"][] = $this->mkprop("resourcetype",     "collection");
            $files["files"][$i]["props"][] = $this->mkprop("getcontenttype",   "httpd/unix-directory");
            ++$i;

            $path = rtrim($options['path'], '/').'/MyFile.xml';
            $name = basename($path);

            $files["files"][$i] = array();
            $files["files"][$i]["path"]  = $path;
            $files["files"][$i]["props"] = array();
            $files["files"][$i]["props"][] = $this->mkprop("displayname",      $name);
            $files["files"][$i]["props"][] = $this->mkprop("creationdate",     filectime(__FILE__));
            $files["files"][$i]["props"][] = $this->mkprop("getlastmodified",  filemtime(__FILE__));
            $files["files"][$i]["props"][] = $this->mkprop("lastaccessed",     fileatime(__FILE__));
            $files["files"][$i]["props"][] = $this->mkprop("ishidden",         false);
            $files["files"][$i]["props"][] = $this->mkprop("getcontenttype",   'text/xml');
            $files["files"][$i]["props"][] = $this->mkprop("resourcetype",     "");
            $files["files"][$i]["props"][] = $this->mkprop("getcontentlength", strlen(sprintf($this->myfile, $name, $path)));
            ++$i;
        }
        return true;
    }
}


Pour tester

En ligne de commande

L'utilitaire cadaver permet de tester toutes les parties du protocoles Webdav.


% cadaver 
dav:!> open http://localhost:50009/
dav:/> ls
Listing collection `/': succeeded.
Coll:   MyDir                                  0  juil. 15 15:35
        MyFile.xml                            60  juil. 15 15:35
dav:/> cd MyDir
dav:/MyDir/> ls
Listing collection `/MyDir/': succeeded.
Coll:   MyDir                                  0  juil. 15 15:35
        MyFile.xml                            66  juil. 15 15:35
dav:/MyDir/> get MyFile.xml
Downloading `/MyDir/MyFile.xml' to MyFile.xml:
Progress: [=============================>] 100,0% of 66 bytes succeeded.
dav:/MyDir/> propget MyFile.xml 
Fetching properties for `MyFile.xml':
displayname = MyFile.xml
creationdate = 2009-07-15T13:35:11Z
getlastmodified = Wed, 15 Jul 2009 13:35:11 GMT
lastaccessed = Wed, 15 Jul 2009 13:39:15 GMT
ishidden = 
getcontenttype = text/xml
resourcetype = 
getcontentlength = 66
dav:/MyDir/> more MyFile.xml
Displaying `/MyDir/MyFile.xml':
MyFile.xml/MyDir/MyFile.xml
dav:/MyDir/> exit
Connection to `localhost' closed.
%

Avec nautilus sous Ubuntu