/*
 * https ; gemini ;
 * tout ; log ; twtxt ;
 * à propos (@) ; ;
 */

Code d'intégration du support de gzip à httpd

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 :

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	<v.string>	STRING
 %token  <v.number>	NUMBER
 %type	<v.port>	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

Solène
sbw

Une réaction?

Envoyez votre commentaire par mail.
Mode d'emploi de la liste de diffusion pour recevoir les réponses.