#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.

# SecuBox-Deb :: secubox-relais-maillage — un nœud SANS WAN servi par un nœud
# qui en a un, à travers le maillage wg-mesh (#1670).
#
# Sur le RELAIS (le nœud qui a le WAN, ex. gk2) :
#   relayer <nom> <ip-maillée-du-pair> [port]   (port par défaut : 9080)
#   retirer <nom>
#   etat
# Sur le PAIR (le nœud sans WAN, ex. gk3) :
#   ouvrir <ip-maillée-du-relais> [port]        (port par défaut : 9080)
#   fermer <ip-maillée-du-relais> [port]
# Partout (minuteur horaire, #1682) :
#   synchroniser   satellite : ouvre le port au maître ; relais : expose par
#                  défaut les noms de chaque pair (voir cmd_synchroniser)
#
# CHAÎNE D'UN NOM, SUR LE RELAIS. Le TLS se termine ici et le WAF d'ici inspecte
# (jamais de bypass) ; sbxwaf relaie en HTTP vers le pair, et c'est WireGuard qui
# chiffre sur le fil. Dans l'ordre, chaque étape vérifiée avant la suivante :
#   1. le pair est un pair du maillage (allowed-ips /32 sur wg-mesh) et répond ;
#   2. le DNS PUBLIC du nom pointe vers ce relais — on ne crée rien chez le
#      registraire ici : on refuse et on dit quoi faire ;
#   3. un certificat (certbot webroot, renouvelé par le crochet existant) ;
#   4. le vhost HAProxy, derrière sbxwaf_inspector ;
#   5. la route sbxwaf vers l'IP MAILLÉE du pair. Pas `secubox-waf-route` : il
#      recopie l'adresse d'une route voisine du même port, qui serait la nôtre.
#
# CÔTÉ PAIR, deux choses et rien de plus : le port n'est ouvert QU'AU relais (pas
# à tout le maillage), et nginx croit l'adresse de visiteur que le relais lui
# transmet — sinon chaque visiteur aurait l'adresse du relais. Le maillage n'est
# PAS un réseau local : le visiteur relayé reste un visiteur WAN.
set -euo pipefail
readonly MODULE="relais-maillage"

ROUTES=/etc/secubox/waf/haproxy-routes.json
CERTS_DIR=/data/haproxy/certs
WEBROOT=/usr/share/secubox/www
NFT_CONF=/etc/nftables.conf
NGINX_CONF=/etc/nginx/conf.d/secubox-relais-maillage.conf
BACKEND=sbxwaf_inspector
RESOLVEUR=1.1.1.1
ACME=/root/.acme.sh
REGLAGES=/etc/secubox/relais-maillage.toml
WG_ETAT=/var/lib/secubox/p2p/wg_mesh.json
PORT_PAIR=9080

log()  { printf '[%s] %s\n' "$MODULE" "$*"; }
meurt() { printf '[%s] ERREUR : %s\n' "$MODULE" "$*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || meurt "à lancer en root"

nom_valide()  { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]]; }
ip_valide()   { [[ "$1" =~ ^10\.10\.0\.([0-9]{1,3})$ ]] && [ "${BASH_REMATCH[1]}" -ge 1 ] && [ "${BASH_REMATCH[1]}" -le 254 ]; }
port_valide() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }

domaine_box() {
    python3 -c 'from secubox_core.auth import domaine_box; print(domaine_box())' 2>/dev/null || true
}

# L'IP publique de CE relais : celle que le DNS public donne à son propre Hall.
ip_publique() {
    local d; d="$(domaine_box)"
    [ -n "$d" ] || meurt "domaine de la box inconnu ([global] domain ou [api] sso_cookie_domain)"
    # `|| true` : sous pipefail, un dig vide tuerait le script EN SILENCE.
    dig +short "hall.$d" A @"$RESOLVEUR" | grep -E '^[0-9.]+$' | head -1 || true
}

# Le nom résout-il, auprès du DNS public, vers <ip> ?
resout_vers() { [ "$(dig +short "$1" A @"$RESOLVEUR" | grep -E '^[0-9.]+$' | head -1 || true)" = "$2" ]; }

