Un serveur WEB en OpenScheme
Scripts CGI côté serveurs
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'entres 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. 
Ce mois-ci, nous allons mettre en place un environnement d'exécution de manière à pourvoir inclure du code Scheme dans les pages HTML, code Scheme qui sera exécuté par le serveur. L'avantage immédiate est l'augmentation sensible de la vitesse d'exécution par rapport aux scripts CGI traditionnels.
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.
Si je ne me trompe pas, cet article est prévu pour juillet-août, donc bonne vacances à tous !
Description
Les scripts CGI ont comme intérêt d'être complètement indépendants de tous langages de programmation : ils sont considérés par le serveur comme des programmes exécutables répondant à un certain protocole de communication. Leur inconvénient majeur est de consommer beaucoup de ressources système pour s'exécuter, surtout si se sont des programmes interprétés. Dans ce cas, l'interprète du langage doit être chargé en mémoire et initialisé à chaque invocation du script. 
C'est pour cette raison que sont apparus des environnements d'exécution liés aux serveurs WEB qui économisent le temps de chargement et d'initialisation de l'interprète. On connaît les ASP sous Windows, mais surtout l'environnement PHP (www.php.net) sous Unix. Ces environnements permettent d'intégrer du code directement dans les pages WEB. Le serveur détecte la présence de ce code et le communique à l'interprète qui est en permanence en attente.
Ce mois-ci, nous proposons de modifier le serveur WEB en Open-Scheme de manière à ce qu'il soit capable d'évaluer des portions de code Scheme incluses dans les pages WEB. Pour ce faire, nous utiliserons les fonctions existantes dans Open-Scheme et qui permettent aux scripts CGI classiques d'être des pages WEB executables incluant des portions de code Scheme.
Là encore, nous constaterons avec quelle facilité le langage Scheme se comporte dans des situations complexes.
Réalisation
Pour détecter la présence de code Scheme inclus, nous proposons de nous baser sur l'extension du fichier en choisissant, par exemple .esm pour tout fichier HTML contenant du code Scheme. Nous appellerons dans la suite ce type de page ``des pages ESM'' pour simplifier.
Nous allons modifier le serveur WEB pour qu'il prenne en compte ce nouveau type de fichiers.
Type MIME
Nous ajoutons l'extension .esm dans la liste des types mime reconnus :
(define *mimes*
 '(("htm"  "text/html" "Page&nbsp;WEB")
   ("html" "text/html" "Page&nbsp;WEB")
   ("txt"  "text/plain" "Texte")
   ("css"  "text/css"   "Style&nbsp;HTML")
   ("gif"  "image/gif"  "Image&nbsp;GIF")
   ...
   ("osm"  "text/html" "OpenScheme&nbsp;source")
    ("esm"  "text/html" "OpenScheme&nbsp;embedded")
    ("scm"  "text/html" "Scheme&nbsp;source")
   ...
   ))
