Des services internet simples et sobres

Retour accueil


Cet article n’a pas encore été relu, un peu d’indulgence :)

Le “trop-long-j’ai-pas-lu” :

Sur un système unix possédant netcat1 vous pouvez faire

nc bebou.netlib.re 2222

puis taper h ou help et avoir la liste des commandes disponibles.

Pourquoi ?

Les outils existants

J’avais développé quelques services que des proches ou moi trouvions utiles, notamment de quoi conjuguer, récupérer des synonymes, faire une recherche youtube et certainement d’autres à l’avenir. Ces outils sont généralement des scripts shell de scraping assez simples, utilisés en local.
Ils se substituent à des usages inadéquats de sites web ou de recherches Google. Remplacer “ouvrir firefox -> taper ‘conjugaison manger’ -> cliquer sur le premier lien qui a pas l’air de vouloir me montrer mille popups de cookies ou de newsletters -> scroller à travers la page pour trouver la ligne que je veux -> faire un copier-coller” par “ouvrir un terminal -> taper ‘c manger je présent’ -> faire un copier-coller” ou tout autre utilisation davantage automatisée semblait opportun. On économise du temps, de la frustration, des aller-retour clavier/souris, du texte à taper, de la bande passante, des cycles cpu, de la ram et on s’offre en prime une meilleure interopérabilité avec le reste du système. Selon quelle est la source des données on se permet également de travailler en mode hors-ligne.

Qu’est-ce que “service” convoque comme images dans ma tête ?

J’ai eu plusieurs conversations avec des membres de Katzele sur le sens que l’on met derrière le mot “service” numérique. Intuitivement, en me nourrissant davantage des exemples que je trouve autour de moi que d’une réflexion proprement théorique sur le sujet, je conçois le service comme le rapport majoritaire qui existe entre les personnes détenant du matériel et des compétences techniques et les autres, souhaitant faire appel à ce matériel et ces compétences là. La première personne, généralement une entreprise, une association, un état, va à l’aide d’ingénieur·es répondre au besoin en créant un “service”. Il y a mille façons de les créer, en impliquant plus ou moins les personnes qui sont en demande par exemple, ce qui donnera mille formes différentes de services mais j’ai le sentiment que dans l’ensemble quelques caractéristiques restent assez constantes. Premièrement le cadre technique dans lesquels ils s’inscrivent est, en 2024 et ce depuis des années, presque toujours le web. Deuxièmement ils semblent vouloir, parfois volontairement, tenir au à distance les personnes utilisateur·ices du fonctionnement interne du service. La première pose des questions de soutenabilité pour des raisons mille fois évoquées sur ce blog bien que l’on puisse déjà drastiquement optimiser cette variable en faisant du web “sobre” et pour peu que l’on y évolue avec un outillage radicalement différent de celui majoritaire. La seconde va à l’encontre de l’utopie technique rêvée à Katzele dans laquelle la ligne de démarcation entre celleux qui détiennent et savent manipuler les outils et celleux qui ne peuvent que les consommer ou en consommer leurs fruits serait floutée ou à minima moins épaisse, dans la limite de ce que la complexité des outils nous permet d’envisager.

C’est au gré de ces pensées que je n’ai durant ces dernières années jamais sérieusement entrepris d’interfacer mes outils via du web. Je privilégiais le fait de faire des ateliers, des formations, des démos et des articles associés pour distribuer le code localement sur les machines des personnes intéressées, propager les idées et inciter les personnes à faire leurs propres versions quand elles le peuvent.

Évidemment la viabilité de cette stratégie est certainement corrélée à la complexité des idées et du code en question. Plus l’outil est complexe plus il sera intéressant pour moi d’investir une unique fois du temps dans une interface web et dire aux personnes “va simplement sur telle url” plutôt que de dupliquer l’outil ou ses variantes n fois sur n machines de n copaines. De la même manière, la pertinence de la critique est corrélée à l’écart qu’il existe entre la simplicité du service rendu et la complexité de son implémentation technique. C’est pour cela que les services implémentés jusque là sont simples et requiert peu d’interactions avec l’utilisateur·ice. C’est aussi parce que je n’ai pas l’envie ni le temps de faire des choses compliquées.

Mais l’idée fera tout de même son bout de chemin