# Joignable par wg-mesh : l'IP est couverte par les allowed-ips d'un pair — un
# /32 côté maître, tout le réseau maillé côté satellite (le maître y relaie).
pair_du_maillage() {
    wg show wg-mesh allowed-ips 2>/dev/null | awk '{for(i=2;i<=NF;i++) print $i}' \
        | python3 -c 'import ipaddress,sys; ip=ipaddress.ip_address(sys.argv[1]); sys.exit(0 if any(l.strip() and ip in ipaddress.ip_network(l.strip(), strict=False) for l in sys.stdin) else 1)' "$1"
}

routes_ecrire() {   # routes_ecrire <nom> [<ip> <port>] — sans ip : retire
    python3 - "$ROUTES" "$@" <<'PY'
import json, os, sys, shutil, time
chemin, nom = sys.argv[1], sys.argv[2]
try:
    d = json.load(open(chemin))
except FileNotFoundError:
    d = {}
shutil.copy2(chemin, f"{chemin}.avant-relais-{int(time.time())}") if os.path.exists(chemin) else None
if len(sys.argv) > 3:
    d[nom] = [sys.argv[3], int(sys.argv[4])]
else:
    d.pop(nom, None)
tmp = chemin + ".sbxtmp"
with open(tmp, "w") as f:
    json.dump(d, f, indent=2, sort_keys=True)
    f.write("\n")
os.chmod(tmp, os.stat(chemin).st_mode & 0o777 if os.path.exists(chemin) else 0o644)
os.replace(tmp, chemin)
PY
}

vhost_present() {
    python3 - "$1" <<'PY'
import sys, tomllib
d = tomllib.load(open("/etc/secubox/haproxy.toml", "rb"))
v = d.get("vhosts") or {}
it = v.values() if isinstance(v, dict) else v
sys.exit(0 if any(x.get("domain") == sys.argv[1] for x in it) else 1)
PY
}

cmd_relayer() {
    local nom="${1:-}" ip="${2:-}" port="${3:-9080}"
    nom_valide "$nom" || meurt "nom invalide : « $nom »"
    ip_valide "$ip"   || meurt "IP maillée invalide : « $ip » (attendu 10.10.0.x)"
    port_valide "$port" || meurt "port invalide : « $port »"

    log "1/5 pair $ip dans le maillage et joignable…"
    pair_du_maillage "$ip" || meurt "$ip n'est pas un pair de wg-mesh sur ce nœud"
    local code
    code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 -H "Host: $nom" "http://$ip:$port/" || true)
    [ "$code" != "000" ] || meurt "le pair ne répond pas sur $ip:$port — sur le pair : secubox-relais-maillage ouvrir <ip-du-relais> $port"
    log "    le pair répond (HTTP $code)"
    # En automatique, on ne publie pas un service arrêté (5xx) : il le sera à
    # une prochaine synchronisation, s'il répond alors.
    if [ -n "${RELAIS_EN_LOT:-}" ] && [[ "$code" == 5* ]]; then
        meurt "le service ne répond pas sur le pair (HTTP $code)"
    fi

    log "2/5 DNS public de $nom…"
    local pub vu
    pub="$(ip_publique)"; [ -n "$pub" ] || meurt "IP publique du relais introuvable"
    vu="$(dig +short "$nom" A @"$RESOLVEUR" | grep -E '^[0-9.]+$' | head -1 || true)"
    [ "$vu" = "$pub" ] || meurt "$nom résout vers « ${vu:-rien} », pas vers ce relais ($pub) — créer l'enregistrement A chez le registraire (module DNS) puis relancer"
    log "    $nom → $pub"

    log "3/5 certificat…"
    if [ -s "$CERTS_DIR/$nom.pem" ]; then
        log "    déjà servi par HAProxy"
    elif [ -s "$CERTS_DIR/*.${nom#*.}.pem" ]; then
        log "    couvert par le joker *.${nom#*.}"
    else
        certbot certonly --webroot -w "$WEBROOT" -d "$nom" --non-interactive --agree-tos \
            --keep-until-expiring --register-unsafely-without-email >/dev/null \
            || meurt "certbot a échoué pour $nom"
        haproxyctl cert sync >/dev/null
        [ -s "$CERTS_DIR/$nom.pem" ] || meurt "certificat émis mais absent de $CERTS_DIR"
        log "    émis et installé"
    fi

    log "4/5 vhost HAProxy (WAF)…"
    if vhost_present "$nom"; then
        log "    déjà déclaré"
    else
        haproxyctl vhost add "$nom" "$BACKEND" true >/dev/null
    fi

    log "5/5 route sbxwaf → $ip:$port…"
    routes_ecrire "$nom" "$ip" "$port"
    if [[ "$nom" == hall.* ]] && command -v secubox-halls-parc >/dev/null; then
        secubox-halls-parc ajouter "https://$nom" >/dev/null && log "Halls du parc : https://$nom peut encadrer les services d'ici"
    fi
    # En lot (synchroniser), le rechargement et la vérification se font une
    # fois, à la fin.
    [ -n "${RELAIS_EN_LOT:-}" ] && return 0
    haproxyctl generate >/dev/null && haproxyctl reload >/dev/null || meurt "HAProxy n'a pas rechargé"
    systemctl reload secubox-waf-ng 2>/dev/null || true
    sleep 2

    code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 15 --resolve "$nom:443:127.0.0.1" "https://$nom/" || true)
    log "vérification : https://$nom/ → HTTP $code"
    case "$code" in 2??|3??|401|403) log "relais en place : $nom → $ip:$port" ;;
        *) meurt "la chaîne répond $code — voir journalctl -u secubox-waf-ng" ;; esac
}

