Thomas Touhey

Passionné d'informatique.

Faites un service shell, comme dans les années 80 !

Récemment, j'ai regardé WarGames, un film classique sur les années 80 et la technologie de l'époque. Je ne vais ni le décrire ni le critiquer ici, mais alors que je voyais le personnage principal communiquer avec les serveurs d'entreprise (et d'école) au travers d'un simple terminal, j'ai réalisé qu'il n'y en avait plus beaucoup aujourd'hui.

Jee repoussais le fait de faire le site de l'auto-entreprise de ma mère, Teapots Upcycl'in. Mais j'ai décidé d'en faire un shell.

Il est disponible tandis que je rédige cet article, vous pouvez l'essayer en tapant ceci dans un terminal UNIX (je suppose que les utilisateurs de Microsoft Windows devront utiliser PuTTY ou équivalent) :

ssh visitor@www.teapots-upcyclin.com

Dans cet article, j'expliquerai comment je l'ai réalisé.

Réalisation du shell

Avant de rendre le shell accessible depuis Internet, le shell a dû être construit. J'avais déjà fait un module Python pour décoder les données brutes pour Teapots, en somme un dossier avec des fichiers ressemblant à de l'INI et des images, organisés d'une façon normée, pour un générateur de site web statique que j'avais commencé à réaliser. J'ai alors pensé que je ferais le shell dans ce même langage pour ne pas refaire cette procédure de décodage.

Un ami à moi m'a dit que je devrais utiliser la fonction getline dans le module linecache. Mais j'ai trouvé mieux : le module cmd, qui s'occupe de toute la base pour moi, et je n'aurai alors plus qu'à implémenter les commandes… et d'autres petites choses. Je vous recommande de lire la page avant de continuer, puisque je ne vais pas répéter ce qui s'y trouve.

Commençons avec les bases. Vous devrez commencer par faire une classe fille de cmd.Cmd (l'exemple officiel s'intitule TurtleShell). Pour changer l'introduction, vous devez juste remplacer le membre intro, même chose avec le prompt. Puis, pour implémenter des commandes, vous devrez simplement faire des méthodes do_<commande> qui prendront une chaîne en argument (que je sépare à l'aide de shlex.split). Vous pouvez également définir la méthode default (appelée lorsque la commande n'est pas reconnue), et gérer le Contrôle-D (EOF) en implémentant la méthode EOF. Le reste des bases est sur la page de référence, mais vous serez peut-être intéressé par du hacking de shell Python 3 avancé : c'est ce que je vais décrire dans la suite de l'article.

Tout d'abord, le contrôle de la complétion automatique (j'ignore pourquoi ce n'est pas documenté). On l'implémente en remplaçant la méthode completenames, qui par défaut cherche toutes les méthodes dont le nom commence par do_. Dans le shell Teapots, j'ai voulu cacher la méthode EOF que je trouve peu esthétique, ainsi que cacher quelques commandes secrètes. Voici peu ou prou comment je l'ai implémenté :

class YourShell(cmd.Cmd):
    public_commands = ["help", "exit", "list"]
    # ...
    def completenames(self, text, *ignored):
        lt = len(text)
        return [c for c in self.public_commands if c[:lt] == text]

Puis viennent les commandes. Elles sont simples à faire : l'on commence par séparer le texte en argument à l'aide de shlex.split (dans un wrapper qui renvoie ['--help'] si shlex.split soulève une exception), puis si ni "--help" ni "-h" ne font partie des arguments et si le nombre d'arguments est correct, nous exécutons la requête. Afin de garder la classe propre et organisée, quelques fonctions sont externes et sont appelées avec le shell (self) et les arguments sous forme de liste.

Les interruptions telles que SIGINT (Contrôle-C) peuvent être générées avec un bloc try/except avec l'exception KeyboardInterrupt. Je l'ai gérée au niveau de cmdloop(), afin qu'elle agisse comme un signal EOF, mais afin de ne rien faire, il serait nécessaire de remplacer onecmd (où, si je me souviens bien, la commande est lue et interprétée), ce que je n'ai pas souhaité faire.

La gestion de la locale est réalisée assez simplement. Les locales sont dans des fichiers au format YAML (tels que fr.yml ou en.yml) dans un sous-dossier où ils sont seuls (dans le cas du shell Teapots). Il suffit alors de les trouver et de les énumérer pour savoir quelles locales sont disponibles, à savoir fr et en. Une variable globale dans le shell indique les locales qu'il cherchera par défaut (DEFAULT_LOCALES = ["fr", "en"]), et s'il n'en trouve aucune, il prendra la première disponible (pas nécessairement la première trouvée, puisqu'un dictionnaire en Python n'a pas d'ordre !). Les locales trouvées et leurs données sont des variables globales, la locale courante du shell est locale ; d'ici, vous pouvez aisément gérer les locales dans votre shell.

Les couleurs sont également assez simples à réaliser de votre côté, mais cela peut donner des résultats peu élégants. J'ai utilisé le module termcolor. Sa méthode colored est assez puissante et claire :)

