Files
edubox/ANALYSE_PRESTASHOP.md
EduBox Dev 124543d658 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
2026-06-23 09:48:00 +00:00

17 KiB

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 :

$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) :

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 :

$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 :

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/ :

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
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

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 :

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 :

podman logs -f <id>-app-1
podman exec <id>-app-1 cat /var/log/apache2/error.log

Sur le serveur :

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