# ── EXPOSITION PAR DÉFAUT (#1682) ────────────────────────────────────────────
# Certificat JOKER *.<domaine> par DNS-01 (acme.sh + Gandi LiveDNS, comme
# *.gk2.secubox.in) : un seul certificat pour tous les noms d'un pair, au lieu
# d'un par nom. Renouvelé par acme.sh --cron, recopié par secubox-certs-deploy.
joker_assure() {
    local d="$1" pem="$CERTS_DIR/*.$1.pem"
    [ -s "$pem" ] && return 0
    [ -x "$ACME/acme.sh" ] && grep -q '^SAVED_GANDI_LIVEDNS_' "$ACME/account.conf" 2>/dev/null || return 1
    log "certificat joker *.$d (DNS-01)…"
    "$ACME/acme.sh" --issue --dns dns_gandi_livedns -d "*.$d" --keylength ec-256 \
        --home "$ACME" >/dev/null 2>&1 || return 1
    local rep="$ACME/*.${d}_ecc" tmp
    [ -s "$rep/fullchain.cer" ] && [ -s "$rep/*.$d.key" ] || return 1
    tmp="$(mktemp "$CERTS_DIR/.joker.XXXXXX")"
    cat "$rep/fullchain.cer" "$rep/*.$d.key" > "$tmp"
    chown haproxy:secubox "$tmp" 2>/dev/null || true
    chmod 0640 "$tmp"
    mv "$tmp" "$pem"
    log "    *.$d installé"
}

reglage() {   # reglage <clé> <défaut> — lu dans relais-maillage.toml
    python3 - "$REGLAGES" "$1" "$2" <<'PY'
import sys, tomllib, json
chemin, cle, defaut = sys.argv[1:4]
try:
    v = tomllib.load(open(chemin, "rb")).get(cle, json.loads(defaut))
except (OSError, ValueError):
    v = json.loads(defaut)
print(" ".join(v) if isinstance(v, list) else json.dumps(v))
PY
}

# Ce que le relais expose LUI-MÊME : le premier label de chaque route de son
# propre domaine. Un pair n'est relayé que pour ces services-là.
labels_exposes() {
    python3 - "$ROUTES" "$1" <<'PY'
import json, sys
d = json.load(open(sys.argv[1])); dom = "." + sys.argv[2]
print(" ".join(sorted({k[: -len(dom)] for k in d if k.endswith(dom) and "." not in k[: -len(dom)]})))
PY
}