Je pense que toutes les choses intéressantes ont été décrites. Je ne partagerai pas le code du shell Teapots puisque j'y ai placé des éléments que je souhaite garder secret, pour le fun ; mais avec ce que je vous ai décrit plus haut, si vous savez développer en Python 3, cela ne devrait pas être trop difficile à imaginer. Essayez de rendre le shell facile à utiliser pour vos utilisateurs, ceci jusqu'aux petits détails ; par exemple, si la commande est inconnue dans le shell Teapots, la méthode default regarde si la "commande" est en réalité un identifiant d'article, et si c'est le cas, il lancera show <commande> ! C'est assez sympathique lorsque le shell veut vous aider :)

Déployer votre shell sur un serveur réel

Maintenant que votre shell marche, vous voudrez probablement que des utilisateurs s'y connectent depuis leur domicile par exemple. La première étape pour cela est de choisir le protocole que nous souhaitons utiliser. J'ai d'abord pensé à Telnet (l'ancêtre non sécurisé de SSH) parce que je connaissais l'animation ASCII de Star Wars sur Telnet sur Blinkenlights.nl qui l'utilisait ; cependant, après quelques recherches, j'ai trouvé qu'il n'était vraiment pas assez sécurisé, et que je voulais une technologie sur laquelle je puisse proposer de se connecter (peut-être pas pour Teapots, mais cela pourrait être une idée pour de futurs projets). J'ai donc choisi SSH.

Maintenant, sur Debian GNU/Linux (distribution sur laquelle ma machine publique tourne), j'utilise le serveur proposé par OpenSSH (option par défaut). Deux possibilités se sont présentées à moi : lancer un serveur différent pour les connexions publiques et déplacer le port pour les opérations "normales" (d'administration) sur un port différent, éventuellement protégé par du port knocking, ou simplement utiliser le serveur normal et n'autoriser que certains comptes à être disponibles publiquement, sans mot de passe ; cette dernière possibilité est la plus simple, mais elle est limitée, je l'ai tout de même choisie.

À présent, créer l'utilisateur et lui assigner le shell n'est pas trop difficile. D'abord, vous devez déclarer le shell en l'ajoutant dans le fichier /etc/shells. Puis, pour créer l'utilisateur et lui assigner le shell, vous pouvez lancer les commandes suivantes (en tant que super-utilisateur, à l'aide de su, sudo ou un équivalent ; et il y a sans doute une façon de faire ça de façon plus conçise, mais ceci fonctionne) :

useradd visitor
passwd -d visitor
chsh -s /usr/bin/myshell visitor

Une fois ceci fait, vous pouvez tenter de vous connecter en tant que votre utilisateur pour voir si le bon shell apparaît, par exemple avec la commande suivante :

sudo su visitor

En effet, vous ne voulez pas de personnes anonymes qui puissent utiliser un shell "réel" et visiter les fichiers sur votre serveur ; bien que si vous gérez les permissions correctement, cela ne soit pas un problème, beaucoup n'y parviennent pas et les fichiers de configurations sont publiquement visibles, ce n'est pas un risque que vous voulez prendre.

Une fois ceci fait, il est temps de se confronter à un problème avec OpenSSH : le login est désactivé pour les utilisateurs n'ayant pas de mot de passe. Vous pouvez faire cela simplement, en disant à tout le monde, par exemple, que le mot de passe de visitor est visitor, mais je ne trouvais pas cela assez simple pour que tout le monde puisse l'utiliser.

Voici comment j'ai pu désactiver la validation du mot de passe pour visitor. Gardez à l'esprit que je ne suis pas un expert en sécurité et que cette manoeuvre peut être dangereuse ; si vous n'avez pas confiance en moi, c'est totalement normal puisque je suis simplement quelqu'un sur Internet que vous ne connaissez probablement pas. Vérifiez par vous-même si la manoeuvre est sécurisée.

Vous devrez modifier les fichiers PAM et la configuration du daemon SSH pour contourner le problème. Dans le dossier PAM (/etc/pam.d), ouvrez le fichier common-auth et remplacez nullok_secure par nullok. Vous pouvez maintenant ouvrir la configuration du daemon SSH (/etc/ssh/sshd_config), aller à la fin du fichier et ajouter quelque chose comme ceci :

Match User visitor
    PasswordAuthentication yes
    PermitEmptyPasswords yes

Redémarrez le daemon SSH (sous Debian GNU/Linux, service sshd restart) ou relancez la machine (radicale mais efficace), et c'est bon, le compte visitor est accessible à tous et toutes !

Conclusion

Si ce n'était pas assez évident, j'aime la ligne de commande, et le fait d'avoir la possibilité de me connecter et d'interagir avec un service au travers d'un shell est comme une fiction, réalisée avec le shell Teapots. Je sais qu'il y a d'autres façons de le faire, comme en communiquant avec une REST API en HTTP, mais ce n'est pas tout à fait la même chose : avec un shell distant, vous avez davantage de contrôle sur le comportement de l'utilisateur, et vous pouvez facilement faire des animations et des applications interactives (déjà entendu parler de sshtron ?) tout en gardant certaines fonctionnalités secrètes plus facilement, puisque la rétro-ingénierie est plus compliquée lorsque l'objet que vous essayez de hacker n'est pas sur une machine que vous contrôlez. Bien sûr, ce n'est pas pour tout le monde (certaines personnes ne sauront même pas ce qu'un shell est !), mais c'est quelque chose de sympathique à réaliser.

Je veux voir davantage de shells, avec l'un implémentant un jeu d'échecs interactif et multijoueurs ! :D

Thomas Touhey

Passionné d'informatique.