Et pourtant, pour de multiples raisons que je n’ai pas audité en profondeur, les personnes sont généralement bien plus enthousiasmées par l’idée d’avoir un service web qu’une version locale, indépendante d’un service tiers et à l’interface plus sobre. J’en prends pour exemple un atelier sur l’ensemble de systèmes que j’utilise pour consulter des vidéos youtube2 de plus de deux heures pendant lesquelles les échanges avaient été fréquents et de qualité. Plusieurs jours après au gré du hasard je retombe sur l’une des personnes ayant assisté à l’atelier qui partage son enthousiasme à une tierce personne autour de l’interface web sobre de recherche youtuube et uniquement de cet aspect là. Je n’ai pas de mal à comprendre que c’était peut-être pour elle le seul outil qui se substituait directement à l’interface youtube classique sans pour autant changer ses habitudes mais j’avoue m’être dit “oh un peu dommage 😞”.

Alors fort de cette expérience et pour éviter de tomber dans le sophisme de la solution parfaite3 je me suis dit que j’allais regarder ce qu’il était possible de faire entre l’interface web simple (formulaire html + cgi) et l’outil en ligne de commande en local.

Quelle forme pour ces services ?

L’objectif est de minimiser au moins :

En permettant :

Communiquer sur internet

Sur internet les machines communiquent entre elles via des socket. Un moyen simple de créer des sockets est d’utiliser un outil fait pour qui pourra les ouvrir ou écouter dessus. netcat-openbsd en est un exemple. Il a l’avantage d’être relativement simple, ne faisant “que” 2340 lignes de C, dont le fichier principal a été édité par une vingtaine de personne depuis 25 ans. Sans faire de revue plus détaillée5 j’en conclu que c’est un logiciel suffisamment stable fonctionnement et techniquement pour assurer une bonne soutenabilité. La documentation est raisonnable et présente même quelques exemples6. Par exemple, pour communiquer à un serveur web, nc permet de faire passer tel quel un header HTTP et de recevoir la réponse :

$ nc bebou.netlib.re 80
GET / HTTP/1.1
Host: arthur.bebou.netlib.re

HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Mon, 10 Jun 2024 12:15:14 GMT
Content-Type: text/html
Content-Length: 5940
Last-Modified: Wed, 05 Jun 2024 17:41:29 GMT
Connection: keep-alive
ETag: "6660a349-1734"
Accept-Ranges: bytes

<!DOCTYPE html>
<html lang="fr">
<head>
    <title>Unix et environnement ?</title>
    <link rel=stylesheet href="/style.css" />
    <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="Un blog au sujet de la culture Unix redirigée vers la diminution de l'impact environnementale du numérique" />
<meta name="author" content="Arthur Pons" />
<link rel="icon" href="/favicon.png" />
[...]

Au passage on voit que c’est côté serveur web qu’est géré la partie “virtual-hosting” qui, sur la base de la valeur du Host renverra tel site plutôt qu’un autre.

Cet exemple qui paraîtra trivial pour les personnes ayant commencé à faire de l’informatique il y a plus de vingt an et un peu magique pour la plupart des autres7 met quelque chose en lumière : Il n’y a rien de magique dans la façon dont les machines parlent entre elles via des protocoles. D’ailleurs n’importe quoi peut s’échanger entre deux sockets. Le manuel nous enseigne que (traduit de l’anglais) :

Il est assez aisé de construire un modèle client/serveur très simple avec nc. Dans une console écoutez sur un port donné. Par exemple :

$ nc -l 1234

nc attend une connection sur le port 1234. Dans une seconde console (ou une seconde machine) connectez vous à la machine et le port correspondant :

$ nc -N 127.0.0.1 1234

Il devrait dorénavant y avoir une connection établie entre les deux ports. Ce qui est tapé dans la seconde console sera concaténé à la première et vice-versa.

Admettons que l’on souhaite que ce que l’on tape dans la première console soit traité d’une manière ou d’une autre et nous revienne. Plusieurs versions de netcat proposent une option -e qui passe les données reçues à un script tier pour traitement. Ce n’est pas le cas de netcat-openbsd, il faudra donc trouver un subterfuge. Ca tombe bien, il nous est donné dans le manuel :

Il n’existe pas d’option -c ou -e dans ce netcat mais vous pouvez toujours exécuter une commande après qu’une connection ait été établie en redirigeant des descripteurs de fichiers. Attention, ouvrir un port et laisser n’importe qui exécuter des commandes arbitraires sur votre serveur est DANGEREUX. Si vous avez réellement besoin de le faire voici un exemple :

