Un serveur WEB en OpenScheme

Modules d'extension externes

Par :

Guilhem de Wailly (gdw@erian-concept.com)



Résumé

Dans cet article, nous continuons la programmation d'un serveur WEB élémentaire en Scheme. Nous utilisons l'environnement Open-Scheme disponible librement sur le site www.open-scheme.com. Certaines fonctionnalités utilisées sont des extensions à la norme du langage qui peuvent être assez facilement portées vers d'autres systèmes Scheme.

Cette étude sera le prétexte pour mieux comprendre par la pratique comment fonctionnent les serveurs WEB.

Le mois dernier, nous avons ajouté au serveur la possibilité traiter plusieurs requêtes simultanément au lieu de les traiter en série, à l'aide du multi-treading.

Ce mois-ci, nous allons modifier le serveur pour qu'il puisse accepter des modules extérieurs dans le traitement des requêtes, afin d'en augmenter sans limite les fonctionnalités et les capacités de traitement. On retourne d'ailleurs cette possibilité dans les serveurs WEB comme Apache.

L'environnement Open-Scheme est amélioré en fonction de ce projet. Aussi, il est nécessaire de se procurer la version la plus récente. Nous faisons notre maximum pour fournir l'environnement en temps et en heure, et librement, sur le serveur WEB d'Open-Scheme (www.open-scheme.com), sur le CDROM de Linux Magazine ou dans l'archive http://www.erian-concept.com/pub/ows.tgz.

Principe

Le serveur WEB que nous avons conçu jusqu'à maintenant est écrit en un seul fichier source d'un peu plus de 1000 lignes de code. Pour étendre ses fonctionnalités, nous sommes obligés de modifier le programme lui-même.

Ce mois-ci, nous allons ajouter la possibilité d'adjoindre dynamiquement des fonctionnalités au serveur WEB par l'ajout de modules de traitement additionnels.

Ce principe est très utilisés dans le serveur WEB Apache pour lequel il existe un nombre important de modules. Certains modules modifient la manière d'authentifier les utilisateurs, d'autres modules permettent la modification dynamique de la page HTML ou du document retourné au client HTTP, certains permettent de placer du code dans les pages WEB, comme les modules PHP ou Perl.

Comme nous allons le voir, la souplesse de Scheme va faciliter énormément l'ajout des modules.

Les modules agissent comme des filtres dans le traitement de la requête HTTP ; aussi, nous les appelons hooks (agrafes en anglais). Nous avons définis six types de modules :

ï‚· translate hook : permet de modifier l'URL accédée ;

ï‚· access hook : permet de modifier l'authentification de l'utilisateur ;

ï‚· pre hook : est invoqué avant la production de la ressource ;

ï‚· post hook : est invoqué après la production de la ressource ;

ï‚· log hook : permet de journaliser l'accès ;

ï‚· error hook : permet de journaliser les erreurs.

L'invocation des filtres peut être schématisée de la manière suivante :