# Pairs à /32 (côté maître) ; maître du satellite (entrée « master » de l'état).
pairs_32() {
    wg show wg-mesh allowed-ips 2>/dev/null | awk '{for(i=2;i<=NF;i++) if ($i ~ /\/32$/) {sub(/\/32$/,"",$i); print $i}}'
}
maitre_du_satellite() {
    python3 - "$WG_ETAT" <<'PY'
import json, sys
try:
    d = json.load(open(sys.argv[1]))
except (OSError, ValueError):
    sys.exit(0)
for p in d.get("peers") or []:
    if p.get("name") == "master" or str(p.get("allowed_ips", "")).endswith("/24"):
        print(p.get("mesh_ip") or ""); break
PY
}

# SYNCHRONISER — sans argument, idempotent, sûr à relancer (minuteur horaire).
#  • Satellite : le port des vhosts est ouvert au SEUL maître, qui voit donc les
#    noms de la box ; real_ip lui fait confiance.
#  • Relais (a des pairs /32) : pour chaque pair, lit /api/v1/webos/public/noms
#    et relaie un nom <svc>.<domaine-du-pair> si, et seulement si :
#      1. <svc>.<domaine-du-relais> est exposé ici (même politique qu'ici) ;
#      2. <svc> n'est pas exclu (défaut : wpad, boot — jamais publics) ;
#      3. le domaine du pair n'est ni celui du relais ni un de ses parents/enfants
#         (un pair ne peut pas revendiquer les noms du relais) ;
#      4. le DNS public du nom pointe vers ce relais (décision du propriétaire du
#         domaine, vérifiée dans cmd_relayer).
#    Le WAF d'ici inspecte tout ; un service réservé au LAN sur le pair y voit un
#    visiteur WAN (real_ip) et reste fermé.
cmd_synchroniser() {
    local maitre
    maitre="$(maitre_du_satellite)"
    if [ -n "$maitre" ] && ip_valide "$maitre" && pair_du_maillage "$maitre"; then
        cmd_ouvrir "$maitre" "$PORT_PAIR" | sed 's/^/  /'
    fi

    # `wg show wg-mesh` echoue quand l'interface n'existe pas (box sans maillage) : sous set -e/pipefail cela tuait le script AVANT
    # la garde ci-dessous (#2532). Pas de pair, ce n'est pas une erreur.
    local pairs; pairs="$(pairs_32 || true)"
    [ -n "$pairs" ] || { log "aucun pair à relayer (pas de pair /32 sur wg-mesh)"; return 0; }
    [ "$(reglage auto true)" = "true" ] || { log "exposition automatique désactivée ($REGLAGES : auto = false)"; return 0; }
    local dom exclus labels
    dom="$(domaine_box)"; [ -n "$dom" ] || meurt "domaine du relais inconnu"
    exclus=" $(reglage exclure '["wpad", "boot"]') "
    labels=" $(labels_exposes "$dom") "

    local ip json dpair nom label ajoutes=0 vus=0 echecs=0
    for ip in $pairs; do
        json="$(curl -s --max-time 6 -H 'Host: hall.localhost' "http://$ip:$PORT_PAIR/api/v1/webos/public/noms" || true)"
        dpair="$(printf '%s' "$json" | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("domaine",""))
except Exception: pass' 2>/dev/null)"
        [ -n "$dpair" ] && nom_valide "$dpair" || continue
        if [ "$dpair" = "$dom" ] || [[ "$dpair" == *".$dom" ]] || [[ "$dom" == *".$dpair" ]]; then
            log "pair $ip : domaine $dpair refusé (celui du relais)"; continue
        fi
        # ÉMETTRE UN JOKER PEUT MASQUER LE DNS. Si les noms du pair résolvent
        # par un joker PARENT (*.secubox.in), le défi DNS-01 crée
        # _acme-challenge.<domaine> : le domaine devient un nœud existant et le
        # joker parent ne s'y applique plus — tous les noms du pair tombent en
        # NXDOMAIN. Un enregistrement *.<domaine> propre au pair l'évite (vécu
        # sur gk3, #1682). On le dit plutôt que de laisser 14 échecs muets.
        local temoin="hall.$dpair" pub; pub="$(ip_publique)"
        local avant=0; resout_vers "$temoin" "$pub" && avant=1
        joker_assure "$dpair" || log "pair $ip : pas de joker *.$dpair — certificats par nom"
        if [ "$avant" = 1 ] && ! resout_vers "$temoin" "$pub"; then
            log "pair $ip : $temoin ne résout plus depuis le défi DNS-01 — créer « *.$dpair A $pub » chez le registraire"
        fi
        for nom in $(printf '%s' "$json" | python3 -c 'import json,sys
print(" ".join(json.load(sys.stdin).get("noms") or []))'); do
            [[ "$nom" == *".$dpair" ]] && nom_valide "$nom" || continue
            label="${nom%.$dpair}"
            [[ "$label" =~ ^[a-z0-9-]+$ ]] || continue
            [[ "$exclus" == *" $label "* ]] && continue
            [[ "$labels" == *" $label "* ]] || continue
            vus=$((vus + 1))
            if python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if d.get(sys.argv[2],[None])[0]==sys.argv[3] else 1)' "$ROUTES" "$nom" "$ip"; then
                continue
            fi
            if ( RELAIS_EN_LOT=1 cmd_relayer "$nom" "$ip" "$PORT_PAIR" ) >/dev/null 2>&1; then
                ajoutes=$((ajoutes + 1)); log "relayé : $nom → $ip"
            else
                echecs=$((echecs + 1)); log "non relayé : $nom (voir : secubox-relais-maillage relayer $nom $ip)"
            fi
        done
    done
    if [ "$ajoutes" -gt 0 ]; then
        haproxyctl generate >/dev/null && haproxyctl reload >/dev/null || meurt "HAProxy n'a pas rechargé"
        systemctl reload secubox-waf-ng 2>/dev/null || true
    fi
    log "synchronisé : $vus nom(s) éligible(s), $ajoutes ajouté(s), $echecs échec(s)"
}

