test(hosts): extend the socket-level pass to the two workstation routes #26

Merged
maximus merged 1 commit from issue-16-hosts-monitoring-commissioning into main 2026-09-25 00:42:09 +00:00
Owner

Etend test-curl.sh aux deux routes postes, et documente la mise en service.

Volontairement Refs #16 et pas Fixes #16 : plusieurs criteres d'acceptation ne sont pas encore tenus (ops VPS bloquees, deploiement Vercel bloque en amont). L'issue se ferme a la main quand le dernier tombe, pas au merge de cette PR.

Ce que contient la PR

  • test-curl.sh : 17 -> 41 cas. Les 24 nouveaux couvrent POST /hosts/<id> et GET /hosts au niveau socket — le 413 rendu avant que la connexion soit coupee, les deux tokens qui se refusent mutuellement leurs routes, le 401 avant le 403, la regex de route qui transforme un id malforme et une traversee en 404 avant tout handler, et le round trip ingestion -> persistance -> lecture.
  • Un second serveur sur le port 3098, demarre sans HOSTS_INGEST_TOKEN, pour observer le 503 fail-closed — c'est le seul comportement du lot qu'une instance unique ne peut pas montrer.
  • CLAUDE.md : le compte vitest etait perime (244 -> 251, auth.test.js 19 -> 26) et test-curl.sh n'y figurait pas.

Deux cas ont demande de l'attention pour valoir quelque chose :

  • Le payload hostile insere __proto__ comme du texte. Ecrit __proto__: dans un litteral objet, il aurait pose le prototype et JSON.stringify n'aurait rien emis — le cas aurait teste un payload qui ne portait jamais la cle.
  • Le cas 413 tourne sous set -e pendant que le serveur detruit la socket juste apres avoir vide la reponse. Toutes les sondes passent par un helper qui absorbe ca ; un curl qui n'a vraiment rien recu rapporte 000, ce qui fait echouer le cas au lieu de le laisser passer en silence.

Le trap EXIT deferencait un SERVER_PID non initialise sous set -u : il tombait en erreur au lieu de nettoyer quand quoi que ce soit echouait avant le boot.

Verification

  • npm test : 251 cas verts
  • bash test-curl.sh : 41 cas verts, joue 3 fois de suite pour le cas 413

Hors PR, deja fait

  • Variables Coolify HOSTS_INGEST_TOKEN et HOSTS_ALLOWED_IDS=thinkpad posees, is_runtime=true is_buildtime=false verifie sur les 4 rows (jumeaux preview compris)
  • la-compagnie-maximus@7e3ee04 : procedure d'ajout d'un poste dans docs/coolify-ops.md, rotation conjointe des deux tokens dans docs/secret-rotation-ops.md

Reste a faire

Detaille en commentaire sur #16.

Refs #16

Etend `test-curl.sh` aux deux routes postes, et documente la mise en service. **Volontairement `Refs #16` et pas `Fixes #16`** : plusieurs criteres d'acceptation ne sont pas encore tenus (ops VPS bloquees, deploiement Vercel bloque en amont). L'issue se ferme a la main quand le dernier tombe, pas au merge de cette PR. ## Ce que contient la PR - `test-curl.sh` : 17 -> 41 cas. Les 24 nouveaux couvrent `POST /hosts/<id>` et `GET /hosts` au niveau socket — le 413 rendu avant que la connexion soit coupee, les deux tokens qui se refusent mutuellement leurs routes, le 401 avant le 403, la regex de route qui transforme un id malforme et une traversee en 404 avant tout handler, et le round trip ingestion -> persistance -> lecture. - Un second serveur sur le port 3098, demarre sans `HOSTS_INGEST_TOKEN`, pour observer le 503 fail-closed — c'est le seul comportement du lot qu'une instance unique ne peut pas montrer. - `CLAUDE.md` : le compte vitest etait perime (244 -> 251, `auth.test.js` 19 -> 26) et `test-curl.sh` n'y figurait pas. Deux cas ont demande de l'attention pour valoir quelque chose : - Le payload hostile insere `__proto__` **comme du texte**. Ecrit `__proto__:` dans un litteral objet, il aurait pose le prototype et `JSON.stringify` n'aurait rien emis — le cas aurait teste un payload qui ne portait jamais la cle. - Le cas 413 tourne sous `set -e` pendant que le serveur detruit la socket juste apres avoir vide la reponse. Toutes les sondes passent par un helper qui absorbe ca ; un curl qui n'a vraiment rien recu rapporte `000`, ce qui fait echouer le cas au lieu de le laisser passer en silence. Le trap `EXIT` deferencait un `SERVER_PID` non initialise sous `set -u` : il tombait en erreur au lieu de nettoyer quand quoi que ce soit echouait avant le boot. ## Verification - `npm test` : 251 cas verts - `bash test-curl.sh` : 41 cas verts, joue 3 fois de suite pour le cas 413 ## Hors PR, deja fait - Variables Coolify `HOSTS_INGEST_TOKEN` et `HOSTS_ALLOWED_IDS=thinkpad` posees, `is_runtime=true is_buildtime=false` verifie sur les 4 rows (jumeaux preview compris) - `la-compagnie-maximus@7e3ee04` : procedure d'ajout d'un poste dans `docs/coolify-ops.md`, rotation conjointe des deux tokens dans `docs/secret-rotation-ops.md` ## Reste a faire Detaille en commentaire sur #16. Refs #16
maximus added 1 commit 2026-08-19 00:19:18 +00:00
test-curl.sh predates vitest and still claimed to be the authoritative suite
for one endpoint. It is now the socket-level pass, and its header says so:
vitest covers the logic, this script covers what only a real client and a
real TCP connection can show.