Côté serveur :

 $ rm -f /tmp/f; mkfifo /tmp/f
 $ cat /tmp/f | /bin/sh -i 2>&1 | nc -l 127.0.0.1 1234 > /tmp/f

Côté client :

  $ nc host.example.com 1234
  $ (shell prompt from host.example.com)

L’example donne carrément un shell interactif à toute personne qui se connecte. C’est effectivement très dangeureux mais en plus ce n’est pas ce que l’on cherche à faire.

Exécuter des commandes à distance

Le serveur est déjà équipé des commandes conjuguer, synonyme etc. On cherche, en se basant sur ce que l’on a appris précédemment, à implémenter le minimum nécessaire sur le serveur pour rendre ces commandes disponibles à distance de manière non authentifiée et sécurisée. L’idée est que la construction de services sous cette forme ne nécessite presque rien de plus que de faire fonctionner les commandes associées en local contrairement à une appli web ou même un cgi.

Si l’on reprend l’exemple

cat /tmp/f | /bin/sh -i 2>&1 | nc -l 127.0.0.1 1234 > /tmp/f

on peut modifier le filtre /bin/sh -i 2>&1 par autre chose. Si l’on veut un service qui modifie toutes les armes par des fleurs on pourra écrire :

$ cat /tmp/f | sed 's/armes/fleurs/g' | nc -l 127.0.0.1 1234 > /tmp/f[^8]

Ce qui donnera côté client

$ nc bebou.netlib.re 1234
Ils prirent les armes

Ah, ça ne fonctionne pas. En effet sed, comme pleins d’autres commandes pour des raisons de performance, ne traite pas les lignes une à une mais en stocke quelques une avant de les faire passer à la moulinette. Il constitue un “buffer”, on dit parfois en franglais qu’il “buffer” son entrée. Pour résoudre ce problème deux solutions. Soit l’outil intègre une option pour ne pas bufferiser (--unbuffered pour GNU sed) soit on utilise la commande stdbuf qui dira à la commande qui lui est passée en argument qu’il ne faut rien bufferiser :

$ cat /tmp/f | stdbuf -o0 sed 's/armes/fleurs/g' | nc -l 127.0.0.1 1234& > /tmp/f
$ nc bebou.netlib.re 1234
Ils prirent les armes
Ils prirent les fleurs

Super ! Nous en savons plus sur comment traiter les données qui arrivent dans le socket du serveur mais nous n’avons toujours pas créé le système que nous voulions. Pour ce faire reprenons l’exemple qui était donné dans le manuel en retirant l’aspect interactif et en ajoutant stdbuf :

$ cat /tmp/f | stdbuf -o0 sh | nc -l 127.0.0.1 1234& > /tmp/f
$ nc bebou.netlib.re 1234
date
lun. 10 juin 2024 16:04:52 CEST

Avec un peu de securité

Sans pour autant récupérer un shell, nous pouvons exécuter des commandes. Pour des raisons évidentes de sécurité nous voulons restreindre ces commandes à celles que nous mettons à disposition, par exemple conjuguer. Pour cela nous pouvons utiliser grep pour filtrer les entrées avant de les passer à sh :

$ cat /tmp/f | stdbuf -o0 grep -E '^conjuguer' | stdbuf -o0 sh | nc -l 127.0.0.1 1234& > /tmp/f
$ nc bebou.netlib.re 1234
ls
conjuguer manger
Indicatif     Présent           je mange
Indicatif     Présent           tu manges
Indicatif     Présent           il mange
Indicatif     Présent           nous mangeons
Indicatif     Présent           vous mangez

C’est mieux, on ne peut plus exécuter d’autres commandes que conjuguer. A moins que…

$ nc bebou.netlib.re 1234
conjuguer;date
lun. 10 juin 2024 16:05:17 CEST

A mince. Le grep n’a pas retiré la ligne puisqu’elle commence bien par “conjuguer”. Le shell a ensuite tenté d’exécuter conjuguer sans argument (en retournant certainement une erreur) puis a compris le point virgule comme délimitant une nouvelle commande, en l’occurrence date, qu’il a exécuté.

Le sh du serveur sur lequel tout cela tourne est dash. J’ai donc consulté sont manuel pour connaître les caractères spéciaux :

