feat(vpn): VPN on-demand Tailscale + agent studioE5 standalone

- Agent studioE5 standalone en Go (console + systray)
- VPN on-demand via tailscaled + tailscale up (authkey Headscale)
- Resolver/serveur dans le tailnet studioe5
- Caddy on-demand TLS pour les instances
- Nouveaux endpoints serveur /api/internal/send-to-node
- Suppression des anciens binaires edubox-agent
- Suivi dans SUIVI_VPN_ONDEMAND.md
This commit is contained in:
EduBox Dev
2026-06-23 09:48:00 +00:00
parent dd49993157
commit 124543d658
40 changed files with 1303 additions and 485 deletions
+387
View File
@@ -0,0 +1,387 @@
# Analyse : Intégration de PrestaShop dans EduBox
## 1. Architecture actuelle d'EduBox (vue d'ensemble)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Serveur cloud (ex: alfrednobel.edudeploy.com) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Caddy │──▶│ Next.js │──▶│ Resolver Go │──▶│ PostgreSQL │ │
│ │ TLS on-demand│ │ (dashboard) │ │ (proxy inst.)│ │ (état) │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ └─────────────┘ │
│ │ │ WebSocket 3001 │
│ │ ▼ │
│ │ Agent EduBox (Go) sur PC étudiant via Tailscale/Headscale │
│ │ ┌─────────────┐ │
│ └─────────────▶│ WordPress │ (mu-plugin edubox-public-url.php) │
│ │ Docker/Podman │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Flux d'une requête publique
1. Le navigateur demande `https://<instance-id>.alfrednobel.edudeploy.com/`
2. Caddy termine TLS et transmet au resolver Go (`:443 → resolver:2020`)
3. Le resolver lit PostgreSQL pour trouver le nœud (Tailscale IP) et le port de l'instance
4. Le resolver fait du reverse proxy HTTP vers `http://<tailscale-ip>:<port>`
5. Le resolver ajoute les headers `X-Forwarded-Proto: https`, `X-Forwarded-Host`, `X-Forwarded-Port: 443`
6. L'agent Go sur le PC étudiant a lancé le conteneur WordPress sur `localhost:<port>` (bind `{PORT}:80`)
7. WordPress reçoit la requête en HTTP interne mais le **mu-plugin** `edubox-public-url.php` détecte `HTTP_HOST`/`X-Forwarded-Proto` et redéfinit `WP_HOME`/`WP_SITEURL` à la volée.
### Pourquoi WordPress fonctionne
- WordPress permet de redéfinir `WP_HOME` et `WP_SITEURL` via `wp-config.php` ou un mu-plugin.
- Le mu-plugin intercepte chaque requête et calcule l'URL publique depuis les headers proxy.
- WordPress accepte d'être servi depuis plusieurs domaines simultanément (localhost + sous-domaine public).
---
## 2. Pourquoi PrestaShop est différent (et plus difficile)
### 2.1 Le domaine est stocké en base de données
PrestaShop enregistre l'URL canonique à plusieurs endroits :
- `ps_configuration` :
- `PS_SHOP_DOMAIN`
- `PS_SHOP_DOMAIN_SSL`
- `PS_SSL_ENABLED`
- `ps_shop_url` :
- `domain`
- `domain_ssl`
Ces valeurs sont écrites **une seule fois lors de l'installation automatique** (`PS_INSTALL_AUTO=1`) via `index_cli.php --domain=<PS_DOMAIN>`.
### 2.2 Redirections canoniques strictes
Dans `classes/controller/FrontController.php` :
```php
$match_url = Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
if (!preg_match('/^' . Tools::pRegexp(rawurldecode($canonical_url), '/') . '([&?].*)?$/', $match_url)) {
// ... redirect vers $canonical_url
}
```
- Si l'URL demandée ne correspond pas exactement à l'URL canonique stockée, PrestaShop envoie une 301/302.
- L'URL canonique est générée avec `Tools::getShopDomainSsl(true)` qui combine :
- `ShopUrl::getMainShopDomainSSL()` (domaine figé en base)
- `Configuration::get('PS_SSL_ENABLED')` (protocole figé en base)
### 2.3 Détection SSL
`Tools::usingSecureMode()` truste bien `HTTP_X_FORWARDED_PROTO: https` (testé et confirmé dans `classes/Tools.php`) :
```php
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
return Tools::strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https';
}
```
Donc le resolver transmet correctement le protocole. Le problème ne vient **pas** d'un défaut de PrestaShop sur la détection SSL en lui-même.
### 2.4 Le vrai problème : l'URL canonique est figée et ne correspond pas à l'URL demandée
Scénario avec `PS_ENABLE_SSL=1` et `PS_DOMAIN=<id>.alfrednobel.edudeploy.com` :
- URL demandée : `https://<id>.alfrednobel.edudeploy.com/`
- Requête interne : `http://<tailscale-ip>:<port>/` avec `X-Forwarded-Proto: https`
- PrestaShop détecte : secure mode = true
- URL canonique : `https://<id>.alfrednobel.edudeploy.com/`
- `match_url` : `https://<id>.alfrednobol.edudeploy.com/`
- Devrait correspondre… mais dans les tests, une boucle a quand même eu lieu.
Causes probables de la boucle :
1. **Port dans `HTTP_HOST`** : le resolver envoie `req.Host = host` (sans `:443`), mais Apache/PHP peut parfois enrichir `HTTP_HOST` avec le port interne (`<id>.domain:80`) selon la configuration.
2. **Cache navigateur** : une 301 est mise en cache, masquant le fix.
3. **Apache `.htaccess` généré par PrestaShop** : contient des règles de rewrite qui peuvent interagir avec le proxy (ex: rediriger `/` vers `index.php` avec un protocole différent).
4. **Cookie/SESSION** : la session PHP ou un cookie sécurisé peut forcer une reconnexion/redirection.
### 2.5 Accès `localhost:<port>` redirige vers le domaine public
C'est le comportement normal de PrestaShop quand `PS_DOMAIN` est le domaine public. Pour EduBox, ce n'est pas bloquant si l'accès étudiant se fait via l'URL publique ou via le Tailscale IP du professeur.
---
## 3. Pistes de solutions
### Solution A — Utiliser le mécanisme natif `PS_HANDLE_DYNAMIC_DOMAIN=1` (recommandée à tester en premier)
L'image Docker officielle `prestashop/prestashop` embarque un script `docker_updt_ps_domains.php` qui est copié à la racine et utilisé comme `DirectoryIndex` quand `PS_HANDLE_DYNAMIC_DOMAIN=1`.
Ce script fait :
```php
$domain = Tools::getHttpHost();
$url = ShopUrl::getShopUrls(Configuration::get('PS_SHOP_DEFAULT'))->where('main', '=', 1)->getFirst();
if ($url) {
$url->domain = $domain;
$url->domain_ssl = $domain;
$url->save();
Configuration::updateValue('PS_SHOP_DOMAIN', $domain);
Configuration::updateValue('PS_SHOP_DOMAIN_SSL', $domain);
Tools::generateHtaccess();
Tools::generateRobotsFile();
Tools::clearSmartyCache();
Media::clearCache();
}
Tools::redirect("index.php");
```
**Avantages**
- Mécanisme officiel, pas de module à maintenir.
- Met à jour le domaine dynamiquement à chaque requête sur `/`.
**Inconvénients**
- Exécution PHP + requêtes SQL + régénération `.htaccess` à chaque requête sur `/` → latence perceptible.
- Ne gère pas nativement le HTTPS vs HTTP (domain_ssl = domain, sans tenir compte de X-Forwarded-Proto).
- Nécessite d'être combiné avec une confiance des headers proxy.
**Implémentation proposée**
Dans le `composeConfig` du template PrestaShop :
```yaml
environment:
PS_DOMAIN: {PUBLIC_DOMAIN}
PS_ENABLE_SSL: "1"
PS_HANDLE_DYNAMIC_DOMAIN: "1"
PS_INSTALL_AUTO: "1"
PS_INSTALL_DB: "0"
```
Et monter un fichier Apache `proxy.conf` dans `/etc/apache2/conf-enabled/` :
```apache
SetEnvIf X-Forwarded-Proto https HTTPS=on
SetEnvIf X-Forwarded-Proto https SERVER_PORT=443
SetEnvIf X-Forwarded-Host ^(.+)$ HTTP_HOST=$1
```
Cela permet à `Tools::usingSecureMode()` de retourner `true` et à `Tools::getHttpHost()` de retourner le bon host public.
**Risques**
- Boucle possible si `PS_ENABLE_SSL=1` mais Apache ne reçoit pas `HTTPS=on` (d'où l'importance du `SetEnvIf`).
- Performance : le `docker_updt_ps_domains.php` est exécuté à chaque hit sur `/`.
---
### Solution B — Créer un module/override PrestaShop "EduBox Public URL" (équivalent du mu-plugin WordPress)
Créer un override `override/classes/Configuration.php` (ou un module hooké tôt) qui surcharge `Configuration::get()` pour les clés `PS_SHOP_DOMAIN`, `PS_SHOP_DOMAIN_SSL` et `PS_SSL_ENABLED`.
Exemple d'override :
```php
<?php
class Configuration extends ConfigurationCore {
public static function get($id_lang = null, $id_shop_group = null, $id_shop = null) {
$key = func_num_args() > 0 && is_string(func_get_arg(0)) ? func_get_arg(0) : $id_lang;
if ($key === 'PS_SHOP_DOMAIN' || $key === 'PS_SHOP_DOMAIN_SSL') {
return Tools::getHttpHost(false, false, true); // host sans port
}
if ($key === 'PS_SSL_ENABLED') {
return Tools::usingSecureMode() ? '1' : '0';
}
return call_user_func_array(['ConfigurationCore', 'get'], func_get_args());
}
}
```
**Avantages**
- Le plus proche du mu-plugin WordPress : aucune réécriture de réponses, pas de latence sur `/`.
- Une fois l'override chargé, toutes les URLs générées par PrestaShop utilisent le domaine de la requête courante.
**Inconvénients / pièges**
- L'override doit être pris en compte par l'autoloader. Sous PrestaShop 8, il faut vider `app/cache/prod/class_index.php`, `var/cache/*` et régénérer l'index avec `PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex()`.
- L'image Docker officielle embarque un `class_index.php` pré-généré qu'il faut invalider.
- Si l'installation se fait avec un override déjà présent, PrestaShop peut ne pas l'activer immédiatement.
**Implémentation proposée**
1. Créer `agent/psplugins/Configuration.php` (embarqué dans l'agent).
2. Au `start`/`reset` d'une instance PrestaShop, l'agent :
- copie l'override dans `/var/www/html/override/classes/Configuration.php`
- vide les caches (`rm -rf app/cache/* var/cache/*`)
- régénère l'autoloader (`php -r "require 'config/config.inc.php'; PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex();"`)
3. Ajouter le montage de l'override dans le `composeConfig` PrestaShop via un placeholder `{PS_OVERRIDES_DIR}`.
4. Garder la config Apache `SetEnvIf X-Forwarded-Proto https HTTPS=on` pour la détection SSL.
**Risques**
- Fragilité des versions : l'override dépend de la signature de `ConfigurationCore::get()` qui peut changer.
- Nécessite de bien gérer les caches à chaque redémarrage du conteneur.
---
### Solution C — Installation "localhost" + réécriture complète par le proxy
Installer PrestaShop avec `PS_DOMAIN=localhost:{PORT}` et `PS_ENABLE_SSL=0`.
Le conteneur vit en HTTP interne. Le resolver réécrit :
- Le header `Location` : `http://localhost:<port>/...``https://<id>.domain/...`
- Le body HTML/CSS/JS : toutes les occurrences de `http://localhost:<port>` et `//localhost:<port>`
C'est l'approche "WordPress-like".
**Avantages**
- Pas besoin de modifier PrestaShop.
- Le conteneur est totalement agnostique du domaine public.
**Inconvénients**
- PrestaShop génère énormément d'URLs absolues (assets, liens admin, webhooks, modules, API). La réécriture body n'est jamais exhaustive.
- Les requêtes AJAX/fetch peuvent pointer vers `localhost:<port>` et échouer côté client.
- Le back-office (`/admin`) génère des redirections complexes.
- Très fragile sur le long terme.
**Verdict** : **non recommandée** pour PrestaShop (contrairement à WordPress).
---
### Solution D — Image Docker PrestaShop personnalisée (patch durable)
Créer un `Dockerfile` dérivé de `prestashop/prestashop:8.1` qui :
1. Applique un patch à `classes/Configuration.php` ou `classes/Tools.php` pour supporter nativement `X-Forwarded-Host`/`X-Forwarded-Proto`.
2. Ou embarque directement l'override EduBox + un script d'init qui vide les caches.
3. Configure Apache pour trust les headers proxy.
**Avantages**
- Totalement reproductible : pas d'opération manuelle sur le conteneur en cours de vie.
- Peut être versionnée et testée indépendamment.
**Inconvénients**
- Nécessite de maintenir et publier une image Docker.
- Ajoute une étape de build CI/CD.
- Si PrestaShop sort une nouvelle version, il faut rebaser le patch.
**Implémentation possible**
```dockerfile
FROM prestashop/prestashop:8.1
COPY edubox-proxy.conf /etc/apache2/conf-enabled/
COPY edubox-override/ /var/www/html/override/
RUN chown -R www-data:www-data /var/www/html/override
```
---
### Solution E — Proxy "intelligent" avec substitution à la volée
Remplacer le resolver Go par (ou ajouter devant) un proxy qui fait de la substitution HTML/CSS/JS beaucoup plus fine, par exemple :
- Nginx avec `sub_filter`
- Apache `mod_substitute`
- Un middleware Node.js type `http-proxy-middleware` avec `selfHandleResponse`
**Avantages**
- Peut corriger les liens absolus que PrestaShop génère.
**Inconvénients**
- Même problème que la solution C : impossible d'être exhaustif.
- Ajoute de la latence et de la complexité.
- Peut casser le JS (si on remplace des chaînes dans du code minifié).
**Verdict** : complément possible, mais pas solution principale.
---
### Solution F — Désactiver les redirections canoniques et SSL forcé
Forcer PrestaShop à ne plus faire de redirections canoniques (`PS_CANONICAL_REDIRECT=0`) et à désactiver SSL partout (`PS_SSL_ENABLED=0`, `PS_SSL_ENABLED_EVERYWHERE=0`).
**Avantages**
- Élimine les boucles de redirection.
**Inconvénients**
- Nécessite un accès au back-office pour modifier les paramètres.
- Les liens générés restent en `http://` et pointent vers le domaine d'installation.
- Mauvaise expérience utilisateur (avertissements navigateur, mixed-content si certains assets passent en http).
**Verdict** : à éviter en production.
---
## 4. Recommandation
Je recommande une **combinaison des solutions A et B**, testée méthodiquement :
### Phase 1 — Solution A (test rapide)
1. Réintroduire un template PrestaShop 8.1 dans `server/prisma/seed.ts` avec :
- `PS_DOMAIN: {PUBLIC_DOMAIN}`
- `PS_ENABLE_SSL: "1"`
- `PS_HANDLE_DYNAMIC_DOMAIN: "1"`
- montage d'une config Apache `proxy.conf` pour trust `X-Forwarded-Proto`
2. Rebuilder le serveur, relancer le seed.
3. Créer une nouvelle instance PrestaShop (pas de reset d'ancienne instance).
4. Tester avec `curl -v -L --max-redirs 5 https://<id>.domain/` et en navigation privée.
### Phase 2 — Solution B (solution cible)
Si la solution A est trop lente ou instable, passer à l'override `Configuration.php` :
1. Créer `agent/psplugins/Configuration.php`.
2. Modifier l'agent pour copier l'override et vider/régénérer les caches au démarrage d'une instance PrestaShop.
3. Ajouter le montage `{PS_OVERRIDES_DIR}` dans le compose template.
4. Conserver la config Apache `SetEnvIf X-Forwarded-Proto`.
5. Tester avec `localhost:<port>` ET `https://<id>.domain/`.
### Phase 3 (optionnel) — Solution D
Si les solutions A/B sont trop fragiles d'une version de PrestaShop à l'autre, créer une image Docker dérivée patchée et la référencer dans le template.
---
## 5. Points d'attention pour l'implémentation
### Headers proxy
Le resolver Go transmet déjà les bons headers. Vérifier qu'aucun middleware Next.js ne les modifie (le middleware actuel ne fait que `NextResponse.next()`).
### Binding de port
Le commit `dd49993` a changé le binding de `127.0.0.1:{PORT}:80` à `{PORT}:80`. Cela signifie que le conteneur PrestaShop est accessible depuis n'importe quelle interface sur le PC étudiant. C'est nécessaire car le Tailscale proxy de l'agent écoute sur toutes les interfaces. **Il ne faut pas revenir à `127.0.0.1` sauf si on change la chaîne de proxy.**
### Cache navigateur
Toujours tester en navigation privée et avec `curl -v -L --max-redirs 5` pour éviter les 301 mises en cache.
### Vider les caches PrestaShop
À chaque changement de domaine/config, il faut vider :
```bash
rm -rf /var/www/html/app/cache/*
rm -rf /var/www/html/var/cache/*
php -r "require '/var/www/html/config/config.inc.php'; PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex();"
```
### Logs utiles
Sur le PC étudiant :
```bash
podman logs -f <id>-app-1
podman exec <id>-app-1 cat /var/log/apache2/error.log
```
Sur le serveur :
```bash
docker logs -f edubox-resolver
docker logs -f edubox-caddy
```
---
## 6. Synthèse comparative
| Solution | Complexité | Robustesse | Perf. | Maintien | Recommandation |
|----------|-----------|------------|-------|----------|----------------|
| A — `PS_HANDLE_DYNAMIC_DOMAIN` | Faible | Moyenne | Moyenne (latence `/`) | Faible | **À tester en premier** |
| B — Override `Configuration` | Moyenne | Forte | Bonne | Moyen | **Solution cible** |
| C — localhost + rewrite proxy | Moyenne | Faible | Bonne | Faible | Non recommandée |
| D — Image Docker patchée | Forte | Très forte | Bonne | Fort | Option long terme |
| E — Proxy substitution | Moyenne | Faible | Moyenne | Faible | Complément seulement |
| F — Désactiver SSL/canonical | Faible | Faible | Bonne | Faible | À éviter |