Twenty-four cases for POST /hosts/<id> and GET /hosts, chosen for what they
prove rather than for coverage: the 413 answered before the connection is
cut, the two tokens refusing each other's routes in both directions, 401
landing ahead of 403 so an unauthenticated caller cannot probe the allowlist,
the route regex turning a malformed id and a traversal attempt into 404
before any handler runs, and the ingest-persist-read round trip closing on a
GET that shows the pushed hostname online.

Two cases needed care to be worth anything:

The hostile payload splices "__proto__" into the JSON as text. Written as
__proto__: in an object literal it would set the prototype and JSON.stringify
would emit nothing, leaving the case asserting against a payload that never
carried the key. It now checks the response and the persisted file, whose key
set must be exactly the whitelist.

The 413 case runs under set -e while the server destroys the socket right
after flushing the response, so curl can exit 55/56 having already read the
status line. Every probe goes through an http_code helper that absorbs that;
a curl which truly got nothing reports 000, which fails the case rather than
passing it quietly. -H "Expect:" also suppresses the 100-continue handshake,
which otherwise changes which side notices the reset first.

The fail-closed 503 needs a server booted without HOSTS_INGEST_TOKEN, so the
script now runs a second instance on 3098 for it, and confirms reads still
answer 200 there — the two tokens are independent, including in absence.

The EXIT trap dereferenced an unset SERVER_PID under set -u, which made it
error out instead of cleaning up when anything failed before the boot. Both
PIDs are initialised and the kills guarded.

CLAUDE.md: the vitest count was stale (244 -> 251, auth.test.js 19 -> 26) and
test-curl.sh was undocumented.

Refs #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
maximus added the
status:review
type:infra
labels 2026-08-19 00:19:54 +00:00
Author
Owner

Revue adversariale — APPROVE

Resume : l'extension de test-curl.sh est solide et verifiee par mutation, pas sur parole. Un seul cas (la traversee, cas 23) ne teste pas ce qu'il annonce ; c'est sans risque pour la production, mais a corriger.

Verification effectuee (copie hors depot du head 50a829b, ports 3098/3099 libres au prealable)

  • bash test-curl.sh : 41/41 verts. npx vitest run : 251 cas, auth.test.js 26 : les compteurs de CLAUDE.md sont exacts.
  • Mutations appliquees au serveur, puis restauration de la copie verifiee par diff :
Mutation Resultat
HOST_ROUTE_RE elargi a ^/hosts/(.+)$ 1 rouge (cas 22 ThinkPad -> 403). Le cas 23 reste vert.
413 remplace par un req.destroy() nu 1 rouge (cas 27 -> 000) : le helper http_code n'avale pas l'erreur de transport. Le piege du catch { return 413 } est evite.
sanitizeSnapshot recopie ...payload a l'ecriture 1 rouge (cles persistees : __proto__, injected)

Suggestions (non bloquantes)

  1. test-curl.sh, cas 23 : la traversee n'atteint jamais le serveur sous cette forme. curl normalise /hosts/../../etc/passwd cote client : la ligne de requete emise est POST /etc/passwd (capturee avec nc). Le 404 vient donc de la table de routes, pas de la regex, et le cas passe meme avec la regex elargie. --path-as-is ne sauve rien, puisque URL() normalise a son tour cote serveur. La forme qui exerce vraiment la regex est encodee : "$BASE_URL/hosts/..%2f..%2fetc%2fpasswd". Elle rend 404 sur le code actuel et 403 sous la regex elargie, donc elle rougit la ou il faut. Garder la forme ../ pour documenter la normalisation, en la renommant, et ajouter la forme encodee comme vraie garde. Le corps de la PR (« la regex de route qui transforme ... une traversee en 404 ») decrit pour l'instant ce que seul le cas 22 prouve.
  2. Cas 30, assertion GET : own.includes("polluted") ne peut jamais etre vrai. polluted est imbrique sous __proto__, et JSON.parse cree une cle propre nommee __proto__. La cle a tester est __proto__. La lecture repasse de toute facon par la liste blanche, et l'assertion sur disque tient la garde (mutation 3), donc rien ne passe entre les mailles. Cette assertion-la reste decorative.
  3. Les boucles d'attente de boot tombent en silence apres 5 s ; tous les cas echouent ensuite en 000. C'est acceptable, mais un fail "server on :3099 never came up" explicite rendrait le diagnostic immediat. Le script ne verifie pas non plus que l'ecouteur est bien le serveur qu'il vient de lancer (port pris par un voisin : EADDRINUSE avale par >/dev/null 2>&1). Le defaut existait deja ; il pese davantage avec deux ports.