Lexical Structure The shell reads input in terms of lines from a file and breaks it up into words at whitespace (blanks and tabs), and at certain sequences of characters that are special to the shell called “operators”. There are two types of operators: control operators and redirection operators (their meaning is discussed later). Following is a list of operators:

   Control operators:
         & && ( ) ; ;; | || <newline>

   Redirection operators:
         < > >| << >> <& >& <<- <>

et

Reserved Words Reserved words are words that have special meaning to the shell and are recognized at the beginning of a line and after a control operator. The following are reserved words:

   !       elif    fi      while   case
   else    for     then    {       }
   do      done    until   if      esac

Une manière un peu bourrine mais je pense, du moins j’espère, assez fiable est de supprimer toutes les occurrences de ces mots et caractères. Nous aurions donc à minima pour les opérateurs et autres caractères importants :

$ cat /tmp/f |
    stdbuf -o0 tr -d '$`&(;|<>' |
    stdbuf -o0 grep -E '^conjuguer' |
    stdbuf -o0 sh |
    nc -l 127.0.0.1 1234& > /tmp/f

Je n’ai pas la certitude que cela empêche toute commande dangereuse d’arriver à la ligne sh. La bonne approche serait certainement de créer une commande spéciale qui parse son entrée et sur la base de certains critères exécute certaines commandes générées par nos soins plutôt que de tenter d’assainir les commandes entrées par l’utilisateur·ice. En attendant le service étant utilisé par très peu de personnes et la survie du serveur n’étant pas absolument primordiale8 j’ai décidé que c’était suffisant. Je vais soumettre l’exercice à des personnes motivées pour tenter de trouver des failles.

Finalement on pourra y ajouter des logs à coup de tee :

$ cat /tmp/f |
    stdbuf -o0 tee prefilter.log |
    stdbuf -o0 tr -d '$`&(;|<>' |
    stdbuf -o0 grep -E '^conjuguer' |
    stdbuf -o0 tee postfilter.log |
    stdbuf -o0 sh |
    nc -l 127.0.0.1 1234& > /tmp/f

On peut ajouter des commandes disponibles en les installant sur la machine et en ajoutant des alternatives à la regex du grep : ^(conjuguer|synonyme). On peut également maintenir un fichier des commandes autorisées, une commande par ligne, et faire :

    stdbuf -o0 grep -wf authorized

Quelques commentaires, bonus et perspectives pour rendre ça un peu sympa

socat

Dans les faits et pour plusieurs raisons que j’ai déjà oublié j’utilise côté serveur socat. Les commandes vues précédemment sont les mêmes, à la suite d’un cat, mais existent dans un script séparé qui est renseigné dans la commande socat qui lance le serveur :

socat -d -d -lf /var/log/socatlog tcp-listen:2222,fork exec:script

TODO : éclaircir pourquoi j’ai finalement choisi socat. En tout cas cet outil score probablement un peu moins bien dans les critères vus ici (32k lignes par ex) mais est plus polyvalent.

Une commande help

Une commande help est une bonne idée ne serait-ce que pour lister les commandes dispos. Pour qu’elle s’affiche au lancement d’une session on peut ajouter un

echo "help pour la liste des commandes"

au début du script. Cela à pour inconvénient d’ajouter cette ligne dans les résultats de commandes lancées non interactivement. Exemple :

$ nc bebou.netlib.re 2222
help pour la liste des commandes
help
h[elp]

    cette aide

s[ynonyme] mot

    la liste des synonymes de "mot"
[...]
s table
tableau    14
répertoire 14
pupitre    13
liste      11
catalogue  11
[...]

mais si l’on passe la commande directement dans stdin (-N dira à nc d’envoyer un caractère de fin de fichier à la fin afin de terminer la connexion et récupérer la main) :

$ echo "s table" | nc -N bebou.netlib.re 2222
help ou h pour obtenir de l'aide
tableau 14
répertoire  14
pupitre 13
liste   11
catalogue   11
[...]
$

On voit que l’on se traine l’aide. Il y a peut-être un moyen d’y remédier côté serveur mais en attendant je script mes usages non interactifs de façon à supprimer la première ligne de résultat. Par exemple, le script zsh que j’utilise pour la conjugaison (noter le sed '1d' qui supprime la première ligne) :

