# Code d'intégration du support de gzip à httpd 2021-10-06T21:07:51Z Comme promis je donne quelques détails sur la façon dont j'ai ajouté la possiblité de servir du contenu compressé avec httpd. Tout d'abord, je tiens à remercier Solène pour ses judicieux conseils et pour m'avoir donné l'impulsion de m'y remettre. En effet, j'avais déjà implémenté un truc similaire en demandant à httpd de servir le contenu au travers d'un cgi. Le principe était très simple : on vérifiait si un fichier avec le même nom et l'extension ".gz" en plus était présent à côté de celui demandé. Si oui, alors c'est celui-ci que l'on envoyait. (voir sbw). Tout ceci était très peu efficace, vous vous en doutez. Pourtant, c'est une fonctionnalité à mon avis indispensable car elle réduit considérablement la quantité d'octets à transférer au prix de quelques calculs supplémentaires pour décompresser. C'est bon pour les bandes passantes (tout le monde n'a pas la fibre) et la compression gzip ne nécessite pas tant de ressources. Par ailleurs, puisque les fichiers sont déjà compressés à l'avance, il s'agit de compression statique. Autrement dit, ce n'est pas au serveur de compresser à la volée, ce qui pourrait représenter quelques problèmes en terme de sécurité. C'est donc parti pour éditer le code source de httpd, le serveur http présent de base dans OpenBSD. Je ne vous cache pas mon très grand espoir de voir ces quelques rajouts intégrés pour de bon. ## Description du code Nos affaires se passent dans le fichier "server_file.c", dans la fonction responsable de l'ouverture/lecture du fichier à envoyer "server_file_request". On commence par vérifier que l'on a activé l'option pour servir du contenu gzippé : ``` if (srv_conf->gzip_static) { ``` Ensuite, nous allons avoir besoin de quelques variables : ``` struct http_descriptor *desc = clt->clt_descreq; struct http_descriptor *resp = clt->clt_descresp; struct stat gzst; struct kv *r, key; char gzpath[PATH_MAX]; ``` "desc" et "resp" permettront de vérifier que le navigateur supporte la compression gzip et d'envoyer l'entête indiquant que le contenu est bien compressé. "r" et "key" serviront à fouiller dans les entêtes. (je m'aperçois que "desc" devrait plutôt s'appeler "req"...). "gzst" et "gzpath" permettront de remplacer les informations sur le fichier demandé et son chemin d'accès si ce dernier est bien disponible chiffré. On vérifie tout d'abord que le navigateur accepte le "gzip" en cherchant l'entête "Accept-Encoding". Si ce dernier existe, on cherche "gzip" puis on continue : ``` /* check Accept-Encoding header */ key.kv_key = "Accept-Encoding"; r = kv_find(&desc->http_headers, &key); if (r != NULL) { if (strstr(r->kv_value, "gzip") != NULL) { ``` Ensuite, on ajoute ".gz" au nom du fichier demandé ``` strlcpy(gzpath, path, sizeof(gzpath)); strlcat(gzpath, ".gz", sizeof(gzpath)); ``` La variable "gzpath" contient maintenant "path" + ".gz". Reste à tester si l'accès à ce fichier gzippé est possible : s'il existe et s'il est lisible : ``` if ((access(gzpath, R_OK) == 0) && (stat(gzpath, &gzst) == 0)) { ``` Si c'est tout bon, alors on remplace le chemin d'accès par la version gzippée et on l'indique dans les entêtes envoyés : ``` path = gzpath; st = &gzst; kv_add(&resp->http_headers, "Content-Encoding", "gzip"); ``` Et voilà :) Le reste, on n'y touche pas, le serveur httpd se charge de servir le fichier. Il aura fallu tout de même déplacer une ligne pour obtenir le type de fichier demandé avant de tester le gzip : ``` media = media_find_config(env, srv_conf, path); ``` Ça paraît en fait tout simple. Il aura fallu réfléchir tout de même un peu, mais très franchement il faut très peu de lignes. J'apprécie énormément la clarté du code d'OpenBSD, finalement très accessible pour un débutant en C comme moi. :) ## Tester le diff Pour ceux que ça intéresse, voici comment tester le diff : * Récupérer le code source comme indiqué ici : http://www.openbsd.org/anoncvs.html * Aller dans /usr/src/usr.sbin/httpd * Récupérer le diff et l'enregistrer dans /tmp/httpd_gzip.diff * Patcher : "patch -p1 < /tmp/httpd_gzip.diff * make && doas make install Ajoutez l'option "gzip_static" dans la configuration d'un domaine servi par httpd. Compressez les fichiers que vous voulez servir gzippés ainsi : ``` $ gzip -9 -c fichier.html > fichier.html.gz ``` Ouvrez un navigateur pour demander "fichier.html" et vérifiez que l'entête "Content-Encoding : gzip" est présent. Vous pouvez aussi remarquer que la taille téléchargée est bien plus petite qu'avant. ## Le diff ``` Index: httpd.conf.5 =================================== RCS file: /cvs/src/usr.sbin/httpd/httpd.conf.5,v retrieving revision 1.118 diff -u -r1.118 httpd.conf.5 --- httpd.conf.5 7 Jun 2021 10:53:59 -0000 1.118 +++ httpd.conf.5 5 Oct 2021 19:27:34 -0000 @@ -381,6 +381,10 @@ features in use .Pq omitted when TLS client verification is not in use . .El +.It Ic gzip_static +Enable static gzip compression. +.Pp +When a file is requested, serves the file with .gz added to its path if it exists. .It Ic hsts Oo Ar option Oc Enable HTTP Strict Transport Security. Valid options are: Index: httpd.h ======================================== RCS file: /cvs/src/usr.sbin/httpd/httpd.h,v retrieving revision 1.157 diff -u -r1.157 httpd.h --- httpd.h 17 May 2021 09:26:52 -0000 1.157 +++ httpd.h 5 Oct 2021 19:27:34 -0000 @@ -85,6 +85,7 @@ #define SERVER_DEF_TLS_LIFETIME (2 * 3600) #define SERVER_MIN_TLS_LIFETIME (60) #define SERVER_MAX_TLS_LIFETIME (24 * 3600) +#define SERVER_DEFAULT_GZIP_STATIC 0 #define MEDIATYPE_NAMEMAX 128 /* file name extension */ #define MEDIATYPE_TYPEMAX 64 /* length of type/subtype */ @@ -542,6 +543,8 @@ struct server_fcgiparams fcgiparams; int fcgistrip; + + int gzip_static; TAILQ_ENTRY(server_config) entry; }; Index: parse.y ======================================== RCS file: /cvs/src/usr.sbin/httpd/parse.y,v retrieving revision 1.125 diff -u -r1.125 parse.y --- parse.y 10 Apr 2021 10:10:07 -0000 1.125 +++ parse.y 5 Oct 2021 19:27:34 -0000 @@ -140,7 +140,7 @@ %token PROTOCOLS REQUESTS ROOT SACK SERVER SOCKET STRIP STYLE SYSLOG TCP TICKET %token TIMEOUT TLS TYPE TYPES HSTS MAXAGE SUBDOMAINS DEFAULT PRELOAD REQUEST %token ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS REWRITE -%token CA CLIENT CRL OPTIONAL PARAM FORWARDED FOUND NOT +%token CA CLIENT CRL OPTIONAL PARAM FORWARDED FOUND NOT GZIPSTATIC %token STRING %token NUMBER %type port @@ -288,6 +288,8 @@ s->srv_conf.hsts_max_age = SERVER_HSTS_DEFAULT_AGE; + s->srv_conf.gzip_static = SERVER_DEFAULT_GZIP_STATIC; + if (last_server_id == INT_MAX) { yyerror("too many servers defined"); free(s); @@ -1140,6 +1142,9 @@ srv_conf->flags &= ~SRVFLAG_BLOCK; srv_conf->flags |= SRVFLAG_NO_BLOCK; } + | GZIPSTATIC { + srv_conf->gzip_static = 1; + } ; block : BLOCK { @@ -1400,6 +1405,7 @@ { "fastcgi", FCGI }, { "forwarded", FORWARDED }, { "found", FOUND }, + { "gzip_static", GZIPSTATIC }, { "hsts", HSTS }, { "include", INCLUDE }, { "index", INDEX }, Index: server_file.c ============================================ RCS file: /cvs/src/usr.sbin/httpd/server_file.c,v retrieving revision 1.70 diff -u -r1.70 server_file.c --- server_file.c 29 Apr 2021 18:23:07 -0000 1.70 +++ server_file.c 5 Oct 2021 19:27:34 -0000 @@ -229,20 +229,49 @@ goto abort; } + media = media_find_config(env, srv_conf, path); + if ((ret = server_file_modified_since(clt->clt_descreq, st)) != -1) { /* send the header without a body */ - media = media_find_config(env, srv_conf, path); if ((ret = server_response_http(clt, ret, media, -1, MINIMUM(time(NULL), st->st_mtim.tv_sec))) == -1) goto fail; goto done; } + /* change path to path.gz if necessary. */ + if (srv_conf->gzip_static) { + struct http_descriptor *desc = clt->clt_descreq; + struct http_descriptor *resp = clt->clt_descresp; + struct stat gzst; + struct kv *r, key; + char gzpath[PATH_MAX]; + + /* check Accept-Encoding header */ + key.kv_key = "Accept-Encoding"; + r = kv_find(&desc->http_headers, &key); + + if (r != NULL) { + if (strstr(r->kv_value, "gzip") != NULL) { + /* append ".gz" to path and check existence */ + strlcpy(gzpath, path, sizeof(gzpath)); + strlcat(gzpath, ".gz", sizeof(gzpath)); + + if ((access(gzpath, R_OK) == 0) && + (stat(gzpath, &gzst) == 0)) { + path = gzpath; + st = &gzst; + kv_add(&resp->http_headers, + "Content-Encoding", "gzip"); + } + } + } + } + /* Now open the file, should be readable or we have another problem */ if ((fd = open(path, O_RDONLY)) == -1) goto abort; - media = media_find_config(env, srv_conf, path); ret = server_response_http(clt, 200, media, st->st_size, MINIMUM(time(NULL), st->st_mtim.tv_sec)); switch (ret) { ``` ## Liens => https://dataswamp.org/~solene/ Solène => https://si3t.ch/Logiciel-libre/Code/archive.tgz sbw dans l'archive de code ## Une réaction? => mailto:bla@bla.si3t.ch?subject=httpd_gzip_code Envoyez votre commentaire par mail. => /log/commentaires Mode d'emploi de la liste de diffusion pour recevoir les réponses.