Conformite

  • Commit unique test(hosts): ... : format conventionnel respecte. Aucun secret : les tokens sont des valeurs de test.
  • Refs #16 volontaire et coherent avec le travail restant. Le label de #16 (status:in-progress) n'est pas bascule en status:approved : l'issue reste ouverte sur des criteres hors PR.
## Revue adversariale — APPROVE **Resume** : l'extension de `test-curl.sh` est solide et verifiee par mutation, pas sur parole. Un seul cas (la traversee, cas 23) ne teste pas ce qu'il annonce ; c'est sans risque pour la production, mais a corriger. ### Verification effectuee (copie hors depot du head `50a829b`, ports 3098/3099 libres au prealable) - `bash test-curl.sh` : 41/41 verts. `npx vitest run` : 251 cas, `auth.test.js` 26 : les compteurs de `CLAUDE.md` sont exacts. - Mutations appliquees au serveur, puis restauration de la copie verifiee par `diff` : | Mutation | Resultat | |---|---| | `HOST_ROUTE_RE` elargi a `^/hosts/(.+)$` | 1 rouge (cas 22 `ThinkPad` -> 403). **Le cas 23 reste vert.** | | 413 remplace par un `req.destroy()` nu | 1 rouge (cas 27 -> `000`) : le helper `http_code` n'avale pas l'erreur de transport. Le piege du `catch { return 413 }` est evite. | | `sanitizeSnapshot` recopie `...payload` a l'ecriture | 1 rouge (cles persistees : `__proto__`, `injected`) | ### Suggestions (non bloquantes) 1. **`test-curl.sh`, cas 23 : la traversee n'atteint jamais le serveur sous cette forme.** curl normalise `/hosts/../../etc/passwd` cote client : la ligne de requete emise est `POST /etc/passwd` (capturee avec `nc`). Le 404 vient donc de la table de routes, pas de la regex, et le cas passe meme avec la regex elargie. `--path-as-is` ne sauve rien, puisque `URL()` normalise a son tour cote serveur. La forme qui exerce vraiment la regex est encodee : `"$BASE_URL/hosts/..%2f..%2fetc%2fpasswd"`. Elle rend 404 sur le code actuel et **403 sous la regex elargie**, donc elle rougit la ou il faut. Garder la forme `../` pour documenter la normalisation, en la renommant, et ajouter la forme encodee comme vraie garde. Le corps de la PR (« la regex de route qui transforme ... une traversee en 404 ») decrit pour l'instant ce que seul le cas 22 prouve. 2. **Cas 30, assertion GET : `own.includes("polluted")` ne peut jamais etre vrai.** `polluted` est imbrique sous `__proto__`, et `JSON.parse` cree une cle propre nommee `__proto__`. La cle a tester est `__proto__`. La lecture repasse de toute facon par la liste blanche, et l'assertion sur disque tient la garde (mutation 3), donc rien ne passe entre les mailles. Cette assertion-la reste decorative. 3. Les boucles d'attente de boot tombent en silence apres 5 s ; tous les cas echouent ensuite en `000`. C'est acceptable, mais un `fail "server on :3099 never came up"` explicite rendrait le diagnostic immediat. Le script ne verifie pas non plus que l'ecouteur est bien le serveur qu'il vient de lancer (port pris par un voisin : `EADDRINUSE` avale par `>/dev/null 2>&1`). Le defaut existait deja ; il pese davantage avec deux ports. ### Conformite - Commit unique `test(hosts): ...` : format conventionnel respecte. Aucun secret : les tokens sont des valeurs de test. - `Refs #16` volontaire et coherent avec le travail restant. Le label de #16 (`status:in-progress`) n'est pas bascule en `status:approved` : l'issue reste ouverte sur des criteres hors PR.
maximus merged commit 00e1743c9d into main 2026-09-25 00:42:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/vps-health-api#26
No description provided.