read ?"Verbe (et filtres) : " verbe
echo "conjuguer -c $verbe" | nc -N bebou.netlib.re 2222 | sed '1d'
read -k1

Des scripts pour un peu plus d’interactivité

En jouant avec le service de synonyme je me suis rendu compte qu’il était assez naturel de vouloir sélectionner l’un des synonymes proposer pour voir ses synonymes et ainsi de suite, à la manière de la navigation dans un document avec des liens hypertexte. Il aurait été dommage de perdre totalement cette fonctionnalité là. Pour cela j’ai opté de ne pas complexifier la commande côté serveur mais de l’implémenter côté client :

read ?"Mot : " mot
while [ -n "$mot" ]
do
    oldmot=$mot
    echo $oldmot | cut -f1
    mot=$(echo "synonyme $mot" | nc -N bebou.netlib.re 2222 | sed '1d' | fzy)
done
read -k1

Ce script donne ce genre de session de navigation :

Mot : table
table
>
  tableau   14
  répertoire    14
> pupitre   13
  liste 11

Puis après sélection de pupitre :

Mot : table
table
pupitre
>
> lutrin    50
  console   20
  chaire    20
  table 13
  bureau    4

Une fois la navigation terminée on peut en sortir en appuyant sur “echap”. Pour les personnes initié·es voici mon menu zenu correspondant :

_conjuger
_synonyme
## pre
## react
;; (c)
    read ?"Verbe (et filtres) : " verbe
    echo "conjuguer -c $verbe" | nc -N bebou.netlib.re 2222 | sed '1d'
    read -k1
;; (s)
    read ?"Mot : " mot
    while [ -n "$mot" ]
    do
        oldmot=$mot
        echo $oldmot | cut -f1
        mot=$(echo "synonyme $mot" | nc -N bebou.netlib.re 2222 | sed '1d' | fzy)
    done
    read -k1

Favoriser le local

Malgré l’existence de ces services que je compte bien continuer à maintenir je maintiens ma volonté que les personnes autour de moi les installent en local. Pour favoriser cela je devrais :


  1. sur debian faire sudo apt install netcat-openbsd si vous ne l’avez pas déjà. En réalité tout outil permettant de se connecter à un port sur une machine distante. socat fonctionne, telnet aussi etc. 

  2. article plus très à jour par rapport à ce que je fais aujourd’hui mais passons 

  3. surtout quand on parle de sobriété dans le numérique, lol. 

  4. en écrivant ces lignes je me rends compte qu’ils n’ont rien d’évident. Notamment celle-ci. L’intuition première est que peu de personne = projet simple et facile à maintenir, pleins de personnes = projet complexe et difficile à maintenir. Je pense que cette intuition n’est pas totalement fausse mais il faut envisager d’autres paramètres. Une personne peut, si elle est très productive, construire des logiciels qui ne sont raisonnablement maintenu par la suite que par une équipe de personne (eg Fabrice Bellard avec qemu et ffmpeg). Il est également possible qu’un outil soit créé et maintenu par une seule personne sans être pour autant documenté ou partagée. A moins que cet outil soit très simple sa maintenance posera problème. D’une certaine manière l’existence d’une équipe, et donc de plus de monde, autour d’un outil peut offrir de la résilience en cas de perte d’une personne. Ça n’est probablement qu’en combinant les variables du nombre de personnes actives pour maintenir l’outil, le nombre de personnes connaissant bien son fonctionnement et la documentation associée que l’on parviendrait à dégager un avis pertinent sur le rapport entre personnes impliquées et soutenabilité. Il faudrait également y inclure des variables économiques telles que la pérennité des financements du projet s’ils existent, la popularité des technologies utilisées etc. Bref c’est bien plus compliqué que ce que ces cinq critères de base laissent penser. 

  5. un jour peut-être 

  6. oui oui, si vous avez l’habitude des manuels des outils GNU ça paraît fou. 

  7. je dois avouer que je n’étais pas très bon élève lors de mes études mais je n’ai pas souvenir que l’on m’ait enseigné des protocoles aussi fondamentaux qu’HTTP et SMTP en me faisant expérimenter de la sorte. Tout conservait une aura un peu mystérieuse. On m’a pourtant enseigné TCP dans les détails. 

  8. il ne serait pas très long de le réinstaller tel quel d’autant plus que tout est backupé