Classes
Il est ensuite nécessaire de modifier les classes <server> et <query> : la première reçoit un nouveau champ embedded-suffix contenant la liste des extensions des fichiers ESM et la seconde, un champ permettant d'indiquer la présence d'une ressource ESM et qui sera utilisé lors du traitement des requêtes :
(define-class <server> #f
 [name            :initform ""]
 [address         :initform ""]
 [port            :initform 0]
 [root-directory  :initform ""]
 [script-alias    :initform '()]
 [script-suffix   :initform '()]
  [embedded-suffix :initform '()]
  [default-indexes :initform '()]
 [browse          :initform #f]
 [access-log      :initform #f]
 [error-log       :initform #f]
 [error           :initform #f])
  et :
(define-class <query> #f
 [date            :initform 0.0]
 [method          :initform ""]
 [uri             :initform ""]
 [protocol        :initform ""]
 [headers         :initform '()]
 [url             :initform ""]
 [cgi             :initform #f]
  [embedded        :initform #f]
  [string          :initform ""]
 [resource        :initform ""])
Création du serveur
Le serveur doit être créé avec un nouveau champ contenant la liste des suffixes des pages ESM : 
(let ([server 
       (build
        <server>
       :name            "http://www.dummy.com"
       :address         "1.1.1.1"
       :port            8080
       :root-directory  "/home/html"
       :script-alias    '("/cgi-bin" ".")
       :script-suffix   '(".cgi"
                           ".bat" 
                           ".sh"
                           ".osm" 
                           ".cgi")
        :embedded-suffix '(".esm")
        :default-indexes '("index.html"
                           "index.htm" 
                           "index.cgi")
       :browse          #t
       :access-log      "ows.log"
       :error-log       "ows.err")])
  ...
Sans cela, les pages ESM ne seront pas reconnues. On pourra ajouter ici autant d'extension que nécessaire.
Traitement de la requête
Le traitement de la requête doit détecter si la ressource demandée est une page ESM. Dans ce cas, la fonction positionne l'indicateur embedded de l'objet query :
(define (process-query server query)
 (if (not (os:directory? 
            (<server>:root-directory server)))
    ...

  ; Vérifier s'il s'agit d'un script CGI
  (let ([url   (<query>:url query)]
       [alias (<server>:script-alias server)])
    ....
 
  ; Vérifier s'il s'agit d'une page ESM
  (if (not (<query>:cgi query))
     (let ([url (<query>:url query)])
       (let loop ([suffix (<server>:embedded-suffix 
                             server)])
         (if (not (null? suffix))
             (if (string-tail? url (car suffix))
                 (<query>:embedded! query #t)
                 (loop (cdr suffix)))))))
      
  ; Vérifier si le répertoire se termine par /
  (let ([url (<query>:url query)])
    ...
La fonction compare l'extension du fichier aux extensions des ESM afin de déterminer si la ressource est une page ESM. Si c'est le cas, l'indicateur embedded de l'objet requête est positionné.
Production des sorties
La fonction de sortie a subi quelques modifications de manière à tenir compte des ESM :
(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     (if path? #f (assoc ext *mimes*))]
        [mime     (if mime (cadr mime) #f)]
        [mime     (if mime mime "text/plain")])
   (format *current-error-port* "mime = ~s\n" mime)
   (if (and (or (string=? protocol "HTTP/1.0")
                (string=? protocol "HTTP/1.1"))
            (or (string=? mime "text/plain")
                (string=? mime "text/html")))
       (begin
         (print-header query "200 OK")
         (format #t 
                 "Last-Modified: ~a\r\n" 
                 (format-date (os:modified resource)))
         (format #t "Content-Type: ~a\r\n" mime)))

   (cond [(string-ci=? (<query>:method query) "head")
           ; Demande de l'entête seulement.
          (format *current-error-port* 
                  "header requested\n")
           (format #t 
                  "Content-Length: ~a\r\n\r\n"
                  (os:size resource))
           'nothing]

         [path?
           ; Affichage des répertoires
          (format *current-error-port* 
                  "tree requested\n")
           (format #t 
                  "Content-Length: ~a\r\n\r\n"
                  (os:size resource))
           (output-tree server query)]

         [(<query>:cgi query)
           ; Prise en compte des CGI
          (format *current-error-port* 
                  "cgi requested\n")
           (format #t 
                  "Content-Length: ~a\r\n\r\n"
                  (os:size resource))
           (output-cgi server query)]

          [(<query>:embedded query)
          ; Prise en compte des ESM
          (format *current-error-port*
                   "html requested\n")
          (output-html server query)]

          [else
           ; Addichage d'une ressource simple.
          (format *current-error-port* 
                  "file requested\n")
           (format #t 
                  "Content-Length: ~a\r\n\r\n"
                  (os:size resource))
           (output-file server query)])))
La prise en compte des pages ESM est assurée en détectant le champ embedded de l'objet requête. L'entête HTML Content-length est affichée directement lorsque la taille de la ressource est connue à l'avance ou bien elle est affichée au moment où la ressource est produite (cas des ESM).
Production des ESM
La fonction output-html est le coeur du système de production des ESM. Cette fonction est chargée de préparer un environnement pour l'évaluation des portions de code Scheme dans les pages HTML dans lequel elle définit ou redéfinit des fonctions et des variables. Puis elle évalue le code inclus dans la page et affiche le code HTML produit :
(define (output-html server query)
(let* (; Environnement d'execution.
       [env (osm:env:new)]
       ; Retourne une valeur d'entête.
       [get (lambda (name)
             (let ([c (assoc name 
                      (<query>:headers query))])
              (if c (cdr c) #f)))]
       ; Valeur du nouveau cookie, ou #f
       [new-cookie #f]
La fonction commence par créer un environnement d'évaluation initialement vide, une fonction locale get qui retourne la valeur d'un entête connaissant son nom et la variable contenant la nouvelle valeur du cookie (voir plus bas).
Nous définissons ensuite une liste de variables utiles pour l'exécution du script. Ces variables seront définies dans l'environnement d'évaluation du script et correspondent aux variables d'environnement des scripts CGI. La valeur de ces variables est obtenue soit à partir des entêtes HTML, soit à partir de la requête :
        ; Variables Scheme liées
       [vars       
        `((http-cookie     .
            ,(get "HTTP-COOKIE"             ))
          (auth-type       .
            ,(get "AUTH-TYPE"               ))
          (content-length  .
            ,(get "CONTENT-LENGTH"          ))
          (content-type    .
            ,(get "CONTENT-TYPE"            ))
          (path-info       .
            ,(get "PATH-INFO"               ))
          (path-translated .
            ,(get "PATH-TRANSLATED"         ))
          (query-string    .
            ,(<query>:string query          ))
          (remote-addr     .
            ,(get "REMOTE-ADDR"             ))
          (remote-host     .
            ,(get "REMOTE-HOST"             ))
          (remote-ident    .
            ,(get "REMOTE-IDENT"            ))
          (remote-user     .
            ,(get "REMOTE-USER"             ))
          (script-name     .
            ,(<query>:resource query        ))
          (server-name     .
            ,(<server>:name server          ))
          (server-address  .
            ,(<server>:address server       ))
          (server-port     .
            ,(<server>:port server          ))
          (server-protocol .
            ,(<query>:protocol query        ))
          (server-software .
            ,*server*                        )
          (request-uri     .
            ,(<query>:uri query             ))
          (request-method  .
            ,(<query>:method query          ))
          (document-root   .
            ,(<server>:root-directory server))
          )]
La fonction recherche ensuite la liste des champs de formulaire pour les définir comme des variables Scheme dans l'environnement d'évaluation des scripts. Cette recherche est effectuée à l'aide de la très puissante fonction net:cgi:parse-input du module NET d'OpenScheme qui permet d'extraire la valeur des champs de formulaires avec les méthodes GET et POST ainsi qu'avec des requêtes multi-parties (multi-part encoding) :
         ; Liste des couples nom-valeur des champs des
        ; formulaires.
        [couples (net:cgi:parse-input
                  (get "CONTENT-TYPE")
                 (get "CONTENT-LENGTH")
                 (<query>:method query)
                 (<query>:uri    query))])
La fonction initialise ensuite l'environnement, ce qui à pour effet de dupliquer toutes les définitions de l'environnement standard d'OpenScheme. L'utilisation d'un environnement particulier a pour effet de protéger l'environnement du serveur WEB contre les erreurs qui pourraient être produites par les pages ESM :
   ; Initialise l'environnement
  (osm:env:init env)
Nous redéfinissons ensuite certaines fonctions standards d'OpenScheme, comme par exemple eval, qui doit effectuer ses évaluations dans l'environnement du script et non dans l'environnement d'OpenScheme afin de préserver son intégrité :
   ; Redéfinition des fonctions standards.
   (osm:env:put! env 'load (lambda (file)
                            (load file env)))
  (osm:env:put! env 'eval (lambda (exp)
                            (eval exp env)))

   ; Definit la fonction defined? pour les champs.
   ; de formulaires.
   (osm:env:put! env
                'defined? 
                (lambda (symbol)
                  (if (assq symbol couples) #t #f)))
Nous définissons ensuite la fonction set-cookie qui permettra aux scripts de définir un cookie qui sera renvoyé au navigateur client :
   ; Définit la fonction set-cookie qui permet
  ; de créer un cookie.
   (osm:env:put! 
   env
   'set-cookie
   (lambda (name value path expire domain secure)
     (set! new-cookie
           (format #f
                   (string-append
                    "Set-Cookie: ~a; "
                    "path=~a; "
                    "expires=~a; "
                    "domain=~a; "
                    "secure=~a\r\n")
                   name
                   value
                   path
                   expire
                   domain
                   secure))))
La difficulté avec les cookies et qu'ils doivent être déclarés dans les entêtes du résultat, avant toutes autres sorties. Cette fonction permet de modifier la variables locale new-cookie lors de l'évaluation du code inclus. Comme le résultat n'est pas directement renvoyé au client mais accumulé dans un tampon, il sera possible de positionner correctement l'entête avant la production effective de la sortie.
Enfin, les variables utilitaires ainsi que les champs de formulaires sont définis dans l'environnement :
   ; Définit toutes les variables.
   (for-each (lambda (var)
              (osm:env:put! env 
                             (car var)
                             (cdr var)))
            vars)
   ; Définit tous les champs de formulaires.
   (for-each (lambda (couple)
              (osm:env:put! env
                             (car couple)
                             (cdr couple)))
            couples)
L'exécution du script peut provoquer des erreurs ou invoquer la fonction exit. Il est nécessaire de modifier le traitement standard des erreurs afin qu'il ne perturbe pas le fonctionnement du serveur ainsi que la fonction exit afin de ne pas terminer le serveur :
   ; Exécute le script...
   (let* (; Ancien filtre des erreurs
          [error:hooks *error:hooks*]
         ; Exécute le script
         [exec 
          (lambda ()
            (call/cc
             (lambda (exit)
                ; Définit exit comme cette continuation
               ; pour que l'appel à exit depuis le script
               ; ne termine pas le serveur WEB.
                (osm:env:put! env 'exit exit)
                ; Redéfinit le filtre des erreurs
                (set! *error:hooks*
                     (list
                      (lambda (fmt . args)
                        (display "<br><font color=red>")
                        (display "<b>Error: ")
                        (format
                         #t 
                         "~a</b></font>"
                         (vformat #f fmt args))
                        (exit))))
                ; Evalue le sript
                (with-input-from-file
                   (<query>:resource query)
                 (lambda ()
                   (net:cgi:parse-html env))))))]
          [html? #t]
           ; Evaluation du script et récupération de
          ; la sortie standard dans une chaîne de 
          ; caractères.
           [output (with-output-to-string exec)])
Lorsque le script a été exécuté, on restaure le filtre des erreurs original :
     ; Restauration des anciens filtres d'erreur.
     (set! *error:hooks* error:hooks)
Si un nouveau cookie a été défini, on affiche l'entête y correspondant :
     ; Si un cookie a été définie, le déclarer.
     (if (and (or (string=? (<query>:protocol query)
                           "HTTP/1.0")
                 (string=? (<query>:protocol query)
                           "HTTP/1.1"))
             new-cookie)
        (format #t
                "Set-Cookie: ~a\r\n"
                new-cookie))
On calcule aussi la taille de la sortie afin de la retourner au client sous la forme d'un entête HTML :
     ; Déclarer la taille de la sortie
     (format #t
            "Content-Length: ~a\r\n\r\n"
            (string-length output))
Le résultat est alors produit, après les entêtes :
     ; Retourner au navigateur la sortie du
    ; script.
     (display output)
Enfin, on affiche des informations sur le serveur :
     ; Afficher sous la forme d'un commentaire
    ; HTML des informations sur OpenScheme.
     (format #t
            "\n\n<! ~a - ~a  [~a]\n\n~a>\n"
            *name* *version* *when* *copyright*))))
Il est maintenant temps de tester notre nouveau serveur !
Exemple
Ecrivons un petit formulaire HTML contenant du code Scheme dans le fichier /home/html/test.esm :
<html>
 <body>
  <osm (set! *osm:inter:error-on-unbounded*
             #f)
 osm>
  <br>
  <font color=red><b>
   Aujourd'hui, 
   <osm (display (date->string (date))) osm>
 </b></font>
  <osm (format #t "<br>nom=~a\n" nom) osm>
 <br>
  Entrez votre nom et votre pr&eacute;nom:
  <form method=post action=/test.esm>
   <br>nom          <input type=text name=nom   >
   <br>pr&eacute;nom<input type=text name=prenom>
   <br><input type=submit>
  </form>
 </body>
</html>
Ce formulaire HTML contient du code Scheme entre les balises <osm et osm>. Ce code sera exécuté par le serveur. Dans le formulaire, nous affectons à la variable *osm:inter:error-on-unbounded* la valeur faux ; ainsi, Open-Scheme ne provoquera pas d'erreur lors de l'évaluation des variables non définies. 
Ensuite, nous affichons la date du jour après l'avoir convertie en chaîne de caractères. Puis nous affichons la valeur de la variable nom, correspondant à la valeur du champ appelé nom dans le formulaire. Lors du premier affichage, ce champ n'est pas défini, et cette variable non plus. C'est pour cette raison que nous avons positionné *osm:inter:error-on-unbounded* à faux, de manière à ne pas provoquer d'erreur.
Lançons maintenant le serveur WEB dans un terminal :
T1:gdw@erian> ./ows.osm
95250.230000: make server socket
95250.240000: accept connection (1)
Depuis un autre terminal, invoquons lynx :
T2:gdw@erian> lynx http://localhost:8080/test.esm
Sur le terminal du serveur, nous obtenons les traces suivantes : 
T1:gdw@erian> ./ows.osm
95250.230000: make server socket
95250.240000: accept connection (1)
95489.570000: read the request
95489.620000: going to read
95490.100000: end to read
95490.330000: line = "GET /test.esm HTTP/1.0"
95490.490000: method = "GET", uri = "/test.esm", protocol = "HTTP/1.0"
95491.240000: header = "Host: localhost:8080"
95491.360000: header = "User-Agent: Lynx/2.8.3rel.1 libwww-FM/2.14"
95491.360000: header = ""
mime = "text/html"
html requested
95492.280000: shutdown the socket

95492.320000: accept connection (2)
Et lynx affiche :

Figure 1 : Ecran de lynx.
lors de la première invocation, la variable nom, , n'est pas définie. En ayant pris soin de positionner la variable *osm:inter:error-on-unbounded* a faux, Open-Scheme ne provoque pas d'erreur lors de son évaluation, et retourne simplement #unbounded. Lors de la deuxième invocation, cette variable sera définie à la valeur saisie dans le formulaire, et sa valeur sera affichée sous la forme d'une chaîne de caractères.
Disponibilité
Le code source ainsi que la dernière version d'OpenScheme (pré-1.4.0) sont disponibles à l'adresse :
http://www.erian-concept.com/pub/ows.tgz.
Nous nous excusons auprès des lecteurs ayant tenté de télécharger les précédentes versions car il y avait une erreur dans l'archive.
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.