Un serveur WEB en OpenScheme
Les serveurs WEB virtuels
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é d'exécuter les scripts CGI inclus dans les pages WEB. 
Ce mois-ci, nous allons modifier le serveur pour qu'il lise sa configuration dans un fichier d'initialisation et qu'il soit capable de gérer plusieurs serveurs différenciés par leur URL.
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.
Correction
Tout d'abord, grand merci à Bertrand Goareguer qui a découvert un problème lors de l'affichage du contenu des répertoires : la page contenant les entrées du répertoire sélectionné s'affichait sous la forme de texte HTML brut. Cela est dû a une mauvaise détection du type MIME lorsque que le contenu du répertoire est affiché, text/plain au lieu de text/html. 
Les corrections ci-dessous corrigent ce problème :
Ajout d'un nouveau type MIME pour les répertoires :
(define *mimes*  '(("/"    "text/html" "Tree")
                  ("htm"  "text/html" "Page&nbsp;WEB")
                  ("html" "text/html" "Page&nbsp;WEB")
                 ...
Détection de ce nouveau type :
(define (output-resource server query)
 (let* ([resource (<query>:resource query)]
        [protocol (<query>:protocol query)]
        [path?    (path? resource)]
        [ext      (if path? #f (os:extname resource))]
         [mime     (assoc (if path? "/" ext) *mimes*)]
         [mime     (if mime (cadr mime) #f)]
        [mime     (if mime mime "text/plain")])
   (format *current-error-port* "mime = ~s\n" mime)
    ...
Réalisation
Tout le monde connaît les hébergeurs de sites WEB qui permettent de créer des sites personnels. Par exemple, Free.fr permet de créer le site WEB nom.free.fr.
Il est évident qu'il ne peut pas y avoir une machine physique par nom de serveur ! On utilise alors une nouveauté du protocole HTTP version 1.0 qui place dans les entêtes de la requête le nom du serveur demandé. Ce nom est utilisé par le serveur WEB, qui peut être unique, pour déterminer l'emplacement des pages WEB correspondantes.
Ce mois-ci, nous allons ajouter la possibilité à notre mini-serveur de gérer plusieurs serveurs dits virtuels.
Pour cela, il est nécessaire de créer un fichier d'initialisation contenant la description des différents contenus.
Fichier d'initialisation
Voilà à quoi ressemble le nouveau fichier d'initialisation du serveur OWS :
[ows]
address         = 127.0.0.1
port            = 8080
root directory  = /home/gdw/erian/doc/html
script alias    = /cgi-bin
script suffix   = .cgi .bat .sh .osm 
embedded suffix = .esm
default index   = index.html index.htm index.cgi
browse          = #t
access logs     = ows.log
error logs      = ows.err

[hote.virtuel.org]
address         = 127.0.0.1
root directory  = /home/gdw/erian/doc/html
script alias    = /cgi-bin
script suffix   = .cgi .bat .sh .osm 
embedded suffix = .esm
default index   = index.html index.htm index.cgi
browse          = #t
access logs     = /tmp/ows.log
error logs      = /tmp/ows.err
Il repose sur la syntaxe des fichiers d'initialisation (.ini) de Windows qui est à la fois simple et aisément modifiable avec tout éditeur de texte. Nous discuterons de ces fichiers plus bas.
Nous trouvons dans cet exemple de fichier de configuration deux serveurs virtuels, ows et hote.virtuel.com. Par convention, le serveur ows est la description du serveur principal, c'est à dire celui qui est considéré lorsque aucun autre serveur ne correspond à l'URL demandée. Le serveur principal possède des options qu'on ne trouve que chez lui, comme le numéro de port.
Les autres champs ont été décrits des les articles précédents.
Un fichier profile est constitué de sections dont les noms sont inscrits entre accolades. Chaque section est constituée de champs associant un nom et une valeur, séparés par le caractères '='. Les champs sont séparés les uns des autres par des sauts de lignes.
Des commentaires peuvent être introduits avec le caractère ';' ou le caractère '#' ; les commentaires se terminent avec la fin de la ligne, comme dans :
; commentaire
# ou autre commentaire
champ = valeur
Le format utilisé dans OpenScheme permet, à la différence de celui utilisé dans Windows, de placer la valeur d'un champ sur plusieurs lignes si le dernier caractère des lignes intermédiaires est '\', comme dans :
long champ = première ligne  \
             deuxième lignes \
             ...
             dernière ligne
De plus, les noms de champs peuvent contenir des espaces.
Les valeurs peuvent être soit des chaînes de caractères libres, soit des objets primitifs Scheme, en fonction des procédures utilisées pour y accéder, comme il est décrit ci-dessous.
Fonctions d'accès OpenScheme
Le modules OP d'OpenScheme permet de lire et écrire dans les fichiers d'initialisation. Ces fichiers sont chargés en mémoire pour en accélérer la lecture.
Lecture
(@profile:get profile section item)
(profile:get profile section item default)
·	profile	: nom du fichier à lire
·	section	: nom de la section
·	item	: nom du champ
·	default	: valeur par défaut
La fonction @profile:get lit la valeur du champ item de la section dans le fichier profile. Si le champ n'est pas défini, elle retourne faux ; sinon, elle retourne la valeur du champ sous la forme d'une chaîne de caractères. Si item est faux, elle retourne la liste de tous les noms de champs de la section. Si section est faux, elle retourne la liste des nom de section du fichier profile.
La fonction profile:get procède de la même manière à la différence que la valeur est lus comme un objet Scheme et que si le champ n'est pas défini, la valeur par défaut default est retournée.
Considérons le fichier profile test.ini suivant :
[section 1]
champ 11 = valeur 1
champ 12 = '(1 2 3)

[section 2]
champ 21 = a
champ 22 = #\a
Nous avons :
Osm> (@profile:get "test.ini" "section 1" "champ 11")
  => "valeur 1"
Dans ce cas, la chaîne de caractères correspondant à la valeur est retournée.
Osm> (@profile:get "test.ini" "section 1" "n'existe pas")
  => #f
Le champ n'existant pas, #f est retourné.
Osm> (profile:get "test.ini" "section 1" "champ 11" 123)
  => valeur
Ici, la valeur est considérée comme un objet Scheme : le symbole valeur est alors retourné.
Osm> (profile:get "test.ini" "section 1" "n'existe pas" 123)
  => 123
Le champ n'existant pas, la valeur par défaut est retournée.
Osm> (@profile:get "test.ini" "section 1" #f)
  => "champ 12\nchamp 11\n"
La liste des noms de champ est retournée sous la forme de chaînes de caractères séparées par un retour chariot.
Osm> (profile:get "test.ini" "section 1" #f 123)
  => ("champ 12" "champ 11")
La liste Scheme des noms de champ est retournée.
Le champ n'existant pas, la valeur par défaut est retournée.
Osm> (@profile:get "test.ini" #f #f)
  => "section 2\nsection 1\n"
La liste des noms de champ est retournée sous la forme de chaînes de caractères séparées par un retour chariot.
Osm> (profile:get "test.ini" #f #f 123)
  => ("section 2" "section 1")
La liste Scheme des noms de champ est retournée.
Ecriture
Les fonctions suivantes permettent de modifier un fichier profile :
(@profile:set profile section item value)
(profile:get profile section item value)
·	profile	: nom du fichier à lire
·	section	: nom de la section
·	item	: nom du champ
·	value	: valeur à écrire
La fonction @profile:set modifie la valeur du champ item de la section dans le fichier profile. Si le fichier n'existe pas, il est créé ; si la section n'existe pas, elle est créée ; si le champ n'existe pas, il est créé et si le champ existe, son ancienne valeur est remplacée. La valeur value est une chaîne de caractère.
La fonction profile:set procède de la même manière à la différence que la valeur value peut être n'importe quel objet Scheme.
Création des serveurs virtuels et itération principale
Le programme principal du serveur est modifié de manière à lire les configurations des serveurs virtuels. Le fichier d'initialisation est par défaut ows.ini et il doit obligatoirement exister :
; nom du fichier profile d'initialisation
(define ows.ini "ows.ini")

; le fichier doit obligatoirement lire
(if (not (os:readable? ows.ini))
   (error "file ã not found" ows.ini))
La configuration des serveurs virtuels est lue à partir du profile ; chaque serveur est placé dans une table de hachage qui permettra de retrouver très rapidement le serveur correspondant à une URL.
; programme principal
(let (; création de la table de hachage qui contiendra
      ; les serveurs
     [servers (hash:new)])
 ; pour chaque nom de serveur virtuel...
(for-each
  (lambda (server) ; server est un nom de section 
                  ; du profile
  (let* (; permet de lire une chaîne de caractères
         [get-str (lambda (item default)
                   (let ([v (@profile:get ows.ini
                                         server
                                         item)])
                    (if v v default)))]
         ; permet de lire un entier
         [get-nb  (lambda (item default)
                   (let ([v (get-str item #f)])
                    (if v 
                     (string->number v)
                    default)))]
         ; permet de lire un booléen
         [get-bln (lambda (item default)
                   (let ([v (get-str item #f)])
                    (if v
                     (if (member (str:lower-case! v)
                                '("#t" "y" "yes"))
                     #t
                     #f)
                    default)))]
         ; permet de lire une liste de chaînes de 
          ; caractères
         [get-lst (lambda (item default)
                   (let ([v (get-str item #f)])
                    (if v 
                     (str:split v)
                    default)))])
    ; place dans la table de hachage le nom du serveur
    ; virtuel converti en symbole Scheme associé à
    ; l'objet <server> construit
    (hash:put!
    servers                  ; table de hachage
     (string->symbol server)  ; nom du serveur
     (build                   ; objet <server>
      <server>
     :name            server
     :address         (get-str "address"         
                                "127.0.0.1")
     :port            (get-nb  "port"            
                                8080)
     :root-directory  (get-str "root directory"  
                                ".")
     :script-alias    (get-str "script alias"    
                                "/cgi-bin")
     :script-suffix   (get-lst "script suffix"   
                                '(".cgi" 
                                 ".bat"
                                 ".sh"
                                 ".osm"))
     :embedded-suffix (get-lst "embedded suffix" 
                                ".esm")
     :default-indexes (get-lst "default index"   
                                '("index.html" 
                                 "index.htm"
                                 "index.cgi"))
     :browse          (get-bln "browse"          
                                #t)
     :access-log      (get-str "access logs"     
                                "ows.log")
     :error-log       (get-str "error logs"      
                                "ows.err")))))
  ; liste des noms de serveurs du fichier profile
 (profile:get ows.ini #f #f #f))
A ce stade, la table de hachage est initialisée avec les différents serveurs construits à partir des informations lues dans le fichier profile. Nous devons vérifier la présence du serveur par défaut.
 (let (; obtenir de la table un serveur nommé ows
       [ows (hash:get servers 'ows)])
  ; s'il n'existe pas, c'est une erreur
 (if (not ows)
  (error "not default [ows] server in ~a"
          ows.ini))
Maintenant, nous allons créer le port d'écoute principal du serveur.
  ; message d'aide à la mise au point
 (format *current-error-port* 
         "~a: make server socket\n" (uptime))
  ; création du port principal
 (let ([socket (net:socket:make-server
                (<server>:port ows))]
       [accept #f])
   ; si le port ne peut pas être créé, erreur!
  (if (not socket)
   (begin
    (format *current-error-port* 
            "~a: Unable to create a server socket\n"
            (uptime))
    (@exit 1)))
Nous allons aussi créer la continuation de récupération sur erreur et nous l'associons à tous les serveurs virtuels.
   (if (call/cc 
       (lambda (return)
         ; affectation de la continuation et du
         ; port à tous les serveurs virtuels
         (hash:for-each
          servers
         (lambda (name server)
          (<server>:port!  server
                           (<server>:port ows))
          (<server>:error! server
                           return)))
        (return #f)))
    ; en cas d'erreur, fermer le port principal
    (close-input-output-port accept))
La phase d'initialisation des serveurs est maintenant terminée. Nous entrons dans la phase de traitement des requêtes basée sur une itération.
   ; itération principale
  (let loop ([n 1])   ; n est le compteur de requêtes
    ; message d'aide à la mise au point
   (format *current-error-port* 
           "~a: accept connection (~a)\n"
           (uptime) n)
    ; acceptation d'une connexion ; le socket retourné
    ; est converti en port Scheme
   (set! accept 
          (net:socket->port 
           (net:socket:accept socket)))
    ; ce port Scheme devient le port
    ; d'entrée/sortie standard
    (set! *current-input-port*  accept)
   (set! *current-output-port* accept)

    ; message d'aide à la mise au point
    (format *current-error-port* 
           "~a: read the request\n"
           (uptime))
    ; lecture de la requête...
    (let ([query (read-request ows)])
    (if query 
      ; recherche du serveur capable de traiter
      ; cette requête parmi les serveurs virtuels
      (let ([server (witch-server ows servers query)])
      (if server
        ; traitement de la requête
        (process-query server query)))))

    ; message d'aide à la mise au point
    (format *current-error-port* 
           "~a: shutdown the socket\n\n"
           (uptime))
    ; fermeture du port 
   (close-input-output-port accept)

     ; itération sur la prochaine requête
   (loop (+ n 1))))))

Recherche du serveur
La fonction de recherche du serveur a comme arguments le serveur principal, la table de hachage des serveurs virtuels et la requête à traiter. La recherche est effectuée sur le nom du serveur situé dans les entêtes de la requête au nom HOST. :  si un serveur possède le même nom que la valeur HOST dans les entête, il est choisi.  Si aucun serveur n'est trouvé, le serveur par défaut est retourné.
; retourne le serveur capable de traiter la requête
(define (witch-server ows servers query)
 (let ([host (assoc "HOST"
                     (<query>:headers query))])
   (if (not host)
        ; s'il n'y a pas l'entête HOST
        ; ows est retourné
       ows
        ; sinon, on recherche le serveur
        ; dans la table de hachage
       (let ([server (hash:get servers
                               (string->symbol 
                                (str:unblank 
                                 (cdr host))))])
         (if server server ows)))))
Le serveur OWS est maintenant configuré à l'aide d'un fichier. Il est de plus capable de traiter les requêtes destinées à des serveurs différents sur la même machine physique en utilisant le concept des serveurs virtuels.
L'auteur

Guilhem de Wailly, gérant de la société Erian Concept (www.erian-concept.com): support, formations, configurations, administration, développements Linux. Environnement Open-Scheme et vice-président de la R&D de Tornado Technology (www.aecviz.com).
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.