---
date: 2020-05-28T20:00:11+02:00
description: "Délivrer du contenu compressé (au format deflate, gzip, brotli) de fichiers statiques (html, xml, css, js, json, …) par le biais du couple des serveurs natifs httpd+slowcgi sous OpenBSD"
draft: false
include_toc: true
show_comments: false
tags: ['httpd','slowcgi','OpenBSD','CGI','deflate','gzip','brotli']
title: "httpd : délivrer des fichiers statiques compressés (+ slowcgi)"
translationKey: "httpd-compressed-static-files"
---
## Description
**OpenBSD** intègre par défaut dans le système de base :
* un serveur web, nommé **httpd**, depuis 5.7 - *que j'ai présenté plus
ou moins succinctement {{< inside "web:httpd:httpd" ici >}}*
* un serveur CGI, nommé **slowcgi**, depuis 5.4
* Site web : https://bsd.plumbing/
* OpenBSD : **6.6, 6.7**
---
**Principe** : si le client web informe qu'il accepte l'encodage de compression
aux formats deflate, gzip, br, alors **httpd** passe la main à **slowcgi**
qui délivre le contenu compressé correspondant.
{{< note warning >}}
Il ne s'agit en aucun cas de la compression à la volée !
{{}}
Le problème est que le serveur **httpd** n'est pas capable de gérer la
délivrance de contenu statique compressé.
L'astuce est d'utiliser le serveur CGI **slowcgi**, intégré lui-aussi dans
le système de base, pour assumer la délivrance de ce contenu statique compressé.
En effet, par le biais de script CGI - *ici, en shell* - nous allons pouvoir
assumer la délivrance de ces contenus statiques suivants :
* Formats de fichiers gérés : **atom**, **css**, **html**, **js**, **json**, **svg**, **txt**, **xml**
* aux deux formats de compression : que sont **gzip**, et **brotli**.
## Installation
Il est nécessaire de [télécharger mon script **sbw.cgi**][1].
Une fois téléchargé, à vous de le mettre dans le répertoire `cgi-bin` du
chroot web. Le mieux étant d'utiliser la commande `install` suivante :
`install -o www -g bin -m 0550 sbw.cgi /var/www/cgi-bin/sbw.cgi`
qui nous permet de l'installer proprement avec les droits minimum, strictement
nécessaire, attribué à l'utilisateur web `www`, au groupe `bin`.
### Dépendances
Le script nécessite l'installation de plusieurs binaires et quelques bibliothèques
à l'intérieur du chroot web pour fonctionner correctement.
Il faut donc veiller à copier :
* le shell en premier, ainsi que les binaires **cat**, **date** et **sha256**,
qui se trouvent tous dans le répertoire système `/bin`.
* les binaires **basename**, **logger** {{}}1{{}},
**stat**, qui se trouvent être dans le répertoire système `/usr/bin`
* la bibliothèque C partagée `/usr/lib/libc.so.xx.0` {{}}2{{}}
* la bibliothèque d'exécution `/usr/libexec/ld.so`
vers leurs répertoires respectifs dans le chroot web `/var/www/`.
Si tous doivent être assujettis à l'utilisateur `root` et groupe `bin` :
* chacun des binaires doit avoir des droits 0555, par exemple :
* pour **sh** :
`install -o root -g bin -m 0555 /bin/sh /var/www/bin/`
* pour **stat** :
`install -o root -g bin -m 0555 /usr/bin/stat /var/www/usr/bin/`
* et chacune des bibliothèques, des droits 0444 :
* pour la **libc** :
`install -o root -g bin -m 0444 /usr/lib/libc.so.xx.0 /var/www/usr/lib/`
* pour **ld.so** :
`install -o root -g bin -m 0444 /usr/libexec/ld.so /var/www/usr/libexec/`
Cela nécessite de créer avant les répertoires correspondant dans le chroot web !
Mais comme je suis quelqu'un de très gentil, retrouvez mon [script de gestion des dépendances][2].
---
{{}}1{{}} De l'intérêt du binaire `logger` :
bien sûr que normalement, idéalement, nous n'avons pas besoin du logger, mais
il a pour propos de journaliser certaines actions, qui en cas d'échec, sont
intéressantes à faire écrire dans les journaux `/var/log/{daemon,messages}`.
De même, si la variable `debug` est paramétrée sur `1`, dans la fonction
principale, alors le logger restituera les valeurs correspondantes aux
différentes variables, à fin d'analyse : s'assurer que telle variable reçoit
bien une valeur, dans tel contexte, et si oui, quelle valeur !
{{}}2{{}} La bibliothèque C partagée, entre
chaque version d'OpenBSD, change aussi de nom. Ainsi, pour OpenBSD :
* v6.7 : `libc.so.96.0`
* v6.6 : `libc.so.95.1`
Ce détail est important et peut difficilement être scripté. Donc, lors de
changement de version d'OpenBSD, il faut bien veiller à modifier le script
pour lui définir le nouveau nom de la bibliothèque, sinon il ne fonctionnera
pas !
---
{{< note tip >}}
L'astuce pour savoir quelles sont les dépendances liées à un binaire est
d'utiliser la commande `ldd` sur le binaire en question.
{{}}
## Configuration
### httpd
Il est nécessaire d'ajouter dans votre contexte `server`, les déclarations
`location` suivantes :
{{< code "web-httpd-delivre-fichiers-compresses-location-examples" httpd >}}
Quant au cas du fichier `sbw.conf`, il renferme le contenu des déclarations
`fastcgi` suivantes :
{{< file "web-httpd-delivre-fichiers-compresses-fastcgi-examples" httpd "/etc/httpd.d/sbw.conf" >}}
**Explications** :
Il importe de définir au moins :
* **root** pour lui signifier le chemin relatif au chroot web, du script
CGI à exécuter.
* le paramètre **realroot** : bien indiquer la racine vers votre espace
web, au sein du chroot web.
* les autres paramètres sont certes optionnels mais néanmoins utiles à
envoyer au script CGI, surtout celui de la gestion du cache.
### slowcgi
Le serveur **slowgci** ne nécessite aucune configuration. Il nécessite
seulement d'être activé et démarré avec l'outil de contrôle `rcctl`.
## Histoire
### Des failles
En effet, l'histoire du protocole {{< abbr HTTP "HyperText Transfert Protocol" >}} nous
a révélé deux failles majeures liées à la compression à la volée, ou compression
dynamique de contenu : **CRIME** et **BREACH** - qui est dérivée de la première.
Ces deux failles peuvent même impacter {{< abbr TLS "Transport Layer Secure" >}}.
Même si la plupart des clients web ont été corrigés pour s'efforcer d'atténuer
ces failles, il n'est clairement pas recommandé d'utiliser la méthode de
compression à la volée, que savent très bien gérer la plupart des serveurs HTTP.
Parmi les parades, a été l'adoption depuis HTTP 1.1 du transfert par encodage
de bloc - *la fameuse entête `Transfer-Encoding: chunked`* - de même, il est
fortement recommandé de mettre en place une politique **Referrer** afin de
n'autoriser que la délivrance de contenu compressé QUE depuis le domaine
en cours, et de la refuser depuis tout autre domaine duquel du contenu est
appelé (CSS, JS, fonts, json, etc.).
### Relative à httpd
L'auteur Reyk Floeter se [refuse à la prise en charge de contenu compressé][3].
Et, même si une [requête][4] a été faite pour délivrer du contenu déjà compressé,
ce n'est pas prêt d'être intégré.
### brotli
Le format **brotli** est un format de compression inventé par une équipe
de l'entreprise Google. Il est considéré comme étant le successeur de gzip
car plus rapide et un meilleur taux de compression.
En savoir plus :
* https://brotli.org/
* Est-ce que votre client web le supporte : https://caniuse.com/#search=brotli
À savoir que **curl** gére le format brotli, depuis la sortie de sa version
7.57.0, par l'usage de l'option `--compressed`, voire de l'option `-H` -
*option qui permet de gérer finement les entêtes HTTP*. *(cf : lire son [manpage][7])*
#### clients non supportés
* L'outil **curl** sous OpenBSD semble ne pas supporter le format de compression,
bien que celui-ci soit intégré dans le code de source de l'outil.
* De même, les clients web console que sont **lynx**, et **w3m** ne prennent
pas en charge brotli, quelque soit l'OS.
### Firefox
Ahhh, fichu Firefox, qui depuis la version 64, [ne gére plus nativement les flux][5]
de syndication Atom et RSS.
En fait, c'est plus subtil qu'il n'y paraît. Si vous délivrez le contenu
Atom et RSS avec le mime type **text/xml**, puisqu'après tout, tous les deux
sont bel et bien des fichiers XML, alors Firefox acceptera de les lire
nativement et de vous les afficher. Il ne les mettra pas en forme, mais
il vous affichera le contenu.
Si vous les délivrer avec leur propre type de contenu, à savoir respectivement
**application/atom+xml** et **application/rss+xml**, tous les deux normés
par une {{< abbr RFC "Request For Comment" >}} ou l'autre, alors Firefox
vous demandera quoi en faire !
Une aberration sans nom !!!
### La petite histoire
Pour la petit histoire, Xavier Cartron @prx est l'auteur original de cette
idée de délivrer du contenu statique compressé.
Le travail que j'ai effectué se base sur sa première version de script CGI
shell. Mais elle ne me satisfaisait pas, pour plusieurs raisons, car il
se contente à délivrer au format gzip, QUE SI gzip est demandé.
Il a eu l'idée géniale d'ajouter la gestion de l'entête `ETag`, qui sert à
fixer un identifiant par ressource délivrée. C'est dans ce contexte, que
le binaire **sha256** est utile.
---
Ayant entendu parler du format de compression {{< anchor brotli brotli >}}, j'ai
donc repris/continué l'écriture afin de pouvoir délivrer du contenu statique
compressé précédemment avec ce format.
Ensuite, j'ai ajouté le code nécessaire pour la gestion des entêtes nécessaires :
* `Content-Length` : pour envoyer le poids du document quelque soit sa version ;
*c'est dans ce contexte que le binaire **stat** est nécessaire*.
* `Last-Modified` : pour récupèrer la date de modification du document à
délivrer ; d'aucun considère que cette entête est plus pertinente que **ETag**.
*C'est dans ce contexte que les binaires **date** et **stats** sont utiles*.
* `Transfer-Encoding` : pour envoyer le document à délivrer dans le bon
format de compression, si besoin est.
Puis, j'ai écrit le code nécessaire pour détecter si les dépendances utiles
étaient bien dans le chroot web, autrement le script ne peut fonctionner.
Si les dépendances ne sont pas installées, alors le serveur renvoie une
erreur 500, avec un message HTML décryptant l'erreur.
**Attention** : le script n'installe pas et ne peut pas installer les
dépendances, du fait d'être dans le chroot web, il ne peut voir ce qui se
passe au-delà !
Puis, après quelques recherches sur le web, j'ai compris que le format de
compression **deflate** qui peut être demandé par certains clients web, est
compris dans le format de compression gzip, de là, la prise en charge.
---
Mais j'étais confronté à des dysfonctionnements que je n'arrivais pas à
comprendre et encore moins à résoudre. Et, là deux "choses" m'ont sérieusement
aider à avancer - *car, à moment donné, ne m'en sentant plus les capacités,
j'ai tout simplement laissé tomber, n'y arrivant pas, ne trouvant pas l'aide
nécessaire pour avancer* - :
* Solène Rapenne, de l'équipe d'OpenBSD, m'a aidé à comprendre que je faisais
l'erreur d'envoyer en trop des retours à la ligne, alors qu'il m'en faut
un seul, au bon moment, celui entre l'envoi des différentes entêtes
et l'envoi du document lui-même, qu'il soit compressé ou non.
* l'idée d'implémenter une variable `debug` et d'utiliser le binaire `logger`
pour m'assurer des différents retours.
Une astuce que m'a donné Solène est l'usage en local de cette commande :
`env HTTP_ACCEPT_ENCODING=br realroot=/var/www/htdocs/domaine.tld/dev/ PATH_INFO=index.html /var/www/cgi-bin/sbw.cgi | less`
m'expliquant qu'il est possible d'interroger localement directement le script
CGI, en lui envoyant les différentes valeurs possibles lors de l'appel…
via les variables d'environnements.
Idée géniale !
À ce propos, étant donné que nous avons installé le script shell CGI **sbw.cgi**
avec des droits utilisateurs **0550**, vous aurez le droit à cette petite
erreur : `env: /var/www/cgi-bin/sbw2.cgi: Permission denied`
il suffit de changer le droit d'exécution pour les autres ;-) *(`0551`, ou `+x`)*
---
J'ai amélioré la détection du mime type en l'obtenant à partir du fichier
appelé sur le système - *qui nécessite l'usage du binaire **basename*** -
et la gestion des types de contenu des flux de syndication Atom ou RSS.
Pour finir, j'ai écrit le code nécessaire pour détecter si l'agent utilisateur
était {{< anchor Firefox firefox >}}.
* Si oui, pour récupèrer son numéro de version, *car étant donné
que les flux de syndication ne sont plus correctement supportés*, un petit
hack était nécessaire pour lui délivrer les flux Atom et RSS au format
"trompeur" **text/xml**. Ainsi, Firefox accepte de le lire au-lieu de demander
à l'ouvrir avec une autre application.
J'ai écrit la première mouture de ce hack nécessitant l'ajout des binaires
`grep` et `awk` - *solution qui ne me satisfaisait pas, mais fonctionnelle*.
Et, suite à la [réflexion de l'utilisateur @eol, sur le forum de la communauté
française d'"OpenBSD pour tous"][6], j'ai remplacé par un travail sur l'expansion
des variables en shell.
Et, voilà !
---
Actuellement, @prx a réécrit son script shell en langage C :
* l'avantage est qu'il n'y a besoin d'aucune dépendance, et que c'est "protégé"
par les mesures de sécurité sur les appels système que sont pledge(2)
et unveil(2).
* néanmoins, il ne gère QUE la compression gzip ; de même certaines petites
choses ne sont pas gérées : pas de gestion de l'entête **Last-Modified**,
ni du support brotli ou deflate, ou du moins pas directement|automatiquement.
## Documentation
En savoir plus sur la mise en place d'une politique Referrer : {{< inside "web:http:referrer" >}}
### manpage
* {{< man install >}}
* {{< man slowcgi 8 >}}, {{< man rcctl 8 >}}
* {{< man pledge 2 >}}, {{< man unveil 2 >}}
### Wikipédia
* {{< wp BREACH >}}
---
[1]: https://framagit.org/hucste/tools/-/raw/master/OpenBSD/slowcgi/sbw.cgi
[2]: https://framagit.org/hucste/tools/-/blob/master/OpenBSD/chroot_deps
[3]: https://github.com/reyk/httpd/issues/21
[4]: https://github.com/reyk/httpd/issues/80
[5]: https://support.mozilla.org/fr/kb/remplacer-lecteur-de-flux-firefox
[6]: https://forum.openbsd.fr.eu.org/showthread.php?tid=2620&pid=20898#pid20898
[7]: https://curl.haxx.se/docs/manpage.html