(let ([error (lambda ()
(if (error-hooks)
(production de l'erreur)))])
(if (lecture-de-la-requête)
(if (translate-hook)
(begin
(log-hook)
(if (access-hook)
(begin
(pre-hooks)
(production-du-résultat)
(post-hooks)))))))

La requête est lue, puis traduite ; cette traduction permet de modifier l'URL demandée avant traitement. L'accès est journalisé. Si la ressource demandée est accessible, les filtres de pré-traitement sont activés, le résultat est produit et enfin, les filtres de post-traitement sont activés. Cette manière de procéder permet au concepteur de sites WEB d'intervenir dans toutes les étapes de la production de la ressource. A tout moment, une erreur peut être produite.

Pour ajouter des modules, chaque serveur virtuel définit donc une liste de filtres (hook) dans le fichier d'initialisation ows.ini :

[ows]
address = 127.0.0.1
...
error logs = ows.err

[www.erian-concept.local]
address = 127.0.0.1
...
error logs = /tmp/ows.err
translate hooks =
access hooks =
pre hooks =
post hooks =
log hooks = log-hook
error hooks =

Une même ligne peut contenir plusieurs filtres, séparés par un blanc ; chaque filtre est identifié par une chaîne de caractères qui est en fait le nom du fichier Scheme où chercher le programme du filtre, avec ou sans l'extension .osm.

Le programme du filtre est une lambda expression (fonction anonyme Scheme) recevant comme argument l'objet serveur, l'objet requête et le port où écrire le résultat. Par exemple, le filtre log-hook.osm pourrait être :

(lambda (server query io-port)
(format *current-error-port*
         "log-hook: server '~a', url '~a'\n"
         (<server>:name server)
          (<query>:url query))
#t)

A chaque requête, ce filtre affichera sur le port d'erreur standard le nom du serveur et la ressource demandée, comme avec :

log-hook: server 'www.erian-concept.local', query '/'

Le filtre renvoie vrai ou faux ; s'il renvoie vrai, le traitement continue et le filtre suivant est activé ; s'il renvoie faux, le traitement est interrompu.

Réalisation

Pour prendre en compte les modules de filtres dans le serveur WEB Ows, nous devons apporter un certain nombre de modifications mineures au logiciel. Encore une fois, nous verrons que la souplesse de Scheme permet de réaliser cette fonctionnalité qui peut paraître assez complexe de manière très naturelle.

Déclaration de la classe du serveur

La classe des serveurs se voit ajouter six attributs correspondants à six listes de filtres :

; déclaration de la classe des serveurs
(define-class <server> #f
[name :initform ""]
...
[connexions :initform 0]
; attributs contenant la liste des filtres. Cette
; est initialement vide.
[translate-hooks :initform '()]
[access-hooks
:initform '()]
[pre-hooks
:initform '()]
[post-hooks
:initform '()]
[log-hooks
:initform '()]
[error-hooks
:initform '()])

Ces listes sont initialisées à la liste vide ; elles seront remplies lors de l'instanciation de la classe après la lecture du fichier d'initialisation.

Instanciation des serveurs

Les serveurs sont créés en lisant le fichier d'initialisation ows.ini. Le code des filtres est chargé à partir du fichier par la fonction load qui se charge de convertir les expressions Scheme en code exécutable :

; programme principal.
; création de la table des serveurs virtuels.

(let ([servers (hash:new)])
(for-each
(lambda (server)
(let* ([get-str (lambda (item default)
...
[get-nb (lambda (item default)
...
[get-bln (lambda (item default)
...
[get-lst (lambda (item default)
...
; fonction qui retourne une liste de fonctions
; lues dans les fichiers dont le nom sont la
; valeur de l'item name dans le fichier ows.ini
[get-hooks (lambda (name)
; applique à chaque nom de
; fichier la lambda expression.
(map (lambda (module)
; lit et compile le contenu
; du fichier.
 (load
 ; si le nom du fichier ne se
 ; termine pas par .osm, ajouter
 ; cette extension
 (if (eqv? (os:extname module)
"")
 (
string-append module
  ".osm")
 module)))
; retourne la liste des noms de
; fichier
(get-lst name '())))])
(hash:put!
servers
(string->symbol server)
(build
<server>
:name server
...
:max-connexions (get-nb "max connexions"
1)
; valeur des listes de filtres
:translate-hooks (get-hooks "translate hooks")
:access-hooks (get-hooks "access hooks")
:pre-hooks (get-hooks "pre hooks")
:post-hooks (get-hooks "post hooks")
:log-hooks (get-hooks "log hooks")
:error-hooks (get-hooks "error hooks")))))
(profile:get ows.ini #f #f #f))
(let ([ows (hash:get servers 'ows)])
(if (not ows)
(error "not default [ows] server in ~a"
ows.ini))
(format *current-error-port*
"~a: make server socket\n" (uptime))
...

Les listes de filtres de l'objet <server> sont obtenues en chargeant chaque fichier contenant le filtre à l'aide de la fonction load, et en concaténant chaque résultat qui est une lambda expression. Cette possibilité de chargement dynamique est très intéressante dans le langage Scheme, et elle peut être difficile à réaliser dans d'autres langages.

Maintenant que la classe est modifiée et que les objets sont correctement initialisés, il ne nous reste plus qu'à invoquer les filtres là où c'est nécessaire.

Mise en place des modules

La traduction des adresses est effectuée dès que la requête est lue et que le serveur virtuel est identifié, dans la fonction process-query. De la même manière, la journalisation est aussi effectuée au plus tôt dans cette fonction :

; Traitement des requêtes.
(
define (process-query server query io-port)
  ; Si l'adresse peut être traduite...
(if (call-hooks <server>:translate-hooks
server
query
io-port)
(
begin
        ; On traite la requête.
        ; Si tous les filtres de journalisation
        ; l'autorisent...

(
if (call-hooks <server>:log-hooks
server
query
io-port)
            ; Journalisation de l'accès dans
            ; le journal du serveur.

(with-append-to-file
(<server>:access-log server)
(
lambda ()
(
format #t
"[~a]: ~a ~a\n"
(
format-date (<query>:date query))
(<query>:method query)
(<query>:uri query)))))
  ...

; Affecter la ressource demandée.
(<query>:resource!
         query
(path! (<server>:root-directory server)
(<query>:url query)))

; Vérification de l'accessibilité de la
        ; ressource.
        ; Les filtres peuvent provoquer une erreur
        ; et donc activer la continuation du serveur.
(if (call-hooks <server>:access-hooks
server
query
io-port)

          ; Traitement par défaut.
(check-access server query io-port))

; Activation des filtres de pré-traitement.
        (call-hooks <server>:pre-hooks
            server
            query
            io-port)

; Fabrication de la réponse.
(let ([err (trap (output-resource server
                                         query
                                         io-port))])
   ; Activation des filtres de post-traitement.
   (call-hooks <server>:pre-hooks
                server
                query
                io-port)
          ; Pour la mise au point, afficher un message.
(if err
(format *current-error-port*
                  "*** Error ~a\n"
                  err))))))

L'activation des filtres est simplement intercalée dans la fonction process-request de manière judicieuse. Comme la description le montre, dans certains cas, la valeur de retour des filtres ne conditionne que le traitement par défaut (log-hooks) ; dans certains autres cas, la production de la ressource est totalement conditionnée par cette valeur de retour (translate-hooks).

La fonction d'affichage des erreurs est elle aussi modifiée pour invoquer les filtres de traitement des erreurs :

(define (print-error server
query
io-port
number
title
. args)
  ; si les filtres de traitements des erreurs l'autorise...
(if (call-hooks <server>:error-hooks
server
query
io-port)
(begin
; traitement par défaut.
(print-header query
  io-port
(format #f
"~a ~a"
number
title))
       ...

L'activation des filtres est effectuée par la fonction call-hooks qui sera décrite plus bas dans le texte. Il suffit de donner à cette fonction le sélecteur de l'attribut de la classe <server>, l'objet représentant le serveur et l'objet représentant la requête.

Appel des filtres

Pour invoquer une liste de filtres, nous définissons la fonction suivante :

; Invocation des filtres.
(define (call-hooks hook-attribut server query io-port)
  ; Pour tous les filtres...
(let loop ([hooks (hook-attribut server)])
    ; Si la liste est vide, on retourne vrai
    ; (traitement par défaut).
(if (null? hooks)
#t
        ; Si le filtre retourne vrai...
(if ((car hooks) server query io-port)
            ; on continue,
(loop (cdr hooks))
            ; sinon, on retourne faux
#f))))

Cette fonction invoque un à un les filtres obtenus par le sélecteur hook-attribut. L'itération est interrompue dès que tous les filtres ont été invoqués ou dès que l'un d'eux retourne la valeur faux. Si tous les filtres ont retourné vrai, la fonction retourne vrai, sinon, elle retourne faux ; cela va permettre de provoquer un traitement par défaut.

Améliorations

Dans cette réalisation, nous avons simplement souhaité intercaler le code nécessaire à la prise en compte des modules, sans remettre en cause la totalité du logiciel. Il est clair que le programme pourrait être réarranger de manière à proposer des filtres par défaut, qui pourraient d'ailleurs être placés dans des fichiers source différents. De cette manière, l'aspect modulaire du serveur WEB serait plus visible.

Par ailleurs, nous pourrions proposer des filtres de traitement par rapport au type MIME de la ressource à produire (comme c'est le cas pour Apache). Mais cela aurait là aussi imposé une profonde refonte de la structure du serveur.

Compilation des fichiers sources Scheme

OpenScheme permet de compiler tout fichier source Scheme en fichier compilé pour sa machine virtuelle. Les fichiers compilés de cette manière possèdent un certain nombre d'avantages sur des fichiers sources originaux :

ï‚· possibilité de mixer des sources et des fichiers compilés un même projet ;

ï‚· temps de chargement beaucoup plus rapide ;

ï‚· diminution de la taille du fichier ;

ï‚· compression et cryptage des chaînes de caractères et des symboles pour en assurer la compacité et la confidentialité ;

ï‚· protection des sources par un codage difficilement réversible ;

ï‚· chargement automatique du fichier compilé ayant l'extension .osb à la place du fichier source (.osm) s'il est plus récent, avec la fonction load.

Les modules du serveur WEB Ows ainsi que le serveur lui-même auront intérêt à être compilés de la sorte pour bénéficier de tous ces avantages. Pour compiler le serveur WEB lui-même, nous tapons :

$ osm --bytecode ows.osm

Cette commande produit un fichier ows.osb (pour OpenScheme binary). L'exécution du serveur WEB ne doit cependant pas être modifiée, et nous pouvons toujours lancer le serveur WEB avec la commande :

$ osm --exec ows.osm

En fait, lorsque OpenScheme tente de charger un fichier ayant l'extension .osm, il regarde toujours s'il n'existe pas un fichier de même nom et ayant l'extension .osb, plus récent que lui ; si un tel fichier existe, il est préférentiellement chargé. Le fichier compilé est lu beaucoup plus rapidement car la phase du préprocesseur Scheme (remplacement des macros) et de compilateur vers la machine virtuelle ne sont pas nécessaires. De plus, le fichier est illisible, ce qui en protège le contenu.

Si le fichier ows.osm devait maintenant être modifié, sa date de dernière modification serait plus récente que celle du fichier ows.osb ; OpenScheme choisirait donc le fichier ows.osm lors de son chargement.

De la même manière, les modules peuvent être compilés. Par exemple, le module log-hook.osm pourra être compilé avec la commande suivante :

$ osm --bytecode log-hooks.osm

Cette commande génère un fichier log-hook.osb. La commande load utilisé lors de la création du serveur virtuel préférera ce dernier fichier au fichier source original. Notons qu'il est toute à fait possible d'utiliser des modules compilés avec un serveur non compilé, et inversement.

L'auteur

Guilhem de Wailly, gérant de la société Erian Concept (www.erian-concept.com): support, formations, configurations, administration, développements Linux.

Références

WEB

 PHP
http://www.php.net

 TCP/IP, Architecture, protocoles, applications
Douglas Comer
InterEdition

ï‚· W3C
http://www.w3c.org

ï‚· CGI
http://www.jmarshall.com/easy/cgi/

ï‚· CGI
http://hoohoo.ncsa.uiuc.edu/cgi/

ï‚· CGI
http://www.tsden.org/ryutaroh/fileupload-e.shtml

ï‚· CGI en Français
http://www.scripts-fr.com/

ï‚· CGI+Scheme
http://www.lh.com/~oleg/ftp/Scheme/web.html

ï‚· FastCGI
http://www.fastcgi.com/

ï‚· HTTP en Français
http://webbo.enst-bretagne.fr/ActiveWebFr/eg-uk-tut.book_29.fr.html

ï‚· HTTP 1.0
http://www.ietf.org/rfc/rfc1945.txt

ï‚· HTTP 1.1
http://www.ietf.org/rfc/rfc2616.txt

Scheme

 Structure et Interprétation des programmes informatiques
H. Abelson, GJ. Sussman
InterEdition

 The Scheme Programming Languages - Ansi Scheme
R. Kent Dybvig
Prentice Hall

 Les langages Lisps - Christian Queinnec
InterEdition

 Programmer avec Scheme - J. Chazarin
Thomson Publishing

 Revised4 Report on the Algorithmic Language Scheme
W. Clinger, J. Rees
ftp://ftp.nj.nec.com/pub/kelsey

Environnements Scheme Free

 Bigloo - Manuel Serrano
http://kaolin.unice.fr
Environnement de programmation Scheme.

 DrScheme - Rice University
http://www.cs.rice.edu/CS/PLT/
Environnement Scheme libre très avancé.

 PCScheme - Texas Instrument
ftp://cui.unige.ch/public/pcs/pcscheme.exe
Un très bon environnement de programmation Scheme pour DOS, avec éditeur intégré.

 Scm - A. Jaffer
http://www-swiss.ai.mit.edu/~jaffer
La référence des interprètes Scheme. Très petit, rapide, pour beaucoup de plates-formes, extensible.

 Stk - Erik Gallesio
http://kaolin.unice.fr
Interprète Scheme avec la bibliothèque TK.

D'autres liens sont visibles sur le site www.schemers.org.

Environnements Scheme commerciaux

 ChezScheme - Cadence, Inc
http://www.scheme.com/
Environnement Scheme très performant.

 EdScheme, 3Dscheme - Scheme, Inc
http://www.schemers.com/
Environnement de programmation Scheme pour Windows.

 Inlab Scheme - Inlab Software GmbH
http://www.munich.net/inlab/scheme/
Environnement commercial

 Open-Scheme - Erian Concept
http://www.open-scheme.com
Environnement professionnel de programmation Scheme comprenant un interprète, un compilateur et un débogueur symbolique.
Existe en version libre et commerciale, pour Linux, FreeBSD, Solaris, Windows et BeOS, sur systèmes Intel et Sun.