--- 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