cmd_retirer() {
    local nom="${1:-}"
    nom_valide "$nom" || meurt "nom invalide : « $nom »"
    routes_ecrire "$nom"
    if vhost_present "$nom"; then
        local cle; cle="$(printf '%s' "$nom" | tr '.-' '__')"
        haproxyctl vhost remove "$cle" >/dev/null || true
    fi
    haproxyctl generate >/dev/null && haproxyctl reload >/dev/null || true
    if [[ "$nom" == hall.* ]] && command -v secubox-halls-parc >/dev/null; then
        secubox-halls-parc retirer "https://$nom" >/dev/null || true
    fi
    log "relais retiré pour $nom (certificat conservé, DNS inchangé)"
}

cmd_etat() {
    python3 - "$ROUTES" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
r = {k: v for k, v in d.items() if isinstance(v, list) and str(v[0]).startswith("10.10.0.")}
print("\n".join(f"{k} → {v[0]}:{v[1]}" for k, v in sorted(r.items())) or "aucun nom relayé vers le maillage")
PY
}

# ── côté PAIR ────────────────────────────────────────────────────────────────
regle_nft() { printf 'iifname "wg-mesh" ip saddr %s tcp dport %s accept comment "secubox-relais"' "$1" "$2"; }

# Chaîne vivante qui porte les règles du relais : celle de nftables.conf.
# JAMAIS `nft -f "$NFT_CONF"` : le fichier commence par `flush ruleset` et
# rejouer efface les tables vivantes — bans sbxwaf, ensembles NAC, sortie tor
# (#1721, #1769). Le fichier sert au prochain démarrage ; la chaîne vivante
# est modifiée règle par règle.
chaine_vivante() {
    local t
    for t in secubox_filter filter; do
        nft list chain inet "$t" input >/dev/null 2>&1 && { printf 'inet %s input' "$t"; return 0; }
    done
    return 1
}
poser_a_chaud() {  # règle
    local ch; ch="$(chaine_vivante)" || return 0
    # shellcheck disable=SC2086
    nft list chain $ch 2>/dev/null | grep -qF -- "$1" && return 0
    # shellcheck disable=SC2086
    nft insert rule $ch "$1"
}
retirer_a_chaud() {  # règle
    local ch h; ch="$(chaine_vivante)" || return 0
    # shellcheck disable=SC2086
    nft -a list chain $ch 2>/dev/null | grep -F -- "$1" | sed -n 's/.*# handle \([0-9][0-9]*\)$/\1/p' \
        | while read -r h; do nft delete rule $ch handle "$h"; done || true   # absente : rien à retirer
}

