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:
@@ -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 |
|
||||
Reference in New Issue
Block a user