erplibre/test/test_install_pipe_to_shell.py

86 lines
3.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Ce qui est téléchargé puis EXÉCUTÉ doit d'abord avoir été obtenu.
« curl … | bash » est l'idiome de la moitié des installateurs. Sans « -f »,
curl rend 0 sur une erreur HTTP et livre le CORPS de l'erreur à bash, qui
l'exécute : une page de miroir en panne, un portail captif ou le 504 d'un
cache hors ligne devient une suite de commandes. Le lecteur reçoit alors
« command not found » et la cause véritable ne se lit plus nulle part.
Le contrôle porte sur la PROPRIÉTÉ et non sur un fichier : tout script du
dépôt qui tube un téléchargement dans un interpréteur doit demander à curl
d'échouer.
"""
import re
import sys
import unittest
from pathlib import Path
RACINE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(RACINE))
# « curl » suivi de ce qui n'est pas une nouvelle commande, jusqu'à un tube
# vers un interpréteur. Le « \\ » de continuation de ligne est traversé.
TUBE = re.compile(r"curl\s+((?:[^\n|;&]|\\\n)*?)\|\s*(?:sudo\s+)?(?:ba)?sh\b")
def demande_lechec(options: str) -> bool:
"""« -f » y est-il demandé, sous l'une de ses formes ?
Les options courtes se GROUPENT : « -sSf » vaut « -s -S -f », et
chercher le seul jeton « -f » manquerait la moitié des appels du dépôt.
"""
for mot in options.split():
if mot == "--fail" or mot.startswith("--fail-"):
return True
if mot.startswith("-") and not mot.startswith("--") and "f" in mot[1:]:
return True
return False
def scripts():
for chemin in sorted(RACINE.glob("script/**/*.sh")):
yield chemin, chemin.read_text(encoding="utf-8", errors="replace")
class TestUnTelechargementTubeDansUnShell(unittest.TestCase):
def test_curl_doit_echouer_sur_une_erreur_http(self):
fautifs = []
for chemin, src in scripts():
for options in TUBE.findall(src):
if not demande_lechec(options):
fautifs.append(
f"{chemin.relative_to(RACINE)} : curl {options.strip()}"
)
self.assertEqual(
fautifs,
[],
"le corps d'une erreur HTTP y serait exécuté :\n "
+ "\n ".join(fautifs),
)
def test_les_options_groupees_sont_reconnues(self):
"""« -sSf » vaut « -s -S -f » : les traiter comme un seul jeton
signalerait à tort la moitié des appels du dépôt."""
for bon in ("-fsSL", "-sSf", "-f", "--fail", "--fail-with-body"):
self.assertTrue(demande_lechec(f"{bon} https://x"), bon)
for mauvais in ("-L", "-sSL", "", "--silent"):
self.assertFalse(demande_lechec(f"{mauvais} https://x"), mauvais)
def test_le_motif_trouve_bien_les_tubes_existants(self):
"""Une expression qui ne trouve rien passerait tous les contrôles.
Le dépôt en porte plusieurs : si ce compte tombe à zéro, c'est
l'expression qu'il faut relire, pas le dépôt qu'il faut féliciter.
"""
trouves = sum(len(TUBE.findall(src)) for _c, src in scripts())
self.assertGreaterEqual(trouves, 3, f"seulement {trouves} trouvés")
if __name__ == "__main__":
unittest.main()