cmd_ouvrir() {
    local relais="${1:-}" port="${2:-9080}"
    ip_valide "$relais" || meurt "IP maillée du relais invalide : « $relais »"
    port_valide "$port" || meurt "port invalide : « $port »"
    pair_du_maillage "$relais" || meurt "$relais n'est pas un pair de wg-mesh sur ce nœud"
    local regle; regle="$(regle_nft "$relais" "$port")"
    if grep -qF "$regle" "$NFT_CONF"; then
        log "pare-feu : déjà ouvert à $relais sur $port"
    else
        # Avant le `drop` terminal (une règle après lui ne sert à rien), validé
        # par `nft -c` avant de remplacer quoi que ce soit.
        python3 - "$NFT_CONF" "$regle" <<'PY'
import re, sys, shutil, time, subprocess, os
chemin, regle = sys.argv[1], sys.argv[2]
t = open(chemin).read()
out, fait = [], False
for l in t.splitlines(keepends=True):
    if not fait and re.match(r"^[ \t]*drop[ \t]*$", l):
        out.append(l[: len(l) - len(l.lstrip())] + regle + "\n"); fait = True
    out.append(l)
if not fait:
    sys.exit("pas de drop terminal dans " + chemin)
tmp = chemin + ".sbxtmp"
open(tmp, "w").write("".join(out))
if subprocess.run(["nft", "-c", "-f", tmp]).returncode != 0:
    os.unlink(tmp); sys.exit("nft -c refuse la règle")
shutil.copy2(chemin, f"{chemin}.avant-relais-{int(time.time())}")
os.replace(tmp, chemin)
PY
        log "pare-feu : $port ouvert au seul relais $relais"
    fi
    # Vivante aussi, même si le fichier l'avait déjà (répare une chaîne vidée).
    poser_a_chaud "$regle"
    local ligne="set_real_ip_from $relais;"
    if [ -f "$NGINX_CONF" ] && grep -qxF "$ligne" "$NGINX_CONF"; then
        log "nginx : le relais $relais est déjà cru"
    else
        { [ -f "$NGINX_CONF" ] || printf '# secubox-relais-maillage (#1670) : relais du maillage dont on croit X-Forwarded-For.\n'
          printf '%s\n' "$ligne"; } >> "$NGINX_CONF"
        nginx -t 2>/dev/null || { sed -i "\|^$ligne\$|d" "$NGINX_CONF"; meurt "nginx -t refuse $NGINX_CONF"; }
        systemctl reload nginx
        log "nginx : adresse de visiteur transmise par $relais crue"
    fi
}

cmd_fermer() {
    local relais="${1:-}" port="${2:-9080}"
    ip_valide "$relais" || meurt "IP maillée du relais invalide"
    local regle; regle="$(regle_nft "$relais" "$port")"
    if grep -qF "$regle" "$NFT_CONF"; then
        cp -p "$NFT_CONF" "$NFT_CONF.avant-relais-$(date +%s)"
        grep -vF "$regle" "$NFT_CONF" > "$NFT_CONF.sbxtmp" && nft -c -f "$NFT_CONF.sbxtmp" \
            && mv "$NFT_CONF.sbxtmp" "$NFT_CONF"
    fi
    retirer_a_chaud "$regle"
    [ -f "$NGINX_CONF" ] && sed -i "\|^set_real_ip_from $relais;\$|d" "$NGINX_CONF" && nginx -t 2>/dev/null && systemctl reload nginx
    log "fermé au relais $relais sur $port"
}

case "${1:-}" in
    relayer) shift; cmd_relayer "$@" ;;
    retirer) shift; cmd_retirer "$@" ;;
    etat)    cmd_etat ;;
    ouvrir)  shift; cmd_ouvrir "$@" ;;
    fermer)  shift; cmd_fermer "$@" ;;
    synchroniser) cmd_synchroniser ;;
    *) sed -n '8,20p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
esac
