Compare commits
1 Commits
73b561ed33
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c2eee6569a |
@@ -2,7 +2,6 @@
|
||||
node_modules/
|
||||
.next/
|
||||
*.log
|
||||
studioE5-data/
|
||||
edubox-data/
|
||||
dist/
|
||||
coverage/
|
||||
@@ -10,19 +9,11 @@ coverage/
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
agent/studioE5-agent
|
||||
agent/studioE5-agent.exe
|
||||
agent/studioE5-agent-mac
|
||||
agent/studioE5-agent-v*
|
||||
agent/edubox-agent
|
||||
agent/edubox-agent.exe
|
||||
agent/edubox-agent-mac
|
||||
agent/edubox-agent-v*
|
||||
server/public/studioE5-agent*
|
||||
server/public/edubox-agent*
|
||||
agent/ui/*.go.html
|
||||
headscale/*.sqlite*
|
||||
headscale/*.key
|
||||
headscale/*.state
|
||||
agent/resolv.conf
|
||||
agent/tailscale-bin/
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
# 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 |
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
:80 {
|
||||
route /studioE5-agent* {
|
||||
route /edubox-agent* {
|
||||
file_server {
|
||||
root /usr/share/caddy/agent
|
||||
}
|
||||
@@ -22,16 +22,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
headscale.studioe5.edudeploy.com:443 {
|
||||
headscale.alfrednobel.edudeploy.com {
|
||||
reverse_proxy headscale:8080
|
||||
}
|
||||
|
||||
studioe5.edudeploy.com:443 {
|
||||
route /studioE5-agent* {
|
||||
file_server {
|
||||
root /usr/share/caddy/agent
|
||||
}
|
||||
gitea.alfrednobel.edudeploy.com {
|
||||
reverse_proxy edubox-gitea:3000
|
||||
}
|
||||
|
||||
alfrednobel.edudeploy.com {
|
||||
reverse_proxy /api/websocket* server:3001
|
||||
reverse_proxy server:3000
|
||||
}
|
||||
@@ -40,15 +39,11 @@ studioe5.edudeploy.com:443 {
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
route /studioE5-agent* {
|
||||
file_server {
|
||||
root /usr/share/caddy/agent
|
||||
}
|
||||
}
|
||||
@instance {
|
||||
not host studioe5.edudeploy.com
|
||||
not host headscale.studioe5.edudeploy.com
|
||||
host *.studioe5.edudeploy.com
|
||||
not host alfrednobel.edudeploy.com
|
||||
not host headscale.alfrednobel.edudeploy.com
|
||||
not host gitea.alfrednobel.edudeploy.com
|
||||
host *.alfrednobel.edudeploy.com
|
||||
}
|
||||
handle @instance {
|
||||
reverse_proxy resolver:2020 {
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
# Suivi – VPN on-demand studioE5 (client A)
|
||||
|
||||
## ✅ Ce qui fonctionne
|
||||
|
||||
1. **Agent standalone (mode console / systray)**
|
||||
- Exécutable : `agent/studioE5-agent`
|
||||
- Config lu depuis `<data-dir>/studioE5-config.json`
|
||||
- Mode console : `-no-tray`
|
||||
|
||||
2. **VPN on-demand dans l'agent**
|
||||
- L’agent ne démarre plus Tailscale au boot.
|
||||
- Le VPN se lance automatiquement à la création/démarrage d’une instance, ou sur commande serveur.
|
||||
- Implémentation basée sur les binaires `tailscaled` + `tailscale up` (pas `tsnet`, car `tsnet` ne loguait pas automatiquement avec une authkey sur un state vierge).
|
||||
|
||||
3. **Commandes serveur → agent**
|
||||
- Endpoint de test : `POST /api/internal/send-to-node`
|
||||
- Actions supportées : `start_vpn`, `stop_vpn`, `start`, `stop`, `reset`, `delete`.
|
||||
|
||||
4. **Resolver/serveur dans le tailnet studioe5**
|
||||
- Service `resolver-vpn` (conteneur Tailscale) partage le netns du `resolver`.
|
||||
- Le resolver peut joindre les IPs Tailscale des nodes (`ping 100.64.0.x` OK).
|
||||
|
||||
5. **Instance WordPress démarrée avec succès**
|
||||
- Le resolver a renvoyé une 302 WordPress via `http://resolver:2020/`.
|
||||
|
||||
6. **Activation zéro-config de l’agent (modèle commercialisable)**
|
||||
- L’agent démarre sans `headscale_url` ni `headscale_auth_key`.
|
||||
- L’utilisateur entre seulement un code d’activation.
|
||||
- Le serveur envoie la config Headscale, l’agent la sauvegarde et démarre le VPN automatiquement.
|
||||
|
||||
## ✅ Blocage levé
|
||||
|
||||
**Rate limit Let’s Encrypt pour `edudeploy.com` est levé.**
|
||||
Le 2026-06-23 vers 09:35 UTC, Caddy a pu obtenir un certificat Let’s Encrypt pour `test-wp-001.studioe5.edudeploy.com` :
|
||||
```
|
||||
tls.obtain: certificate obtained successfully identifier=test-wp-001.studioe5.edudeploy.com issuer=acme-v02.api.letsencrypt.org-directory
|
||||
```
|
||||
|
||||
Le flux complet HTTPS public est désormais validé :
|
||||
```bash
|
||||
curl -sS -I -L https://test-wp-001.studioe5.edudeploy.com/
|
||||
# => HTTP/2 302 -> HTTP/2 200 (WordPress install.php)
|
||||
```
|
||||
|
||||
Le DNS wildcard `*.studioe5.edudeploy.com` est en place. Caddy utilise toujours `tls { on_demand }` et émet un certificat par sous-domaine d’instance.
|
||||
|
||||
## 🎯 Validation du flux HTTPS public
|
||||
|
||||
Le 2026-06-23 09:39 UTC, le flux complet a été validé :
|
||||
|
||||
```text
|
||||
Client (HTTPS) → Caddy (:443) → resolver (:2020) → Tailnet (100.64.0.8) → agent → WordPress (:8001)
|
||||
```
|
||||
|
||||
Résultat :
|
||||
```bash
|
||||
$ curl -sS -I -L https://test-wp-001.studioe5.edudeploy.com/
|
||||
HTTP/2 302
|
||||
location: https://test-wp-001.studioe5.edudeploy.com/wp-admin/install.php
|
||||
...
|
||||
HTTP/2 200
|
||||
```
|
||||
|
||||
- Certificat Let’s Encrypt obtenu automatiquement par Caddy (`tls { on_demand }`).
|
||||
- Le resolver réécrit les en-têtes `Location` et le contenu HTML pour passer de `http://` à `https://`.
|
||||
|
||||
## 📁 Fichiers modifiés (non exhaustif)
|
||||
|
||||
- `agent/tailscale.go` – lancement `tailscaled` + `tailscale up`, gestion start/stop/status.
|
||||
- `agent/websocket.go` – handlers `start_vpn` / `stop_vpn`, `ensureTailscale()`.
|
||||
- `agent/docker.go` – remplacement des placeholders `{PORT}` et `{INSTANCE_ID}` dans les compose.
|
||||
- `docker-compose.yml` – ajout du sidecar `resolver-vpn`, suppression des `cap_add`/`ip route` obsolètes sur `server`/`resolver`.
|
||||
- `Caddyfile` – configuration on-demand TLS pour les instances.
|
||||
- `.env` – clé pré-auth Headscale mise à jour (clé réutilisable).
|
||||
|
||||
## 🧪 Tests / environnement de test actuel
|
||||
|
||||
Agent de test lancé en arrière-plan :
|
||||
- data-dir : `/tmp/studioe5-test-clienta`
|
||||
- node-id : `vps-8fc665eb`
|
||||
- tailnet IP actuelle : `100.64.0.8`
|
||||
- PID : `3151830` (lancé le 2026-06-23 09:36 UTC)
|
||||
|
||||
Instance de test créée :
|
||||
- ID : `test-wp-001`
|
||||
- Node : `vps-8fc665eb`
|
||||
- Port : `8001`
|
||||
- Template : `wordpress-wordpress-latest`
|
||||
- État : WordPress répond sur `http://127.0.0.1:8001` **et en HTTPS public sur `https://test-wp-001.studioe5.edudeploy.com/`**.
|
||||
|
||||
## 🛠️ Commandes utiles pour reprendre
|
||||
|
||||
### Voir l’agent de test
|
||||
```bash
|
||||
pgrep -a studioe5-agent
|
||||
```
|
||||
|
||||
### Relancer l’agent de test (si besoin)
|
||||
```bash
|
||||
mkdir -p /tmp/studioe5-test-clienta
|
||||
cat > /tmp/studioe5-test-clienta/studioE5-config.json <<EOF
|
||||
{
|
||||
"server": "wss://studioe5.edudeploy.com/api/websocket",
|
||||
"headscale_url": "https://headscale.studioe5.edudeploy.com",
|
||||
"headscale_auth_key": "$(grep HEADSCALE_AUTH_KEY /opt/studioe5-client-a/.env | cut -d= -f2)",
|
||||
"node_id": "vps-8fc665eb",
|
||||
"data_dir": "/tmp/studioe5-test-clienta"
|
||||
}
|
||||
EOF
|
||||
cd /opt/studioe5-client-a/agent
|
||||
./studioE5-agent -no-tray -data-dir /tmp/studioe5-test-clienta
|
||||
```
|
||||
|
||||
### Démarrer le VPN manuellement
|
||||
```bash
|
||||
curl -sS -X POST https://studioe5.edudeploy.com/api/internal/send-to-node \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"nodeId":"vps-8fc665eb","message":{"action":"start_vpn"}}'
|
||||
```
|
||||
|
||||
### Voir les nodes Headscale
|
||||
```bash
|
||||
cd /opt/studioe5-client-a
|
||||
docker compose exec -T headscale headscale nodes list studioe5
|
||||
```
|
||||
|
||||
### Tester le resolver (depuis Caddy)
|
||||
```bash
|
||||
cd /opt/studioe5-client-a
|
||||
docker exec studioe5-caddy curl -sS -I -H "Host: test-wp-001.studioe5.edudeploy.com" http://resolver:2020/
|
||||
```
|
||||
|
||||
### Tester en HTTPS public (dès que la limite sera levée)
|
||||
```bash
|
||||
curl -sS -I -L https://test-wp-001.studioe5.edudeploy.com/
|
||||
```
|
||||
|
||||
## 🌐 Flux complet testé via l’API web
|
||||
|
||||
Test réalisé le 2026-06-23 en utilisant le compte superadmin :
|
||||
|
||||
1. **Authentification NextAuth** sur `/api/auth/callback/credentials`.
|
||||
2. **Création d’instance** via `POST /api/instances` :
|
||||
```json
|
||||
{
|
||||
"nodeId": "vps-8fc665eb",
|
||||
"templateId": "wordpress-wordpress-latest",
|
||||
"port": 8002
|
||||
}
|
||||
```
|
||||
→ Instance créée : `cmqqgrur20001lw67t2bdgzkg`.
|
||||
3. Le serveur a automatiquement envoyé l’action `start` au node via WebSocket.
|
||||
4. L’agent a démarré le VPN (si besoin), écrit le compose et a lancé les conteneurs WordPress.
|
||||
5. Caddy a obtenu un certificat Let’s Encrypt pour `cmqqgrur20001lw67t2bdgzkg.studioe5.edudeploy.com`.
|
||||
6. **Validation HTTPS** :
|
||||
```bash
|
||||
curl -sS -I -L https://cmqqgrur20001lw67t2bdgzkg.studioe5.edudeploy.com/
|
||||
# => HTTP/2 302 -> HTTP/2 200 (WordPress install.php)
|
||||
```
|
||||
|
||||
Le flux `UI → API → WebSocket → agent → Docker → VPN → Caddy → HTTPS public` est fonctionnel.
|
||||
|
||||
## 💻 Téléchargement de l’agent
|
||||
|
||||
L’agent est servi par Caddy depuis le dossier `agent/` monté dans le conteneur Caddy (`./agent:/usr/share/caddy/agent`).
|
||||
|
||||
### Binaires disponibles
|
||||
|
||||
- **Windows (archive complète)** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.0-windows.zip`
|
||||
- Contient `studioE5-agent.exe` + `tailscale-bin/windows/` (`tailscale.exe`, `tailscaled.exe`, `wintun.dll`) + `README-Windows.txt`.
|
||||
- **Windows (exécutable seul)** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.0.exe`
|
||||
- Nécessite d’avoir installé Tailscale Windows séparément ou d’avoir les binaires dans `tailscale-bin/windows/`.
|
||||
- **Linux** : `https://studioe5.edudeploy.com/studioE5-agent-v0.3.0`
|
||||
|
||||
### Builder / préparer les binaires
|
||||
|
||||
```bash
|
||||
cd /opt/studioe5-client-a/agent
|
||||
|
||||
# 1. Télécharger les binaires Tailscale Windows (nécessite msitools)
|
||||
./download-tailscale-bins.sh 1.98.4
|
||||
|
||||
# 2. Builder l’agent pour Windows et Linux (macOS nécessite CGO)
|
||||
./build.sh
|
||||
```
|
||||
|
||||
Le `build.sh` génère automatiquement `studioE5-agent-v0.3.0-windows.zip` et copie les binaires versionnés dans `server/public/`.
|
||||
|
||||
### Flow d’activation zéro-config (modèle commercialisable)
|
||||
|
||||
L’élève/employé n’a **aucune configuration technique** à saisir :
|
||||
|
||||
1. **Télécharger** l’agent Windows (`studioE5-agent-v0.3.0-windows.zip`).
|
||||
2. **Extraire** et **lancer** `studioE5-agent.exe`.
|
||||
3. **Entrer le code d’activation** à 6 caractères fourni par l’établissement (affiché dans l’UI locale `http://localhost:7070`).
|
||||
4. L’agent contacte le serveur, le serveur vérifie le code et renvoie **automatiquement** :
|
||||
- l’identité de l’élève (`studentName`)
|
||||
- l’URL Headscale
|
||||
- la clé pré-auth Headscale
|
||||
5. L’agent sauvegarde ces informations localement et **démarre automatiquement le VPN**.
|
||||
6. L’agent est alors visible dans le dashboard et peut recevoir des instances.
|
||||
|
||||
### Configuration manuelle (mode debug / admin)
|
||||
|
||||
Si besoin, on peut toujours forcer une config via `data/studioE5-config.json` :
|
||||
|
||||
```json
|
||||
{
|
||||
"server": "wss://studioe5.edudeploy.com/api/websocket",
|
||||
"headscale_url": "https://headscale.studioe5.edudeploy.com",
|
||||
"headscale_auth_key": "CLE_PREAUTH_ICI",
|
||||
"node_id": "IDENTIFIANT_DU_POSTE",
|
||||
"data_dir": "C:\\studioE5-agent\\data"
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ `headscale_auth_key` doit être une clé pré-auth réutilisable valide pour le tailnet studioe5. Ne jamais commiter cette clé.
|
||||
|
||||
Lancement :
|
||||
```powershell
|
||||
.\studioE5-agent.exe -no-tray -data-dir C:\studioE5-agent\data
|
||||
```
|
||||
|
||||
## 📋 Prochaines étapes à faire
|
||||
|
||||
- [x] ~~Attendre la fin du rate limit Let’s Encrypt~~ (levé le 2026-06-23).
|
||||
- [x] ~~Relancer un test HTTPS sur `https://test-wp-001.studioe5.edudeploy.com/`~~ → **OK** (HTTP/2 200).
|
||||
- [x] ~~Créer une branche dédiée et commiter les modifications VPN on-demand~~ → branche `feat/studioe5-vpn-ondemand`, commit `124543d`. Push vers Gitea à faire dès que le remote sera accessible (actuellement `localhost:3001` et `gitea.alfrednobel.edudeploy.com` injoignables).
|
||||
- [x] ~~Tester le flux complet depuis l’interface web~~ → **OK** via l’API authentifiée (`POST /api/instances`), instance `cmqqgrur20001lw67t2bdgzkg` accessible en HTTPS public.
|
||||
- [ ] **Obtenir un certificat wildcard** pour `*.studioe5.edudeploy.com` (voir étude ci-dessous).
|
||||
- [ ] **Nettoyer les instances/agent de test** une fois le wildcard en place et le push effectué.
|
||||
- [x] ~~Packager les binaires Tailscale pour Windows~~ → **OK**, `download-tailscale-bins.sh` + `studioE5-agent-v0.3.0-windows.zip` prêt.
|
||||
- [ ] **Nettoyer les anciens nodes/volumes Headscale** créés pendant les tests.
|
||||
- [ ] **Documenter la procédure de mise en production** pour le client A (config agent, clés Headscale, ports, etc.).
|
||||
|
||||
## 🔒 Étude certificat wildcard `*.studioe5.edudeploy.com`
|
||||
|
||||
### Pourquoi passer en wildcard ?
|
||||
|
||||
Avec `tls { on_demand }`, Caddy émet **un certificat Let’s Encrypt par sous-domaine d’instance**. Cela expose au rate limit de 50 certificats par domaine principal (`edudeploy.com`) sur 7 jours. Un certificat wildcard unique (`*.studioe5.edudeploy.com`) couvre tous les sous-domaines d’instances et évite ce problème.
|
||||
|
||||
### Contrainte technique
|
||||
|
||||
Un certificat wildcard nécessite le **challenge DNS-01** (le challenge HTTP-01 ne permet pas de valider `*.domain.tld`). Caddy doit donc pouvoir créer un enregistrement TXT automatiquement chez le registrar DNS.
|
||||
|
||||
### Infomaniak (registrar actuel)
|
||||
|
||||
Le DNS de `edudeploy.com` est chez **Infomaniak** :
|
||||
```bash
|
||||
dig NS edudeploy.com +short
|
||||
# nsany1.infomaniak.com.
|
||||
# nsany2.infomaniak.com.
|
||||
```
|
||||
|
||||
Il existe un module Caddy DNS pour Infomaniak :
|
||||
- Repository : `github.com/caddy-dns/infomaniak`
|
||||
- Nécessite un **token API Infomaniak** avec droits DNS.
|
||||
|
||||
### Implémentation à envisager
|
||||
|
||||
1. **Générer un token API Infomaniak** (compte client A ou compte dédié avec accès au domaine).
|
||||
2. **Builder une image Caddy custom** avec le module :
|
||||
```dockerfile
|
||||
FROM caddy:2-builder AS builder
|
||||
RUN xcaddy build --with github.com/caddy-dns/infomaniak
|
||||
|
||||
FROM caddy:2-alpine
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
```
|
||||
3. **Modifier le `Caddyfile`** pour gérer le wildcard :
|
||||
```caddy
|
||||
*.studioe5.edudeploy.com {
|
||||
tls {
|
||||
dns infomaniak {env.INFOMANIAK_API_TOKEN}
|
||||
}
|
||||
reverse_proxy resolver:2020 {
|
||||
header_up Host {host}
|
||||
}
|
||||
}
|
||||
```
|
||||
4. **Ajouter le token dans `.env`** et le passer au conteneur Caddy.
|
||||
5. Supprimer ou ajuster le bloc `:443` actuel qui utilise `on_demand` pour les instances.
|
||||
|
||||
### Alternative sans module DNS
|
||||
|
||||
Obtenir le certificat wildcard manuellement (Certbot DNS-01, acheté, etc.) et le charger dans Caddy :
|
||||
```caddy
|
||||
*.studioe5.edudeploy.com {
|
||||
tls /data/certs/wildcard.crt /data/certs/wildcard.key
|
||||
reverse_proxy resolver:2020 {
|
||||
header_up Host {host}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Inconvénient : renouvellement manuel.
|
||||
|
||||
## 🔧 Notes techniques
|
||||
|
||||
- Le conteneur `resolver-vpn` utilise `network_mode: service:resolver` pour partager le netns avec le resolver.
|
||||
- L’agent utilise `tailscaled --tun=userspace-networking` ; le resolver-vpn utilise un vrai TUN (`tailscale0`).
|
||||
- Le `Caddyfile` actuel utilise `tls { on_demand }` pour les instances. En cas de nouvelle rate limit, on peut temporairement remettre `tls internal` dans le bloc `:443` pour valider le flux sans certificat public.
|
||||
+16
-70
@@ -2,88 +2,34 @@
|
||||
set -e
|
||||
|
||||
VERSION="0.3.0"
|
||||
APP_NAME="studioE5"
|
||||
BIN_NAME="studioE5-agent"
|
||||
LDFLAGS="-X main.version=${VERSION}"
|
||||
|
||||
# On Windows, build a GUI binary so no console window opens on double-click.
|
||||
WIN_LDFLAGS="${LDFLAGS} -H windowsgui"
|
||||
|
||||
echo "Building ${APP_NAME} Agent v${VERSION}..."
|
||||
echo "Building EduBox Agent v${VERSION}..."
|
||||
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags "${WIN_LDFLAGS}" -o ${BIN_NAME}.exe .
|
||||
echo " ${BIN_NAME}.exe (Windows amd64)"
|
||||
cp ${BIN_NAME}.exe "${BIN_NAME}-v${VERSION}.exe"
|
||||
echo " ${BIN_NAME}-v${VERSION}.exe (Windows amd64)"
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent.exe .
|
||||
echo " edubox-agent.exe (Windows amd64)"
|
||||
cp edubox-agent.exe "edubox-agent-v${VERSION}.exe"
|
||||
echo " edubox-agent-v${VERSION}.exe (Windows amd64)"
|
||||
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ${BIN_NAME} .
|
||||
echo " ${BIN_NAME} (Linux amd64)"
|
||||
cp ${BIN_NAME} "${BIN_NAME}-v${VERSION}"
|
||||
echo " ${BIN_NAME}-v${VERSION} (Linux amd64)"
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent .
|
||||
echo " edubox-agent (Linux amd64)"
|
||||
cp edubox-agent "edubox-agent-v${VERSION}"
|
||||
echo " edubox-agent-v${VERSION} (Linux amd64)"
|
||||
|
||||
# macOS build requires CGO for the systray menu; skip gracefully if unavailable.
|
||||
if GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -ldflags "${LDFLAGS}" -o ${BIN_NAME}-mac . 2>/dev/null; then
|
||||
echo " ${BIN_NAME}-mac (macOS amd64)"
|
||||
cp ${BIN_NAME}-mac "${BIN_NAME}-v${VERSION}-mac"
|
||||
echo " ${BIN_NAME}-v${VERSION}-mac (macOS amd64)"
|
||||
MAC_BUILT=1
|
||||
else
|
||||
echo " ${BIN_NAME}-mac (macOS amd64) - skipped, CGO required for systray"
|
||||
MAC_BUILT=0
|
||||
fi
|
||||
|
||||
# Build Windows distribution zip (agent + Tailscale binaries)
|
||||
ZIP_NAME="${BIN_NAME}-v${VERSION}-windows.zip"
|
||||
if [ -d "tailscale-bin/windows" ]; then
|
||||
python3 - <<PY
|
||||
import zipfile, os
|
||||
with zipfile.ZipFile("${ZIP_NAME}", 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.write("${BIN_NAME}.exe", "${BIN_NAME}.exe")
|
||||
for f in ["tailscale.exe", "tailscaled.exe", "wintun.dll"]:
|
||||
zf.write(f"tailscale-bin/windows/{f}", f"tailscale-bin/windows/{f}")
|
||||
readme = r"""${APP_NAME} Agent - Windows
|
||||
=======================
|
||||
1. Extract this archive to a folder (e.g. C:\${APP_NAME}-agent).
|
||||
2. Create a data folder (e.g. C:\${APP_NAME}-agent\data).
|
||||
3. Create the config file data\${BIN_NAME}-config.json:
|
||||
{
|
||||
"server": "wss://studioe5.edudeploy.com/api/websocket",
|
||||
"headscale_url": "https://headscale.studioe5.edudeploy.com",
|
||||
"headscale_auth_key": "YOUR_PREAUTH_KEY",
|
||||
"node_id": "YOUR_NODE_ID",
|
||||
"data_dir": "C:\\${APP_NAME}-agent\\data"
|
||||
}
|
||||
4. Run the agent:
|
||||
${BIN_NAME}.exe -no-tray -data-dir C:\${APP_NAME}-agent\data
|
||||
|
||||
Tailscale binaries (tailscale.exe, tailscaled.exe, wintun.dll) are bundled
|
||||
in tailscale-bin\windows\ and used automatically by the agent.
|
||||
"""
|
||||
zf.writestr("README-Windows.txt", readme)
|
||||
print(f" ${ZIP_NAME}")
|
||||
PY
|
||||
else
|
||||
echo " Warning: tailscale-bin/windows not found, run ./download-tailscale-bins.sh first"
|
||||
fi
|
||||
GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent-mac .
|
||||
echo " edubox-agent-mac (macOS amd64)"
|
||||
cp edubox-agent-mac "edubox-agent-v${VERSION}-mac"
|
||||
echo " edubox-agent-v${VERSION}-mac (macOS amd64)"
|
||||
|
||||
# Copy versioned binaries to server/public so the dashboard can serve them
|
||||
SERVER_PUBLIC="../server/public"
|
||||
if [ -d "${SERVER_PUBLIC}" ]; then
|
||||
cp "${BIN_NAME}-v${VERSION}" "${SERVER_PUBLIC}/${BIN_NAME}-v${VERSION}"
|
||||
cp "${BIN_NAME}-v${VERSION}.exe" "${SERVER_PUBLIC}/${BIN_NAME}-v${VERSION}.exe"
|
||||
if [ "$MAC_BUILT" = "1" ]; then
|
||||
cp "${BIN_NAME}-v${VERSION}-mac" "${SERVER_PUBLIC}/${BIN_NAME}-v${VERSION}-mac"
|
||||
fi
|
||||
if [ -f "${ZIP_NAME}" ]; then
|
||||
cp "${ZIP_NAME}" "${SERVER_PUBLIC}/${ZIP_NAME}"
|
||||
fi
|
||||
cp "edubox-agent-v${VERSION}" "${SERVER_PUBLIC}/edubox-agent-v${VERSION}"
|
||||
cp "edubox-agent-v${VERSION}-mac" "${SERVER_PUBLIC}/edubox-agent-v${VERSION}-mac"
|
||||
cp "edubox-agent-v${VERSION}.exe" "${SERVER_PUBLIC}/edubox-agent-v${VERSION}.exe"
|
||||
echo " Copied versioned binaries to ${SERVER_PUBLIC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Download URLs (once served by Caddy):"
|
||||
echo " https://studioe5.edudeploy.com/${BIN_NAME}-v${VERSION}.exe"
|
||||
echo " https://studioe5.edudeploy.com/${ZIP_NAME}"
|
||||
echo "Done."
|
||||
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// AgentConfig holds user-editable settings for the agent.
|
||||
type AgentConfig struct {
|
||||
Server string `json:"server"`
|
||||
HeadscaleURL string `json:"headscale_url"`
|
||||
HeadscaleAuthKey string `json:"headscale_auth_key"`
|
||||
NodeID string `json:"node_id"`
|
||||
DataDir string `json:"data_dir"`
|
||||
}
|
||||
|
||||
const configFileName = "studioE5-config.json"
|
||||
|
||||
// defaultServerURL is the production WebSocket endpoint baked into the agent.
|
||||
// It can be overridden by the config file for self-hosted or test setups.
|
||||
const defaultServerURL = "wss://studioe5.edudeploy.com/api/websocket"
|
||||
|
||||
// uniqueNodeID returns a stable-ish unique identifier for this machine.
|
||||
// It combines the hostname with a short random suffix so every install is distinct.
|
||||
func uniqueNodeID() string {
|
||||
h, err := os.Hostname()
|
||||
if err != nil || h == "" {
|
||||
h = "node"
|
||||
}
|
||||
b := make([]byte, 4)
|
||||
if _, err := rand.Read(b); err == nil {
|
||||
return fmt.Sprintf("%s-%s", h, hex.EncodeToString(b))
|
||||
}
|
||||
return fmt.Sprintf("%s-%d", h, os.Getpid())
|
||||
}
|
||||
|
||||
// defaultConfig returns sensible defaults for a first run.
|
||||
// The user only needs to provide an activation code; Headscale credentials are
|
||||
// delivered by the server during activation.
|
||||
func defaultConfig(dataDir string) *AgentConfig {
|
||||
return &AgentConfig{
|
||||
Server: defaultServerURL,
|
||||
HeadscaleURL: "",
|
||||
HeadscaleAuthKey: "",
|
||||
NodeID: uniqueNodeID(),
|
||||
DataDir: dataDir,
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWithDefaults fills missing fields from disk with sensible defaults.
|
||||
func mergeWithDefaults(cfg *AgentConfig, dataDir string) *AgentConfig {
|
||||
defaults := defaultConfig(dataDir)
|
||||
if cfg == nil {
|
||||
return defaults
|
||||
}
|
||||
if cfg.Server == "" {
|
||||
cfg.Server = defaults.Server
|
||||
}
|
||||
if cfg.NodeID == "" {
|
||||
cfg.NodeID = defaults.NodeID
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = defaults.DataDir
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// configPath returns the absolute path to the config file.
|
||||
func configPath(dataDir string) string {
|
||||
return filepath.Join(dataDir, configFileName)
|
||||
}
|
||||
|
||||
// loadOrCreateConfig loads the config file. If it does not exist, it creates
|
||||
// one with default values and returns it (the caller can then open the settings UI).
|
||||
func loadOrCreateConfig(dataDir string) (*AgentConfig, bool, error) {
|
||||
cp := configPath(dataDir)
|
||||
|
||||
if _, err := os.Stat(cp); err == nil {
|
||||
data, err := os.ReadFile(cp)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
cfg := &AgentConfig{}
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
cfg = mergeWithDefaults(cfg, dataDir)
|
||||
return cfg, false, nil
|
||||
}
|
||||
|
||||
cfg := defaultConfig(dataDir)
|
||||
if err := saveConfig(dataDir, cfg); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return cfg, true, nil
|
||||
}
|
||||
|
||||
// saveConfig writes the config file to disk.
|
||||
func saveConfig(dataDir string, cfg *AgentConfig) error {
|
||||
cp := configPath(dataDir)
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(cp, data, 0600)
|
||||
}
|
||||
+3
-5
@@ -20,20 +20,18 @@ func getContainerEngine() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
func writeCompose(dataDir, instanceID, compose string, port int) error {
|
||||
func writeCompose(dataDir, instanceID, compose string) error {
|
||||
dir := instanceDir(dataDir, instanceID)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the studioE5 mu-plugin is available and substitute its path
|
||||
// Ensure the EduBox mu-plugin is available and substitute its path
|
||||
muDir, err := writeMUPlugin(dataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compose = strings.ReplaceAll(compose, "{MU_PLUGINS_DIR}", filepath.Dir(muDir))
|
||||
compose = strings.ReplaceAll(compose, "{INSTANCE_ID}", instanceID)
|
||||
compose = strings.ReplaceAll(compose, "{PORT}", fmt.Sprintf("%d", port))
|
||||
|
||||
f := filepath.Join(dir, "docker-compose.yml")
|
||||
return os.WriteFile(f, []byte(compose), 0644)
|
||||
@@ -117,7 +115,7 @@ fi
|
||||
}
|
||||
|
||||
// stripWordPressHardcodedURLs removes hardcoded WP_HOME/WP_SITEURL defines
|
||||
// from wp-config.php so the studioE5 mu-plugin can compute them from the Host
|
||||
// from wp-config.php so the EduBox mu-plugin can compute them from the Host
|
||||
// header. This is useful when repairing older instances created before the
|
||||
// mu-plugin existed.
|
||||
func stripWordPressHardcodedURLs(dataDir, instanceID string) error {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Télécharge les binaires Tailscale Windows depuis l'installateur MSI officiel.
|
||||
# Nécessite: curl, msitools (msiextract)
|
||||
|
||||
VERSION="${1:-1.98.4}"
|
||||
ARCH="amd64"
|
||||
OUTDIR="$(dirname "$0")/tailscale-bin/windows"
|
||||
MSI_URL="https://pkgs.tailscale.com/stable/tailscale-setup-${VERSION}-${ARCH}.msi"
|
||||
TMPDIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMPDIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Downloading Tailscale ${VERSION} Windows installer..."
|
||||
curl -L -o "$TMPDIR/tailscale-setup.msi" "$MSI_URL"
|
||||
|
||||
echo "Extracting binaries..."
|
||||
mkdir -p "$TMPDIR/extract"
|
||||
msiextract -C "$TMPDIR/extract" "$TMPDIR/tailscale-setup.msi" >/dev/null
|
||||
|
||||
echo "Installing to ${OUTDIR}..."
|
||||
mkdir -p "$OUTDIR"
|
||||
cp "$TMPDIR/extract/PFiles64/Tailscale/tailscale.exe" "$OUTDIR/"
|
||||
cp "$TMPDIR/extract/PFiles64/Tailscale/tailscaled.exe" "$OUTDIR/"
|
||||
cp "$TMPDIR/extract/PFiles64/Tailscale/wintun.dll" "$OUTDIR/"
|
||||
|
||||
echo "Done. Installed:"
|
||||
ls -lh "$OUTDIR"
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -1,51 +0,0 @@
|
||||
import zlib
|
||||
import struct
|
||||
|
||||
|
||||
def png_chunk(chunk_type: bytes, data: bytes) -> bytes:
|
||||
return (
|
||||
struct.pack(">I", len(data))
|
||||
+ chunk_type
|
||||
+ data
|
||||
+ struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
|
||||
def create_png(width: int, height: int, pixels: list) -> bytes:
|
||||
"""Create a PNG from a 2D list of (R, G, B) tuples."""
|
||||
raw = bytearray()
|
||||
for row in pixels:
|
||||
raw.append(0) # no filter
|
||||
for r, g, b in row:
|
||||
raw.extend((r, g, b))
|
||||
compressed = zlib.compress(bytes(raw), 9)
|
||||
|
||||
ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
||||
ihdr = png_chunk(b"IHDR", ihdr_data)
|
||||
idat = png_chunk(b"IDAT", compressed)
|
||||
iend = png_chunk(b"IEND", b"")
|
||||
|
||||
return b"\x89PNG\r\n\x1a\n" + ihdr + idat + iend
|
||||
|
||||
|
||||
BLUE = (37, 99, 235)
|
||||
WHITE = (255, 255, 255)
|
||||
SIZE = 64
|
||||
|
||||
pixels = []
|
||||
for y in range(SIZE):
|
||||
row = []
|
||||
for x in range(SIZE):
|
||||
dx = x - SIZE // 2
|
||||
dy = y - SIZE // 2
|
||||
dist = (dx * dx + dy * dy) ** 0.5
|
||||
if dist < SIZE * 0.25:
|
||||
row.append(WHITE)
|
||||
else:
|
||||
row.append(BLUE)
|
||||
pixels.append(row)
|
||||
|
||||
with open("icon.png", "wb") as f:
|
||||
f.write(create_png(SIZE, SIZE, pixels))
|
||||
|
||||
print("Generated icon.png")
|
||||
+41
-1
@@ -3,12 +3,52 @@ module edubox-agent
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.12.2
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
tailscale.com v1.100.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/creachadair/msync v0.7.1 // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gaissmai/bart v0.26.1 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/term v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
||||
)
|
||||
|
||||
+224
-2
@@ -1,11 +1,233 @@
|
||||
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
|
||||
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA=
|
||||
github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs=
|
||||
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
|
||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
|
||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8=
|
||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
|
||||
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
|
||||
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA=
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
|
||||
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 193 B |
+20
-33
@@ -5,24 +5,31 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// version is injected at build time via -ldflags "-X main.version=X.Y.Z"
|
||||
var version = "dev"
|
||||
|
||||
const (
|
||||
AGENT_VERSION = "0.3.0"
|
||||
APP_NAME = "studioE5"
|
||||
)
|
||||
const AGENT_VERSION = "0.3.0"
|
||||
|
||||
var (
|
||||
dataDir = flag.String("data-dir", "./studioE5-data", "Répertoire de données")
|
||||
serverAddr = flag.String("server", "ws://localhost:3001", "Adresse WebSocket du serveur")
|
||||
nodeID = flag.String("node-id", defaultNodeID(), "ID du nœud (défaut: hostname)")
|
||||
dataDir = flag.String("data-dir", "./edubox-data", "Répertoire de données")
|
||||
uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX")
|
||||
noTray = flag.Bool("no-tray", false, "Désactiver l'icône dans la barre système (mode console)")
|
||||
headscaleURL = flag.String("headscale-url", "", "URL du serveur Headscale (ex: http://151.80.60.98:8080)")
|
||||
headscaleAuthKey = flag.String("headscale-auth-key", "", "Clé d'authentification Headscale")
|
||||
)
|
||||
|
||||
func defaultNodeID() string {
|
||||
h, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
@@ -42,39 +49,19 @@ func main() {
|
||||
log.Fatalf("Cannot create data-dir: %v", err)
|
||||
}
|
||||
|
||||
cfg, _, err := loadOrCreateConfig(*dataDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot load config: %v", err)
|
||||
}
|
||||
|
||||
if err := saveConfig(*dataDir, cfg); err != nil {
|
||||
log.Fatalf("Cannot save config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[%s Agent] version=%s node=%s data-dir=%s server=%s", APP_NAME, AGENT_VERSION, cfg.NodeID, *dataDir, cfg.Server)
|
||||
log.Printf("[EduBox Agent] version=%s node=%s data-dir=%s", AGENT_VERSION, *nodeID, *dataDir)
|
||||
|
||||
if *uiEnabled {
|
||||
go startUI(*dataDir, cfg.NodeID, cfg.Server)
|
||||
go startUI(*dataDir, *nodeID, *serverAddr)
|
||||
}
|
||||
|
||||
go startWebSocket(cfg.Server, cfg.NodeID, *dataDir, cfg.HeadscaleURL, cfg.HeadscaleAuthKey)
|
||||
go startWebSocket(*serverAddr, *nodeID, *dataDir)
|
||||
|
||||
shutdownCh := make(chan struct{})
|
||||
if *noTray {
|
||||
log.Printf("[%s Agent] running in console mode (no tray)", APP_NAME)
|
||||
<-shutdownCh
|
||||
return
|
||||
if *headscaleURL != "" && *headscaleAuthKey != "" {
|
||||
go startTailscaleAndReport(*dataDir, *nodeID, *headscaleURL, *headscaleAuthKey)
|
||||
}
|
||||
|
||||
// Run tray on its own locked OS thread; keep main blocked so the process
|
||||
// does not exit when systray is not available (e.g. headless Linux).
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
runTray(APP_NAME, shutdownCh)
|
||||
}()
|
||||
|
||||
<-shutdownCh
|
||||
select {}
|
||||
}
|
||||
|
||||
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/systray"
|
||||
)
|
||||
|
||||
//go:embed icon.png
|
||||
var iconBytes []byte
|
||||
|
||||
const uiURL = "http://localhost:7070"
|
||||
|
||||
func runTray(appName string, shutdownCh chan struct{}) {
|
||||
systray.Run(func() { onTrayReady(appName, shutdownCh) }, func() { onTrayExit(shutdownCh) })
|
||||
}
|
||||
|
||||
func onTrayReady(appName string, shutdownCh chan struct{}) {
|
||||
systray.SetIcon(iconBytes)
|
||||
systray.SetTitle(appName)
|
||||
systray.SetTooltip(fmt.Sprintf("%s Agent - Cliquez pour ouvrir l'interface", appName))
|
||||
|
||||
mOpen := systray.AddMenuItem("Ouvrir l'interface", "Ouvrir l'interface web locale")
|
||||
mInstances := systray.AddMenuItem("Mes instances", "Afficher les instances")
|
||||
mSettings := systray.AddMenuItem("Paramètres", "Ouvrir les paramètres")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter l'agent")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mOpen.ClickedCh:
|
||||
openBrowser(uiURL)
|
||||
case <-mInstances.ClickedCh:
|
||||
openBrowser(uiURL + "#instances")
|
||||
case <-mSettings.ClickedCh:
|
||||
openBrowser(uiURL + "#settings")
|
||||
case <-mQuit.ClickedCh:
|
||||
close(shutdownCh)
|
||||
systray.Quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func onTrayExit(shutdownCh chan struct{}) {
|
||||
log.Printf("Tray exit requested")
|
||||
// If the user did not already trigger shutdown via the menu, signal it now.
|
||||
select {
|
||||
case <-shutdownCh:
|
||||
default:
|
||||
close(shutdownCh)
|
||||
}
|
||||
// Give other goroutines a moment to clean up, then exit.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func openBrowser(url string) {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = "rundll32"
|
||||
args = []string{"url.dll,FileProtocolHandler", url}
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
args = []string{url}
|
||||
default:
|
||||
cmd = "xdg-open"
|
||||
args = []string{url}
|
||||
}
|
||||
|
||||
if err := exec.Command(cmd, args...).Start(); err != nil {
|
||||
log.Printf("Failed to open browser: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeName(name string) string {
|
||||
return strings.ReplaceAll(name, " ", "")
|
||||
}
|
||||
+61
-135
@@ -2,178 +2,104 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
var (
|
||||
tsCmd *exec.Cmd
|
||||
tsCmdMu sync.Mutex
|
||||
tsIP string
|
||||
tsDataDir string
|
||||
tsSocket string
|
||||
)
|
||||
|
||||
type tailscaleStatus struct {
|
||||
Self struct {
|
||||
TailscaleIPs []string `json:"TailscaleIPs"`
|
||||
} `json:"Self"`
|
||||
}
|
||||
|
||||
func tailscaleBin(name string) string {
|
||||
// Prefer bundled binaries (tailscale-bin/<os>/tailscaled etc.).
|
||||
ex, err := os.Executable()
|
||||
if err == nil {
|
||||
bundled := filepath.Join(filepath.Dir(ex), "tailscale-bin", runtime.GOOS, name)
|
||||
if runtime.GOOS == "windows" {
|
||||
bundled += ".exe"
|
||||
}
|
||||
if _, err := os.Stat(bundled); err == nil {
|
||||
return bundled
|
||||
}
|
||||
}
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p
|
||||
}
|
||||
return name
|
||||
}
|
||||
var globalTSServer *tsnet.Server
|
||||
|
||||
func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, error) {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
// Configure tsnet to use our Headscale server
|
||||
os.Setenv("TS_AUTHKEY", authKey)
|
||||
os.Setenv("TS_CONTROL_URL", headscaleURL)
|
||||
|
||||
if tsCmd != nil {
|
||||
return tsIP, nil
|
||||
s := &tsnet.Server{
|
||||
Hostname: nodeID,
|
||||
Dir: dataDir,
|
||||
Logf: log.Printf,
|
||||
}
|
||||
|
||||
if dataDir == "" {
|
||||
return "", fmt.Errorf("tailscale data dir is empty")
|
||||
}
|
||||
tsDataDir = filepath.Join(dataDir, "tailscale")
|
||||
if err := os.MkdirAll(tsDataDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("create tailscale dir: %w", err)
|
||||
}
|
||||
tsSocket = filepath.Join(tsDataDir, "tailscaled.sock")
|
||||
stateFile := filepath.Join(tsDataDir, "tailscaled.state")
|
||||
|
||||
log.Printf("Starting tailscaled for node %s", nodeID)
|
||||
tsCmd = exec.Command(tailscaleBin("tailscaled"),
|
||||
"--state="+stateFile,
|
||||
"--socket="+tsSocket,
|
||||
"--tun=userspace-networking",
|
||||
)
|
||||
tsCmd.Stdout = os.Stdout
|
||||
tsCmd.Stderr = os.Stderr
|
||||
if err := tsCmd.Start(); err != nil {
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("start tailscaled: %w", err)
|
||||
if err := s.Start(); err != nil {
|
||||
return "", fmt.Errorf("tailscale start: %w", err)
|
||||
}
|
||||
|
||||
// Give tailscaled a moment to start listening.
|
||||
time.Sleep(1 * time.Second)
|
||||
globalTSServer = s
|
||||
|
||||
// Bring the interface up with the auth key.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
// Wait for Tailscale to come up and retrieve IP
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
upCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
"--socket="+tsSocket,
|
||||
"up",
|
||||
"--authkey="+authKey,
|
||||
"--login-server="+headscaleURL,
|
||||
"--hostname="+nodeID,
|
||||
"--accept-dns=false",
|
||||
"--operator=root",
|
||||
)
|
||||
upCmd.Stdout = os.Stdout
|
||||
upCmd.Stderr = os.Stderr
|
||||
if err := upCmd.Run(); err != nil {
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("tailscale up: %w", err)
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tailscale local client: %w", err)
|
||||
}
|
||||
|
||||
// Wait for an IP address.
|
||||
var tailscaleIP string
|
||||
for {
|
||||
out, err := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
"--socket="+tsSocket,
|
||||
"status", "--json",
|
||||
).Output()
|
||||
status, err := lc.Status(ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("tailscale status: %w", err)
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
var st tailscaleStatus
|
||||
if err := json.Unmarshal(out, &st); err == nil && len(st.Self.TailscaleIPs) > 0 {
|
||||
tsIP = st.Self.TailscaleIPs[0]
|
||||
if status.Self != nil && len(status.Self.TailscaleIPs) > 0 {
|
||||
tailscaleIP = status.Self.TailscaleIPs[0].String()
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("tailscale IP timeout")
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Tailscale started with IP: %s", tsIP)
|
||||
return tsIP, nil
|
||||
log.Printf("Tailscale started with IP: %s", tailscaleIP)
|
||||
return tailscaleIP, nil
|
||||
}
|
||||
|
||||
func stopTailscale() {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
stopTailscaleLocked()
|
||||
func startTailscaleProxy(port int) (net.Listener, error) {
|
||||
if globalTSServer == nil {
|
||||
return nil, fmt.Errorf("tailscale server not started")
|
||||
}
|
||||
|
||||
func stopTailscaleLocked() {
|
||||
if tsCmd == nil || tsCmd.Process == nil {
|
||||
ln, err := globalTSServer.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tailscale listen on port %d: %w", port, err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Printf("tailscale proxy accept error on port %d: %v", port, err)
|
||||
return
|
||||
}
|
||||
if tsSocket != "" {
|
||||
_ = exec.Command(tailscaleBin("tailscale"), "--socket="+tsSocket, "down").Run()
|
||||
go handleProxyConn(conn, port)
|
||||
}
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
tsIP = ""
|
||||
log.Printf("Tailscale stopped")
|
||||
}()
|
||||
log.Printf("Tailscale proxy started on port %d", port)
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
func isTailscaleRunning() bool {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
if tsCmd == nil || tsCmd.Process == nil {
|
||||
return false
|
||||
func handleProxyConn(src net.Conn, port int) {
|
||||
defer src.Close()
|
||||
dst, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
if err != nil {
|
||||
log.Printf("tailscale proxy dial localhost:%d error: %v", port, err)
|
||||
return
|
||||
}
|
||||
// Signal 0 checks process existence without affecting it.
|
||||
return tsCmd.Process.Signal(syscall.Signal(0)) == nil
|
||||
defer dst.Close()
|
||||
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
_, _ = io.Copy(dst, src)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
_, _ = io.Copy(src, dst)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
<-done
|
||||
}
|
||||
|
||||
func getTailscaleIP() string {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
return tsIP
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-54
@@ -2,12 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -23,55 +20,6 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
fmt.Fprint(w, uiHTML)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
cfg, _, err := loadOrCreateConfig(dataDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Do not expose the auth key in plain GET unless requested; for local UI it is fine.
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
case http.MethodPost:
|
||||
var cfg AgentConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = dataDir
|
||||
}
|
||||
if err := saveConfig(dataDir, &cfg); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/restart", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
go func() {
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("Restart failed: %v", err)
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
|
||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
@@ -124,9 +72,9 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
})
|
||||
|
||||
port := "7070"
|
||||
log.Printf("%s UI starting on http://localhost:%s", APP_NAME, port)
|
||||
log.Printf("UI starting on http://localhost:%s", port)
|
||||
if err := http.ListenAndServe("127.0.0.1:"+port, nil); err != nil {
|
||||
log.Fatalf("%s UI server error: %v", APP_NAME, err)
|
||||
log.Fatalf("UI server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-101
@@ -2,7 +2,7 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>studioE5 Agent</title>
|
||||
<title>EduBox Agent</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: #f1f5f9; margin: 0; padding: 2rem; color: #1e293b; }
|
||||
@@ -10,13 +10,9 @@
|
||||
.card { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1rem; }
|
||||
h1 { font-size: 1.5rem; margin: 0 0 1rem; }
|
||||
h2 { font-size: 1.125rem; margin: 0 0 1rem; }
|
||||
label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.25rem; color: #475569; }
|
||||
input { width: 100%; padding: 0.6rem; border: 1px solid #cbd5e1; border-radius: 8px; margin-bottom: 0.75rem; font-size: 1rem; }
|
||||
input:read-only { background: #f1f5f9; }
|
||||
button { width: 100%; padding: 0.7rem; background: #2563eb; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 1rem; }
|
||||
button:hover { background: #1d4ed8; }
|
||||
button.secondary { background: #e2e8f0; color: #1e293b; }
|
||||
button.secondary:hover { background: #cbd5e1; }
|
||||
.status { margin-top: 0.75rem; font-size: 0.9rem; min-height: 1.2rem; }
|
||||
.success { color: #16a34a; }
|
||||
.error { color: #dc2626; }
|
||||
@@ -34,44 +30,16 @@
|
||||
.instance-link { font-size: 0.875rem; color: #2563eb; text-decoration: none; font-weight: 500; }
|
||||
.instance-link:hover { text-decoration: underline; }
|
||||
.empty { text-align: center; color: #64748b; padding: 1rem 0; }
|
||||
.toolbar { display: flex; gap: 0.5rem; margin-top: 1rem; }
|
||||
.toolbar button { flex: 1; }
|
||||
.note { font-size: 0.8rem; color: #64748b; margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="home-card" class="card">
|
||||
<h1>studioE5 Agent</h1>
|
||||
<div class="card">
|
||||
<h1>EduBox Agent</h1>
|
||||
<div id="main">
|
||||
<p class="info">Connexion en cours...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-card" class="card" style="display:none;">
|
||||
<h2>Paramètres</h2>
|
||||
<form id="settings-form" onsubmit="saveSettings(event)">
|
||||
<label for="cfg-server">Serveur WebSocket</label>
|
||||
<input type="text" id="cfg-server" placeholder="ws://localhost:3001">
|
||||
|
||||
<label for="cfg-node">ID du nœud</label>
|
||||
<input type="text" id="cfg-node" placeholder="MON-PC">
|
||||
|
||||
<label for="cfg-headscale-url">URL Headscale</label>
|
||||
<input type="text" id="cfg-headscale-url" placeholder="https://headscale.exemple.com">
|
||||
|
||||
<label for="cfg-headscale-key">Clé Headscale</label>
|
||||
<input type="password" id="cfg-headscale-key" placeholder="hskey-auth-...">
|
||||
|
||||
<label for="cfg-data-dir">Répertoire de données</label>
|
||||
<input type="text" id="cfg-data-dir" readonly>
|
||||
|
||||
<button type="submit">Enregistrer et redémarrer</button>
|
||||
</form>
|
||||
<div id="settings-status" class="status"></div>
|
||||
<p class="note">Le redémarrage est nécessaire pour prendre en compte les nouveaux paramètres.</p>
|
||||
</div>
|
||||
|
||||
<div id="instances-card" class="card" style="display:none;">
|
||||
<h2>Mes instances</h2>
|
||||
<div id="instances" class="instance-list"></div>
|
||||
@@ -81,8 +49,6 @@
|
||||
<script>
|
||||
const ws = new WebSocket('ws://' + location.host + '/ws');
|
||||
const main = document.getElementById('main');
|
||||
const homeCard = document.getElementById('home-card');
|
||||
const settingsCard = document.getElementById('settings-card');
|
||||
const instancesCard = document.getElementById('instances-card');
|
||||
const instancesContainer = document.getElementById('instances');
|
||||
|
||||
@@ -94,7 +60,6 @@
|
||||
const msg = JSON.parse(ev.data);
|
||||
|
||||
if (msg.action === 'not_activated') {
|
||||
showHome();
|
||||
main.innerHTML = `
|
||||
<p>Entre ton code d'activation (6 caractères) :</p>
|
||||
<input type="text" id="code" maxlength="6" placeholder="XXXXXX" onkeydown="if(event.key==='Enter')activate()">
|
||||
@@ -102,13 +67,9 @@
|
||||
<div id="status" class="status"></div>
|
||||
`;
|
||||
} else if (msg.action === 'activated') {
|
||||
showHome();
|
||||
main.innerHTML = `
|
||||
<p class="success">✅ Activé : <strong>${escapeHtml(msg.studentName || '')}</strong></p>
|
||||
<p class="info">Tes instances apparaissent ci-dessous.</p>
|
||||
<div class="toolbar">
|
||||
<button class="secondary" onclick="showSettings()">⚙️ Paramètres</button>
|
||||
</div>
|
||||
`;
|
||||
instancesCard.style.display = 'block';
|
||||
ws.send(JSON.stringify({action: 'instances'}));
|
||||
@@ -169,65 +130,6 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/config');
|
||||
const cfg = await res.json();
|
||||
document.getElementById('cfg-server').value = cfg.server || '';
|
||||
document.getElementById('cfg-node').value = cfg.node_id || '';
|
||||
document.getElementById('cfg-headscale-url').value = cfg.headscale_url || '';
|
||||
document.getElementById('cfg-headscale-key').value = cfg.headscale_auth_key || '';
|
||||
document.getElementById('cfg-data-dir').value = cfg.data_dir || '';
|
||||
} catch (err) {
|
||||
document.getElementById('settings-status').innerHTML = `<span class="error">Erreur chargement config</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(event) {
|
||||
event.preventDefault();
|
||||
const status = document.getElementById('settings-status');
|
||||
status.innerHTML = 'Enregistrement...';
|
||||
const cfg = {
|
||||
server: document.getElementById('cfg-server').value.trim(),
|
||||
node_id: document.getElementById('cfg-node').value.trim(),
|
||||
headscale_url: document.getElementById('cfg-headscale-url').value.trim(),
|
||||
headscale_auth_key: document.getElementById('cfg-headscale-key').value.trim(),
|
||||
data_dir: document.getElementById('cfg-data-dir').value.trim()
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(cfg)
|
||||
});
|
||||
if (res.ok) {
|
||||
status.innerHTML = '<span class="success">✅ Enregistré. Redémarrage en cours...</span>';
|
||||
await fetch('/api/restart', {method: 'POST'});
|
||||
setTimeout(() => location.reload(), 3000);
|
||||
} else {
|
||||
status.innerHTML = `<span class="error">❌ Erreur ${res.status}</span>`;
|
||||
}
|
||||
} catch (err) {
|
||||
status.innerHTML = `<span class="error">❌ ${escapeHtml(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function showSettings() {
|
||||
homeCard.style.display = 'none';
|
||||
instancesCard.style.display = 'none';
|
||||
settingsCard.style.display = 'block';
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
function showHome() {
|
||||
homeCard.style.display = 'block';
|
||||
settingsCard.style.display = 'none';
|
||||
}
|
||||
|
||||
if (location.hash === '#settings') {
|
||||
showSettings();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
return String(text)
|
||||
|
||||
+45
-110
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -21,8 +22,6 @@ type WSMessage struct {
|
||||
StudentName string `json:"studentName,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
TailscaleIP string `json:"tailscaleIp,omitempty"`
|
||||
HeadscaleURL string `json:"headscaleUrl,omitempty"`
|
||||
HeadscaleAuthKey string `json:"headscaleAuthKey,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -30,27 +29,11 @@ var (
|
||||
mainConnMu sync.Mutex
|
||||
)
|
||||
|
||||
// headscale config received from the server during activation.
|
||||
// These are mutable because activation may happen after the agent starts.
|
||||
var (
|
||||
currentHeadscaleURL string
|
||||
currentHeadscaleAuthKey string
|
||||
headscaleConfigMu sync.Mutex
|
||||
tsProxies = make(map[int]net.Listener)
|
||||
tsProxiesMu sync.Mutex
|
||||
)
|
||||
|
||||
func setHeadscaleConfig(url, authKey string) {
|
||||
headscaleConfigMu.Lock()
|
||||
currentHeadscaleURL = url
|
||||
currentHeadscaleAuthKey = authKey
|
||||
headscaleConfigMu.Unlock()
|
||||
}
|
||||
|
||||
func getHeadscaleConfig() (string, string) {
|
||||
headscaleConfigMu.Lock()
|
||||
defer headscaleConfigMu.Unlock()
|
||||
return currentHeadscaleURL, currentHeadscaleAuthKey
|
||||
}
|
||||
|
||||
func sendMessage(msg WSMessage) error {
|
||||
mainConnMu.Lock()
|
||||
defer mainConnMu.Unlock()
|
||||
@@ -103,9 +86,7 @@ func notifyUI(msg map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func startWebSocket(serverAddr, nodeID, dataDir, headscaleURL, headscaleAuthKey string) {
|
||||
setHeadscaleConfig(headscaleURL, headscaleAuthKey)
|
||||
|
||||
func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
for {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(serverAddr, nil)
|
||||
if err != nil {
|
||||
@@ -136,11 +117,6 @@ func startWebSocket(serverAddr, nodeID, dataDir, headscaleURL, headscaleAuthKey
|
||||
log.Println("Waiting for activation...")
|
||||
} else {
|
||||
log.Printf("Already activated as %s", act.StudentName)
|
||||
// If already activated and we have credentials, ensure VPN is up.
|
||||
hsURL, hsKey := getHeadscaleConfig()
|
||||
if hsURL != "" && hsKey != "" {
|
||||
go startTailscaleAndReport(dataDir, nodeID, hsURL, hsKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Heartbeat goroutine
|
||||
@@ -193,25 +169,6 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("Activated as %s", act.StudentName)
|
||||
}
|
||||
}
|
||||
|
||||
// The server also sends Headscale credentials on activation.
|
||||
if msg.HeadscaleURL != "" && msg.HeadscaleAuthKey != "" {
|
||||
setHeadscaleConfig(msg.HeadscaleURL, msg.HeadscaleAuthKey)
|
||||
cfg, _, err := loadOrCreateConfig(dataDir)
|
||||
if err != nil {
|
||||
log.Printf("loadOrCreateConfig error: %v", err)
|
||||
} else {
|
||||
cfg.HeadscaleURL = msg.HeadscaleURL
|
||||
cfg.HeadscaleAuthKey = msg.HeadscaleAuthKey
|
||||
if err := saveConfig(dataDir, cfg); err != nil {
|
||||
log.Printf("saveConfig error: %v", err)
|
||||
} else {
|
||||
log.Printf("Saved Headscale config received from server")
|
||||
}
|
||||
}
|
||||
go startTailscaleAndReport(dataDir, nodeID, msg.HeadscaleURL, msg.HeadscaleAuthKey)
|
||||
}
|
||||
|
||||
notifyUI(map[string]interface{}{
|
||||
"action": "activated",
|
||||
"studentName": msg.StudentName,
|
||||
@@ -219,35 +176,6 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
case "registered":
|
||||
// Server acknowledged our register message; nothing to do.
|
||||
return
|
||||
case "start_vpn":
|
||||
log.Printf("Server requested VPN start")
|
||||
hsURL, hsKey := getHeadscaleConfig()
|
||||
if hsURL == "" || hsKey == "" {
|
||||
log.Printf("Cannot start VPN: headscale config missing")
|
||||
sendMessage(WSMessage{Action: "vpn_error", NodeID: nodeID, Error: "headscale config missing"})
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ip, err := startTailscale(dataDir, nodeID, hsURL, hsKey)
|
||||
if err != nil {
|
||||
log.Printf("start_vpn error: %v", err)
|
||||
sendMessage(WSMessage{Action: "vpn_error", NodeID: nodeID, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
for {
|
||||
if err := sendMessage(WSMessage{Action: "tailscale_ip", NodeID: nodeID, TailscaleIP: ip}); err != nil {
|
||||
log.Printf("Waiting for WebSocket to send tailscale_ip...")
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("Sent tailscale_ip to server: %s", ip)
|
||||
break
|
||||
}
|
||||
}()
|
||||
case "stop_vpn":
|
||||
log.Printf("Server requested VPN stop")
|
||||
stopTailscale()
|
||||
sendMessage(WSMessage{Action: "vpn_stopped", NodeID: nodeID})
|
||||
case "activation_failed":
|
||||
log.Printf("handleMessage: activation_failed received, error=%s", msg.Error)
|
||||
notifyUI(map[string]interface{}{
|
||||
@@ -264,7 +192,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
}); err != nil {
|
||||
log.Printf("upsertInstance error: %v", err)
|
||||
}
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig, msg.Port); err != nil {
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
||||
log.Printf("writeCompose error: %v", err)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
@@ -277,7 +205,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
return
|
||||
}
|
||||
// Repair older WordPress instances: remove hardcoded WP_HOME/WP_SITEURL
|
||||
// so the studioE5 mu-plugin can compute the public URL from the Host header.
|
||||
// so the EduBox mu-plugin can compute the public URL from the Host header.
|
||||
go func() {
|
||||
// Give the container a moment to be ready before touching wp-config.php
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -285,8 +213,16 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("stripWordPressHardcodedURLs error: %v", err)
|
||||
}
|
||||
}()
|
||||
// Ensure Tailscale is running so the server can reach the node
|
||||
go ensureTailscale(dataDir, nodeID, msg.Port)
|
||||
// Start Tailscale proxy so the server can reach localhost via Tailscale IP
|
||||
tsProxiesMu.Lock()
|
||||
if _, exists := tsProxies[msg.Port]; !exists {
|
||||
if ln, err := startTailscaleProxy(msg.Port); err == nil {
|
||||
tsProxies[msg.Port] = ln
|
||||
} else {
|
||||
log.Printf("startTailscaleProxy error: %v", err)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
@@ -294,6 +230,15 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "stop":
|
||||
log.Printf("Stop instance %s", msg.InstanceID)
|
||||
// Stop Tailscale proxy for this instance port
|
||||
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||
tsProxiesMu.Lock()
|
||||
if ln, exists := tsProxies[inst[msg.InstanceID].Port]; exists {
|
||||
_ = ln.Close()
|
||||
delete(tsProxies, inst[msg.InstanceID].Port)
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
}
|
||||
if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil {
|
||||
log.Printf("dockerComposeDown error: %v", err)
|
||||
}
|
||||
@@ -304,13 +249,21 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "delete":
|
||||
log.Printf("Delete instance %s", msg.InstanceID)
|
||||
tsProxiesMu.Lock()
|
||||
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||
if ln, exists := tsProxies[inst[msg.InstanceID].Port]; exists {
|
||||
_ = ln.Close()
|
||||
delete(tsProxies, inst[msg.InstanceID].Port)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
dockerComposeRm(dataDir, msg.InstanceID)
|
||||
removeInstance(dataDir, msg.InstanceID)
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "reset":
|
||||
log.Printf("Reset instance %s", msg.InstanceID)
|
||||
dockerComposeRm(dataDir, msg.InstanceID)
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig, msg.Port); err != nil {
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
||||
log.Printf("writeCompose error: %v", err)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
@@ -323,7 +276,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
return
|
||||
}
|
||||
// Repair older WordPress instances: remove hardcoded WP_HOME/WP_SITEURL
|
||||
// so the studioE5 mu-plugin can compute the public URL from the Host header.
|
||||
// so the EduBox mu-plugin can compute the public URL from the Host header.
|
||||
go func() {
|
||||
// Give the container a moment to be ready before touching wp-config.php
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -331,8 +284,16 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("stripWordPressHardcodedURLs error: %v", err)
|
||||
}
|
||||
}()
|
||||
// Ensure Tailscale is running so the server can reach the node
|
||||
go ensureTailscale(dataDir, nodeID, msg.Port)
|
||||
// Start Tailscale proxy so the server can reach localhost via Tailscale IP
|
||||
tsProxiesMu.Lock()
|
||||
if _, exists := tsProxies[msg.Port]; !exists {
|
||||
if ln, err := startTailscaleProxy(msg.Port); err == nil {
|
||||
tsProxies[msg.Port] = ln
|
||||
} else {
|
||||
log.Printf("startTailscaleProxy error: %v", err)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
@@ -342,29 +303,3 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("Unknown action: %s", msg.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTailscale(dataDir, nodeID string, port int) {
|
||||
hsURL, hsKey := getHeadscaleConfig()
|
||||
if hsURL == "" || hsKey == "" {
|
||||
log.Printf("Cannot ensure Tailscale: headscale config missing")
|
||||
return
|
||||
}
|
||||
if isTailscaleRunning() {
|
||||
return
|
||||
}
|
||||
log.Printf("Tailscale not running, starting it for instance port %d", port)
|
||||
ip, err := startTailscale(dataDir, nodeID, hsURL, hsKey)
|
||||
if err != nil {
|
||||
log.Printf("ensureTailscale start error: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
if err := sendMessage(WSMessage{Action: "tailscale_ip", NodeID: nodeID, TailscaleIP: ip}); err != nil {
|
||||
log.Printf("Waiting for WebSocket to send tailscale_ip...")
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("Sent tailscale_ip to server: %s", ip)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
+43
-39
@@ -1,18 +1,18 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
container_name: studioe5-postgres
|
||||
container_name: edubox-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: studioe5
|
||||
POSTGRES_USER: edubox
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: studioe5
|
||||
POSTGRES_DB: edubox
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql
|
||||
networks:
|
||||
- studioe5
|
||||
- edubox
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U studioe5 -d studioe5"]
|
||||
test: ["CMD-SHELL", "pg_isready -U edubox -d edubox"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -21,9 +21,13 @@ services:
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
container_name: studioe5-server
|
||||
container_name: edubox-server
|
||||
volumes:
|
||||
- ./server/public:/app/public:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
command: >
|
||||
sh -c "ip route add 100.64.0.0/10 via $$(ip route | awk '/default/ {{print $$3}}') || true && exec node_modules/.bin/next start"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
@@ -31,21 +35,28 @@ services:
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL}
|
||||
SUPERADMIN_EMAIL: ${SUPERADMIN_EMAIL}
|
||||
SUPERADMIN_PASSWORD: ${SUPERADMIN_PASSWORD}
|
||||
MAIN_DOMAIN: ${MAIN_DOMAIN}
|
||||
HEADSCALE_URL: ${HEADSCALE_URL}
|
||||
HEADSCALE_AUTH_KEY: ${HEADSCALE_AUTH_KEY}
|
||||
MAIN_DOMAIN: ${MAIN_DOMAIN}
|
||||
GITEA_URL: ${GITEA_URL}
|
||||
GITEA_TOKEN: ${GITEA_TOKEN}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- studioe5
|
||||
- edubox
|
||||
|
||||
|
||||
resolver:
|
||||
build:
|
||||
context: ./resolver
|
||||
dockerfile: Dockerfile
|
||||
container_name: studioe5-resolver
|
||||
container_name: edubox-resolver
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
command: >
|
||||
sh -c "ip route add 100.64.0.0/10 via \$$(ip route | awk '/default/ {print \$$3}') || true && exec ./resolver"
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
MAIN_DOMAIN: ${MAIN_DOMAIN}
|
||||
@@ -53,34 +64,11 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- studioe5
|
||||
|
||||
resolver-vpn:
|
||||
image: tailscale/tailscale:latest
|
||||
container_name: studioe5-resolver-vpn
|
||||
restart: unless-stopped
|
||||
network_mode: service:resolver
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
environment:
|
||||
TS_AUTHKEY: ${HEADSCALE_AUTH_KEY}
|
||||
TS_LOGIN_SERVER: ${HEADSCALE_URL}
|
||||
TS_EXTRA_ARGS: --login-server=${HEADSCALE_URL}
|
||||
TS_STATE_DIR: /var/lib/tailscale
|
||||
TS_HOSTNAME: studioe5-resolver
|
||||
TS_USERSPACE: "false"
|
||||
TS_ACCEPT_DNS: "false"
|
||||
volumes:
|
||||
- resolver_ts_state:/var/lib/tailscale
|
||||
depends_on:
|
||||
- resolver
|
||||
- edubox
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: studioe5-caddy
|
||||
container_name: edubox-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
@@ -91,11 +79,11 @@ services:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- studioe5
|
||||
- edubox
|
||||
|
||||
headscale:
|
||||
image: headscale/headscale:latest
|
||||
container_name: studioe5-headscale
|
||||
container_name: edubox-headscale
|
||||
restart: unless-stopped
|
||||
command: serve
|
||||
ports:
|
||||
@@ -104,15 +92,31 @@ services:
|
||||
volumes:
|
||||
- ./headscale:/etc/headscale
|
||||
networks:
|
||||
- studioe5
|
||||
- edubox
|
||||
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: edubox-gitea
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=sqlite3
|
||||
- GITEA__database__PATH=/data/gitea/gitea.db
|
||||
volumes:
|
||||
- gitea_data:/data
|
||||
networks:
|
||||
- edubox
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
headscale_data:
|
||||
resolver_ts_state:
|
||||
gitea_data:
|
||||
|
||||
networks:
|
||||
studioe5:
|
||||
edubox:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Headscale configuration for studioE5 client A
|
||||
server_url: https://headscale.studioe5.edudeploy.com
|
||||
# Headscale configuration for EduBox
|
||||
server_url: https://headscale.alfrednobel.edudeploy.com
|
||||
listen_addr: 0.0.0.0:8080
|
||||
metrics_listen_addr: 0.0.0.0:9090
|
||||
grpc_listen_addr: 127.0.0.1:50443
|
||||
@@ -14,7 +14,7 @@ prefixes:
|
||||
|
||||
dns:
|
||||
magic_dns: true
|
||||
base_domain: studioe5.local
|
||||
base_domain: edubox.local
|
||||
nameservers:
|
||||
global:
|
||||
- 1.1.1.1
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
FROM prestashop/prestashop:9
|
||||
|
||||
# Apply EduBox patches so PrestaShop 9 works behind the dynamic-domain reverse proxy.
|
||||
COPY edubox-tools.patch \
|
||||
edubox-link.patch \
|
||||
edubox-frontcontroller.patch \
|
||||
edubox-shop.patch \
|
||||
edubox-shopurl.patch \
|
||||
edubox-shop-getbaseurl.patch \
|
||||
edubox-shopcontext.patch \
|
||||
edubox-asseturl.patch \
|
||||
edubox-install.patch \
|
||||
edubox-install-language.patch \
|
||||
edubox-language.patch \
|
||||
edubox-configuration.patch \
|
||||
edubox-dashboard-warning.patch \
|
||||
edubox-docker-run.patch \
|
||||
/tmp/
|
||||
RUN patch -p1 -d /var/www/html < /tmp/edubox-tools.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-link.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-frontcontroller.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-shop.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-shopurl.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-shop-getbaseurl.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-shopcontext.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-asseturl.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-install.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-install-language.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-language.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-configuration.patch && \
|
||||
patch -p1 -d /var/www/html < /tmp/edubox-dashboard-warning.patch && \
|
||||
patch -p1 -d / < /tmp/edubox-docker-run.patch && \
|
||||
rm /tmp/edubox-*.patch
|
||||
|
||||
# Apache proxy configuration
|
||||
COPY proxy.conf /etc/apache2/conf-enabled/edubox-proxy.conf
|
||||
|
||||
# Pre-download French translation pack so the installer works offline.
|
||||
# Agents may not have outbound internet access during installation.
|
||||
# The official image copies /tmp/data-ps/prestashop/ into /var/www/html on first
|
||||
# boot, so we place the pack there as well.
|
||||
COPY translations-symfony-fr-FR.zip /tmp/data-ps/prestashop/translations/sf-fr-FR.zip
|
||||
RUN chown -R www-data:www-data /tmp/data-ps/prestashop/translations
|
||||
|
||||
# Early bootstrap normalisation for X-Forwarded-* headers
|
||||
COPY defines_custom.inc.php /var/www/html/config/defines_custom.inc.php
|
||||
|
||||
# Clear caches on every start so dynamic domains/ports are picked up
|
||||
COPY edubox-clear-cache-init.sh /tmp/init-scripts/edubox-clear-cache.sh
|
||||
RUN chmod +x /tmp/init-scripts/edubox-clear-cache.sh
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
@@ -1,99 +0,0 @@
|
||||
# EduBox PrestaShop 9 Image
|
||||
|
||||
Image Docker patchée basée sur `prestashop/prestashop:9`, conçue pour fonctionner
|
||||
avec le reverse proxy dynamique d'EduBox.
|
||||
|
||||
## Pourquoi une image patchée ?
|
||||
|
||||
PrestaShop 9 (Apache 2.4 + PHP 8.5) a plusieurs problèmes majeurs derrière EduBox :
|
||||
|
||||
1. Les headers `X-Forwarded-*` sont corrompus par Apache/PHP : `$_SERVER` les
|
||||
reçoit sous forme d'arrays au lieu de strings. On contourne ce bug via
|
||||
`getenv()` dans `config/defines_custom.inc.php`.
|
||||
2. PrestaShop utilise partout le domaine stocké en base (`ps_shop_url`) et la
|
||||
configuration `PS_SSL_ENABLED`. Derrière EduBox, le domaine public change à
|
||||
chaque instance (`<id>.alfrednobel.edudeploy.com`) et toutes les requêtes
|
||||
publiques arrivent en HTTPS. Les patches forcent l'utilisation de l'hôte et
|
||||
du protocole de la requête courante.
|
||||
3. Les agents étudiants peuvent être hors ligne. Le pack de langue français est
|
||||
donc embarqué dans l'image pour éviter tout téléchargement pendant
|
||||
l'installation.
|
||||
|
||||
## Build local
|
||||
|
||||
```bash
|
||||
cd /opt/edubox/prestashop-image
|
||||
docker build -t edubox-prestashop:9 .
|
||||
```
|
||||
|
||||
## Push sur le registry Gitea
|
||||
|
||||
```bash
|
||||
docker tag edubox-prestashop:9 \
|
||||
151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-9
|
||||
docker push \
|
||||
151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-9
|
||||
```
|
||||
|
||||
## Patches appliqués
|
||||
|
||||
| Patch | Fichier modifié | Objectif |
|
||||
|-------|-----------------|----------|
|
||||
| `edubox-tools.patch` | `classes/Tools.php` | `getShopDomain()` / `getShopDomainSsl()` utilisent `getHttpHost()` dynamiquement en conservant les ports non standards (ex. `localhost:8088`) ; `.htaccess` généré sans condition `HTTP_HOST` (images/catégories). |
|
||||
| `edubox-link.patch` | `classes/Link.php` | `getBaseLink()` et `getAdminBaseLink()` utilisent `usingSecureMode()` et `getHttpHost()`. |
|
||||
| `edubox-frontcontroller.patch` | `classes/controller/FrontController.php` | Désactive `sslRedirection()` pour éviter les boucles HTTP/HTTPS. |
|
||||
| `edubox-shop.patch` | `classes/shop/Shop.php` | `Shop::initialize()` utilise le shop par défaut sans redirection forcée. |
|
||||
| `edubox-shopurl.patch` | `classes/shop/ShopUrl.php` | `getMainShopDomain()` / `getMainShopDomainSSL()` retournent le domaine de la requête en conservant les ports non standards. |
|
||||
| `edubox-shop-getbaseurl.patch` | `classes/shop/Shop.php` | `Shop::getBaseURL()` utilise le host/port de la requête courante. |
|
||||
| `edubox-shopcontext.patch` | `src/Core/Context/ShopContext.php` | `getBaseURL()` du BO est reconstruit à partir de la requête courante. |
|
||||
| `edubox-configuration.patch` | `classes/Configuration.php` | `PS_SHOP_DOMAIN`, `PS_SHOP_DOMAIN_SSL`, `PS_SSL_ENABLED`, `_PS_BASE_URL_`, `_PS_BASE_URL_SSL_` sont résolus dynamiquement depuis la requête, pas depuis le cache DB. |
|
||||
| `edubox-asseturl.patch` | `src/Adapter/Assets/AssetUrlGeneratorTrait.php` | Les assets CCC utilisent le protocole de la requête, pas `PS_SSL_ENABLED`. |
|
||||
| `edubox-install.patch` | `src/PrestaShopBundle/Install/Install.php` | `finalize()` respecte `PS_FOLDER_ADMIN` (évite le bug overlayfs `admin` → `admin-edubox`). |
|
||||
| `edubox-install-language.patch` | `src/PrestaShopBundle/Install/Install.php` | Évite le téléchargement du pack legacy `fr.gzip` quand le pack Symfony est embarqué. |
|
||||
| `edubox-language.patch` | `classes/Language.php` | Utilise `_PS_TRANSLATIONS_DIR_` au runtime pour le cache langue ; évite le téléchargement réseau si le pack est présent. |
|
||||
| `edubox-dashboard-warning.patch` | `controllers/admin/AdminDashboardController.php` | Désactive le bandeau d’avertissement "domaine différent de SEO & URL". |
|
||||
| `edubox-docker-run.patch` | `/tmp/docker_run.sh` | Supprime un `install.lock` résiduel si une installation précédente a échoué. |
|
||||
|
||||
## Fichiers injectés
|
||||
|
||||
- `proxy.conf` : Apache truste `X-Forwarded-Proto: https` pour positionner
|
||||
`HTTPS=on` dans l'environnement PHP. Active aussi `AllowOverride All` pour
|
||||
que le `.htaccess` de PrestaShop fonctionne.
|
||||
- `config/defines_custom.inc.php` : normalise `HTTP_X_FORWARDED_HOST`,
|
||||
`HTTP_X_FORWARDED_PROTO` et `HTTP_HOST` corrompus ; définit
|
||||
`PS_TRUSTED_PROXIES` pour Symfony.
|
||||
- `translations-symfony-fr-FR.zip` → copié sous `sf-fr-FR.zip` dans
|
||||
`/var/www/html/translations/` : pack de langue Symfony français embarqué
|
||||
( PrestaShop attend le préfixe `sf-` ).
|
||||
- `edubox-clear-cache-init.sh` → `/tmp/init-scripts/edubox-clear-cache.sh` :
|
||||
vidage des caches Smarty/Symfony et des assets CCC à chaque démarrage du
|
||||
conteneur, afin que les changements de domaine/port soient pris en compte.
|
||||
|
||||
## Utilisation dans EduBox
|
||||
|
||||
Le template PrestaShop 9 dans `server/prisma/seed.ts` utilise cette image :
|
||||
|
||||
```yaml
|
||||
app:
|
||||
image: 151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-8
|
||||
```
|
||||
|
||||
## Mise à jour vers une nouvelle version de PrestaShop
|
||||
|
||||
Si PrestaShop sort une version `9.x.y` :
|
||||
|
||||
1. Modifier le `FROM` du Dockerfile : `FROM prestashop/prestashop:9.x.y`
|
||||
2. Relancer le build. Les patches qui échouent doivent être adaptés aux
|
||||
nouvelles lignes/code de PrestaShop.
|
||||
3. Re-tagger et pousser : `9.x.y-edubox-1`.
|
||||
4. Mettre à jour `server/prisma/seed.ts` avec le nouveau tag.
|
||||
|
||||
## Déploiement sur les agents
|
||||
|
||||
L'image doit être accessible depuis chaque agent étudiant. Deux options :
|
||||
|
||||
1. **Registry privé** (recommandé) : tagger et pousser l'image sur un registry
|
||||
(Docker Hub, registry Gitea, GHCR, etc.) puis mettre à jour
|
||||
`server/prisma/seed.ts` avec le nom complet.
|
||||
2. **Build manuel sur chaque agent** : copier ce dossier sur l'agent et lancer
|
||||
`docker build` avant le premier déploiement.
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* EduBox reverse proxy normalisation for PrestaShop 9 running behind the
|
||||
* EduBox dynamic-public-domain resolver.
|
||||
*
|
||||
* The official PrestaShop 9 + PHP 8.5 + Apache image has a bug where
|
||||
* X-Forwarded-* headers are exposed to PHP as arrays whose value is the
|
||||
* header name. getenv() returns the correct string, so we use it to
|
||||
* reconstruct $_SERVER entries used by Tools::getHttpHost/ShopDomainSSL.
|
||||
*/
|
||||
|
||||
if ($val = getenv('HTTP_X_FORWARDED_HOST')) {
|
||||
$_SERVER['HTTP_X_FORWARDED_HOST'] = $val;
|
||||
}
|
||||
if ($val = getenv('HTTP_X_FORWARDED_PROTO')) {
|
||||
$_SERVER['HTTP_X_FORWARDED_PROTO'] = $val;
|
||||
}
|
||||
|
||||
// Apache/PHP 8.5 sometimes corrupts HTTP_HOST into an array; fall back safely.
|
||||
if (!empty($_SERVER['HTTP_HOST']) && is_array($_SERVER['HTTP_HOST'])) {
|
||||
$_SERVER['HTTP_HOST'] = !empty($_SERVER['SERVER_NAME']) && !is_array($_SERVER['SERVER_NAME'])
|
||||
? $_SERVER['SERVER_NAME']
|
||||
: (getenv('HTTP_X_FORWARDED_HOST') ?: 'localhost');
|
||||
}
|
||||
|
||||
if (!empty($_SERVER['HTTPS']) && is_array($_SERVER['HTTPS'])) {
|
||||
$_SERVER['HTTPS'] = 'off';
|
||||
}
|
||||
|
||||
// Tell Symfony to trust the EduBox resolver so $request->isSecure() and
|
||||
// $request->getHost() honour X-Forwarded-* headers.
|
||||
putenv('PS_TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR');
|
||||
$_SERVER['PS_TRUSTED_PROXIES'] = '127.0.0.1,REMOTE_ADDR';
|
||||
$_ENV['PS_TRUSTED_PROXIES'] = '127.0.0.1,REMOTE_ADDR';
|
||||
@@ -1,20 +0,0 @@
|
||||
--- a/src/Adapter/Assets/AssetUrlGeneratorTrait.php
|
||||
+++ b/src/Adapter/Assets/AssetUrlGeneratorTrait.php
|
||||
@@ -49,12 +49,14 @@ trait AssetUrlGeneratorTrait
|
||||
protected function getFQDN()
|
||||
{
|
||||
if (null === $this->fqdn) {
|
||||
- if ($this->configuration->get('PS_SSL_ENABLED') && ToolsLegacy::usingSecureMode()) {
|
||||
- $this->fqdn = $this->configuration->get('_PS_BASE_URL_SSL_');
|
||||
- } else {
|
||||
+ // EduBox: rely on the current request security, not on PS_SSL_ENABLED.
|
||||
+ // Behind the reverse proxy every public request is HTTPS.
|
||||
+ if (ToolsLegacy::usingSecureMode()) {
|
||||
+ $this->fqdn = $this->configuration->get('_PS_BASE_URL_SSL_') ?: $this->configuration->get('_PS_BASE_URL_');
|
||||
+ } else {
|
||||
$this->fqdn = $this->configuration->get('_PS_BASE_URL_');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fqdn;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
# EduBox: clear PrestaShop caches at every container start so that dynamic
|
||||
# domains/ports (localhost:PORT or reverse-proxy public URL) are picked up.
|
||||
echo "* EduBox: clearing PrestaShop caches for dynamic domain..."
|
||||
rm -rf /var/www/html/var/cache/*
|
||||
rm -rf /var/www/html/app/cache/*
|
||||
rm -rf /var/www/html/cache/smarty/cache/*
|
||||
rm -rf /var/www/html/cache/smarty/compile/*
|
||||
rm -rf /var/www/html/themes/*/assets/cache/*
|
||||
rm -rf /var/www/html/img/tmp/*
|
||||
@@ -1,36 +0,0 @@
|
||||
--- a/classes/Configuration.php 2026-06-04 14:48:44.000000000 +0000
|
||||
+++ b/classes/Configuration.php 2026-06-23 16:27:03.944472677 +0000
|
||||
@@ -210,6 +210,33 @@
|
||||
Configuration::loadConfiguration();
|
||||
}
|
||||
|
||||
+ // EduBox: dynamic public domains and ports (local access + reverse proxy).
|
||||
+ // These keys must be resolved from the current request, not from the DB cache.
|
||||
+ if ($key === 'PS_SHOP_DOMAIN' || $key === 'PS_SHOP_DOMAIN_SSL') {
|
||||
+ $host = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($host, -3) === ':80' || substr($host, -4) === ':443') {
|
||||
+ $host = substr($host, 0, strrpos($host, ':'));
|
||||
+ }
|
||||
+ return $host;
|
||||
+ }
|
||||
+ if ($key === 'PS_SSL_ENABLED' || $key === 'PS_SSL_ENABLED_EVERYWHERE') {
|
||||
+ return Tools::usingSecureMode() ? '1' : '0';
|
||||
+ }
|
||||
+ if ($key === '_PS_BASE_URL_') {
|
||||
+ $host = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($host, -3) === ':80' || substr($host, -4) === ':443') {
|
||||
+ $host = substr($host, 0, strrpos($host, ':'));
|
||||
+ }
|
||||
+ return 'http://' . $host;
|
||||
+ }
|
||||
+ if ($key === '_PS_BASE_URL_SSL_') {
|
||||
+ $host = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($host, -3) === ':80' || substr($host, -4) === ':443') {
|
||||
+ $host = substr($host, 0, strrpos($host, ':'));
|
||||
+ }
|
||||
+ return 'https://' . $host;
|
||||
+ }
|
||||
+
|
||||
$idLang = self::isLangKey($key) ? (int) $idLang : 0;
|
||||
|
||||
if (self::$_new_cache_shop === null) {
|
||||
@@ -1,49 +0,0 @@
|
||||
--- a/controllers/admin/AdminDashboardController.php
|
||||
+++ b/controllers/admin/AdminDashboardController.php
|
||||
@@ -330,43 +330,9 @@
|
||||
|
||||
protected function getWarningDomainName()
|
||||
{
|
||||
- $warning = false;
|
||||
- if (Shop::isFeatureActive()) {
|
||||
- return;
|
||||
- }
|
||||
-
|
||||
- $shop = Context::getContext()->shop;
|
||||
- if ($_SERVER['HTTP_HOST'] != $shop->domain && $_SERVER['HTTP_HOST'] != $shop->domain_ssl && Tools::getValue('ajax') == false) {
|
||||
- $warning = $this->trans('You are currently connected under the following domain name:', [], 'Admin.Dashboard.Notification') . ' <span style="color: #CC0000;">' . $_SERVER['HTTP_HOST'] . '</span><br />';
|
||||
- if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE')) {
|
||||
- $warning .= $this->trans(
|
||||
- 'This is different from the shop domain name set in the Multistore settings: "%s".',
|
||||
- [
|
||||
- '%s' => $shop->domain,
|
||||
- ],
|
||||
- 'Admin.Dashboard.Notification'
|
||||
- ) . $this->trans(
|
||||
- 'If this is your main domain, please {link}change it now{/link}.',
|
||||
- [
|
||||
- '{link}' => '<a href="' . $this->context->link->getAdminLink('AdminShopUrl', true, [], ['id_shop_url' => (int) $shop->id, 'updateshop_url' => 1]) . '">',
|
||||
- '{/link}' => '</a>',
|
||||
- ],
|
||||
- 'Admin.Dashboard.Notification'
|
||||
- );
|
||||
- } else {
|
||||
- $warning .= $this->trans('This is different from the domain name set in the "SEO & URLs" tab.', [], 'Admin.Dashboard.Notification') . '
|
||||
- ' . $this->trans(
|
||||
- 'If this is your main domain, please {link}change it now{/link}.',
|
||||
- [
|
||||
- '{link}' => '<a href="' . $this->context->link->getAdminLink('AdminMeta') . '#meta_fieldset_shop_url">',
|
||||
- '{/link}' => '</a>',
|
||||
- ],
|
||||
- 'Admin.Dashboard.Notification'
|
||||
- );
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- return $warning;
|
||||
+ // EduBox: instances use dynamic public domains behind a reverse proxy.
|
||||
+ // The domain stored during installation never matches the request host.
|
||||
+ return false;
|
||||
}
|
||||
|
||||
public function ajaxProcessRefreshDashboard()
|
||||
@@ -1,16 +0,0 @@
|
||||
--- a/tmp/docker_run.sh 2026-06-20 17:57:12.682339048 +0000
|
||||
+++ b/tmp/docker_run.sh 2026-06-20 17:57:12.852338398 +0000
|
||||
@@ -21,6 +21,13 @@
|
||||
|
||||
# From now, stop at error
|
||||
set -e
|
||||
+# EduBox: if a previous installation failed, install.lock remains but PrestaShop is not configured.
|
||||
+# Remove the stale lock so the installer can run again on the next start.
|
||||
+if [ -f ./install.lock ] && [ ! -f ./config/settings.inc.php ] && [ ! -f ./app/config/parameters.php ]; then
|
||||
+ echo "\n* Stale install.lock detected, removing it to allow reinstallation ..."
|
||||
+ rm -f ./install.lock
|
||||
+fi
|
||||
+
|
||||
|
||||
if [ ! -f ./config/settings.inc.php ] && [ ! -f ./app/config/parameters.php ] && [ ! -f ./install.lock ]; then
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
--- a/classes/controller/FrontController.php
|
||||
+++ b/classes/controller/FrontController.php
|
||||
@@ -849,18 +849,9 @@
|
||||
*/
|
||||
protected function sslRedirection()
|
||||
{
|
||||
- // If we call a SSL controller without SSL or a non SSL controller with SSL, we redirect with the right protocol
|
||||
- if (Configuration::get('PS_SSL_ENABLED') && $_SERVER['REQUEST_METHOD'] != 'POST' && $this->ssl != Tools::usingSecureMode()) {
|
||||
- $this->context->cookie->disallowWriting();
|
||||
- header('HTTP/1.1 301 Moved Permanently');
|
||||
- header('Cache-Control: no-cache');
|
||||
- if ($this->ssl) {
|
||||
- header('Location: ' . Tools::getShopDomainSsl(true) . $_SERVER['REQUEST_URI']);
|
||||
- } else {
|
||||
- header('Location: ' . Tools::getShopDomain(true) . $_SERVER['REQUEST_URI']);
|
||||
- }
|
||||
- exit;
|
||||
- }
|
||||
+ // EduBox: disabled. Behind the EduBox reverse proxy every request is
|
||||
+ // served over HTTPS publicly, so PrestaShop must never redirect to HTTP.
|
||||
+ return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,30 +0,0 @@
|
||||
--- a/src/PrestaShopBundle/Install/Install.php 2026-06-20 18:07:13.506985399 +0000
|
||||
+++ b/src/PrestaShopBundle/Install/Install.php 2026-06-20 18:07:22.294363061 +0000
|
||||
@@ -622,17 +622,20 @@
|
||||
'locale' => (string) $xml->locale,
|
||||
];
|
||||
|
||||
- if (file_exists(_PS_TRANSLATIONS_DIR_ . (string) $iso . '.gzip') == false) {
|
||||
- $language = EntityLanguage::downloadLanguagePack($iso, _PS_INSTALL_VERSION_);
|
||||
+ // EduBox: skip legacy language pack download if Symfony pack is bundled
|
||||
+ $errors = [];
|
||||
+ $locale = $params_lang['locale'];
|
||||
+
|
||||
+ if (!EntityLanguage::translationPackIsInCache($locale)) {
|
||||
+ if (file_exists(_PS_TRANSLATIONS_DIR_ . (string) $iso . '.gzip') == false) {
|
||||
+ $language = EntityLanguage::downloadLanguagePack($iso, _PS_INSTALL_VERSION_);
|
||||
|
||||
- if ($language == false) {
|
||||
- throw new PrestashopInstallerException($this->translator->trans('Cannot download language pack "%iso%"', ['%iso%' => $iso], 'Install'));
|
||||
+ if ($language == false) {
|
||||
+ throw new PrestashopInstallerException($this->translator->trans('Cannot download language pack "%iso%"', ['%iso%' => $iso], 'Install'));
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
- $errors = [];
|
||||
- $locale = $params_lang['locale'];
|
||||
-
|
||||
/* @todo check if a newer pack is available */
|
||||
if (!EntityLanguage::translationPackIsInCache($locale)) {
|
||||
EntityLanguage::downloadXLFLanguagePack($locale, $errors);
|
||||
@@ -1,9 +0,0 @@
|
||||
--- a/src/PrestaShopBundle/Install/Install.php
|
||||
+++ b/src/PrestaShopBundle/Install/Install.php
|
||||
@@ -1202,7 +1202,7 @@ class Install extends AbstractInstall
|
||||
{
|
||||
- $adminFolder = 'admin-dev';
|
||||
+ $adminFolder = getenv('PS_FOLDER_ADMIN') ?: 'admin-dev';
|
||||
|
||||
// If we need, we generate a random name for admin folder (for security purpose!)
|
||||
if (file_exists(_PS_ROOT_DIR_ . '/admin/')) {
|
||||
@@ -1,36 +0,0 @@
|
||||
--- a/classes/Language.php
|
||||
+++ b/classes/Language.php
|
||||
@@ -1235,6 +1235,12 @@
|
||||
*/
|
||||
public static function downloadXLFLanguagePack($locale, &$errors = [], $type = self::PACK_TYPE_SYMFONY)
|
||||
{
|
||||
+ // EduBox: if the translation pack is already present in the image,
|
||||
+ // do not try to download it (agents may be offline).
|
||||
+ if (static::translationPackIsInCache($locale, $type)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
$file = self::getPathToCachedTranslationPack($locale, $type);
|
||||
$url = (self::PACK_TYPE_EMAILS === $type) ? self::EMAILS_LANGUAGE_PACK_URL : self::SF_LANGUAGE_PACK_URL;
|
||||
$url = str_replace(
|
||||
@@ -1697,7 +1703,9 @@
|
||||
*/
|
||||
public static function translationPackIsInCache(string $locale, string $type = self::PACK_TYPE_SYMFONY): bool
|
||||
{
|
||||
- return file_exists(self::getPathToCachedTranslationPack($locale, $type));
|
||||
+ // EduBox: use runtime constant instead of class constant, because
|
||||
+ // _PS_TRANSLATIONS_DIR_ may not be defined when this file is compiled.
|
||||
+ return file_exists(_PS_TRANSLATIONS_DIR_ . $type . '-' . $locale . '.zip');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1710,7 +1718,8 @@
|
||||
*/
|
||||
private static function getPathToCachedTranslationPack(string $locale, string $type = self::PACK_TYPE_SYMFONY): string
|
||||
{
|
||||
- return self::TRANSLATION_PACK_CACHE_DIR . $type . '-' . $locale . '.zip';
|
||||
+ // EduBox: use runtime constant instead of class constant.
|
||||
+ return _PS_TRANSLATIONS_DIR_ . $type . '-' . $locale . '.zip';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,46 +0,0 @@
|
||||
--- a/classes/Link.php 2026-06-20 20:05:45.983104609 +0000
|
||||
+++ b/classes/Link.php 2026-06-20 20:05:46.195748630 +0000
|
||||
@@ -862,7 +862,7 @@
|
||||
public function getAdminBaseLink($idShop = null, $ssl = null, $relativeProtocol = false)
|
||||
{
|
||||
if (null === $ssl) {
|
||||
- $ssl = Configuration::get('PS_SSL_ENABLED');
|
||||
+ $ssl = Tools::usingSecureMode();
|
||||
}
|
||||
|
||||
if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE')) {
|
||||
@@ -881,9 +881,10 @@
|
||||
}
|
||||
|
||||
if ($relativeProtocol) {
|
||||
- $base = '//' . ($ssl && $this->ssl_enable ? $shop->domain_ssl : $shop->domain);
|
||||
+ $base = '//' . Tools::getHttpHost(false, false, false);
|
||||
} else {
|
||||
- $base = (($ssl && $this->ssl_enable) ? 'https://' . $shop->domain_ssl : 'http://' . $shop->domain);
|
||||
+ $protocol = Tools::usingSecureMode() ? 'https://' : 'http://';
|
||||
+ $base = $protocol . Tools::getHttpHost(false, false, false);
|
||||
}
|
||||
|
||||
return $base . $shop->getBaseURI();
|
||||
@@ -1391,7 +1392,7 @@
|
||||
public function getBaseLink($idShop = null, $ssl = null, $relativeProtocol = false)
|
||||
{
|
||||
if (null === $ssl) {
|
||||
- $ssl = Configuration::get('PS_SSL_ENABLED');
|
||||
+ $ssl = Tools::usingSecureMode();
|
||||
}
|
||||
|
||||
if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && $idShop !== null) {
|
||||
@@ -1401,9 +1402,10 @@
|
||||
}
|
||||
|
||||
if ($relativeProtocol) {
|
||||
- $base = '//' . ($ssl && $this->ssl_enable ? $shop->domain_ssl : $shop->domain);
|
||||
+ $base = '//' . Tools::getHttpHost(false, false, false);
|
||||
} else {
|
||||
- $base = (($ssl && $this->ssl_enable) ? 'https://' . $shop->domain_ssl : 'http://' . $shop->domain);
|
||||
+ $protocol = Tools::usingSecureMode() ? 'https://' : 'http://';
|
||||
+ $base = $protocol . Tools::getHttpHost(false, false, false);
|
||||
}
|
||||
|
||||
return $base . $shop->getBaseURI();
|
||||
@@ -1,29 +0,0 @@
|
||||
--- a/classes/shop/Shop.php
|
||||
+++ b/classes/shop/Shop.php
|
||||
@@ -489,15 +489,16 @@ class ShopCore extends ObjectModel
|
||||
*/
|
||||
public function getBaseURL($auto_secure_mode = true, $add_base_uri = true)
|
||||
{
|
||||
- if ($auto_secure_mode && Tools::usingSecureMode()) {
|
||||
- if (!$this->domain_ssl) {
|
||||
- return false;
|
||||
- }
|
||||
- $url = 'https://' . $this->domain_ssl;
|
||||
+ // EduBox: use the current request host so local access on non-standard
|
||||
+ // ports (e.g. localhost:8088) and reverse-proxy domains both work.
|
||||
+ $host = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($host, -3) === ':80' || substr($host, -4) === ':443') {
|
||||
+ $host = substr($host, 0, strrpos($host, ':'));
|
||||
+ }
|
||||
+
|
||||
+ if ($auto_secure_mode && Tools::usingSecureMode()) {
|
||||
+ $url = 'https://' . $host;
|
||||
} else {
|
||||
- if (!$this->domain) {
|
||||
- return false;
|
||||
- }
|
||||
- $url = 'http://' . $this->domain;
|
||||
+ $url = 'http://' . $host;
|
||||
}
|
||||
|
||||
if ($add_base_uri) {
|
||||
@@ -1,46 +0,0 @@
|
||||
--- a/classes/shop/Shop.php
|
||||
+++ b/classes/shop/Shop.php
|
||||
@@ -411,38 +411,14 @@
|
||||
} else {
|
||||
$shop = new Shop($id_shop);
|
||||
if (!Validate::isLoadedObject($shop) || !$shop->active) {
|
||||
- // No shop found ... too bad, let's redirect to default shop
|
||||
- $default_shop = new Shop((int) Configuration::get('PS_SHOP_DEFAULT'));
|
||||
+ // EduBox: behind a reverse proxy with dynamic public domains,
|
||||
+ // the requested host never matches ps_shop_url. Always use the
|
||||
+ // default shop instead of redirecting to a fixed canonical URL.
|
||||
+ $shop = new Shop((int) Configuration::get('PS_SHOP_DEFAULT'));
|
||||
|
||||
- // Hmm there is something really bad in your Prestashop !
|
||||
- if (!Validate::isLoadedObject($default_shop)) {
|
||||
+ if (!Validate::isLoadedObject($shop)) {
|
||||
throw new PrestaShopException('Shop not found');
|
||||
}
|
||||
-
|
||||
- $params = $_GET;
|
||||
- unset($params['id_shop']);
|
||||
- $url = $default_shop->domain;
|
||||
- if (!Configuration::get('PS_REWRITING_SETTINGS')) {
|
||||
- $url .= $default_shop->getBaseURI() . 'index.php?' . http_build_query($params);
|
||||
- } else {
|
||||
- // Catch url with subdomain "www"
|
||||
- if (strpos($url, 'www.') === 0 && 'www.' . $_SERVER['HTTP_HOST'] === $url || $_SERVER['HTTP_HOST'] === 'www.' . $url) {
|
||||
- $url .= $_SERVER['REQUEST_URI'];
|
||||
- } else {
|
||||
- $url .= $default_shop->getBaseURI();
|
||||
- }
|
||||
-
|
||||
- if (count($params)) {
|
||||
- $url .= '?' . http_build_query($params);
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- $redirect_type = Configuration::get('PS_CANONICAL_REDIRECT');
|
||||
- $redirect_code = ($redirect_type == 1 ? '302' : '301');
|
||||
- $redirect_header = ($redirect_type == 1 ? 'Found' : 'Moved Permanently');
|
||||
- header('HTTP/1.0 ' . $redirect_code . ' ' . $redirect_header);
|
||||
- header('Location: ' . Tools::getShopProtocol() . $url);
|
||||
- exit;
|
||||
} elseif (defined('_PS_ADMIN_DIR_') && empty($shop->physical_uri)) {
|
||||
$shop_default = new Shop((int) Configuration::get('PS_SHOP_DEFAULT'));
|
||||
$shop->physical_uri = $shop_default->physical_uri;
|
||||
@@ -1,28 +0,0 @@
|
||||
--- a/src/Core/Context/ShopContext.php
|
||||
+++ b/src/Core/Context/ShopContext.php
|
||||
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace PrestaShop\PrestaShop\Core\Context;
|
||||
|
||||
+use Tools;
|
||||
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
|
||||
|
||||
/**
|
||||
@@ -121,11 +122,12 @@ class ShopContext
|
||||
|
||||
public function getBaseURL(): string
|
||||
{
|
||||
- if ($this->secured) {
|
||||
- $url = 'https://' . $this->domainSSL;
|
||||
- } else {
|
||||
- $url = 'http://' . $this->domain;
|
||||
- }
|
||||
+ // EduBox: behind a reverse proxy with dynamic public domains the shop
|
||||
+ // URL stored in the database is never the real public URL. Rebuild the
|
||||
+ // base URL from the current request instead.
|
||||
+ $secure = Tools::usingSecureMode();
|
||||
+ $domain = $secure ? Tools::getShopDomainSsl(false, false) : Tools::getShopDomain(false, false);
|
||||
+ $url = ($secure ? 'https://' : 'http://') . $domain;
|
||||
|
||||
return $url . $this->getBaseURI();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
--- a/classes/shop/ShopUrl.php
|
||||
+++ b/classes/shop/ShopUrl.php
|
||||
@@ -175,15 +175,23 @@
|
||||
|
||||
public static function getMainShopDomain($id_shop = null)
|
||||
{
|
||||
- ShopUrl::cacheMainDomainForShop($id_shop);
|
||||
-
|
||||
- return self::$main_domain[(int) $id_shop] ?? null;
|
||||
+ // EduBox: dynamic public domain behind reverse proxy or direct local access.
|
||||
+ // Always use the request host instead of the domain stored in database.
|
||||
+ // Keep non-standard ports (e.g. localhost:8088) so local access works.
|
||||
+ $host = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($host, -3) === ':80' || substr($host, -4) === ':443') {
|
||||
+ $host = substr($host, 0, strrpos($host, ':'));
|
||||
+ }
|
||||
+ return $host;
|
||||
}
|
||||
|
||||
public static function getMainShopDomainSSL($id_shop = null)
|
||||
{
|
||||
- ShopUrl::cacheMainDomainForShop($id_shop);
|
||||
-
|
||||
- return self::$main_domain_ssl[(int) $id_shop] ?? null;
|
||||
+ // EduBox: dynamic public domain behind reverse proxy or direct local access.
|
||||
+ $host = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($host, -3) === ':80' || substr($host, -4) === ':443') {
|
||||
+ $host = substr($host, 0, strrpos($host, ':'));
|
||||
+ }
|
||||
+ return $host;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
--- a/classes/Tools.php 2026-06-04 14:48:44.000000000 +0000
|
||||
+++ b/classes/Tools.php 2026-06-23 16:34:13.226899992 +0000
|
||||
@@ -269,8 +269,10 @@
|
||||
*/
|
||||
public static function getShopDomain($http = false, $entities = false)
|
||||
{
|
||||
- if (!$domain = ShopUrl::getMainShopDomain()) {
|
||||
- $domain = Tools::getHttpHost();
|
||||
+ // EduBox: dynamic domain + keep non-standard ports (e.g. localhost:8088).
|
||||
+ $domain = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($domain, -3) === ':80' || substr($domain, -4) === ':443') {
|
||||
+ $domain = substr($domain, 0, strrpos($domain, ':'));
|
||||
}
|
||||
if ($entities) {
|
||||
$domain = htmlspecialchars($domain, ENT_COMPAT, 'UTF-8');
|
||||
@@ -292,14 +294,16 @@
|
||||
*/
|
||||
public static function getShopDomainSsl($http = false, $entities = false)
|
||||
{
|
||||
- if (!$domain = ShopUrl::getMainShopDomainSSL()) {
|
||||
- $domain = Tools::getHttpHost();
|
||||
+ // EduBox: dynamic domain + keep non-standard ports.
|
||||
+ $domain = Tools::getHttpHost(false, false, false);
|
||||
+ if (substr($domain, -3) === ':80' || substr($domain, -4) === ':443') {
|
||||
+ $domain = substr($domain, 0, strrpos($domain, ':'));
|
||||
}
|
||||
if ($entities) {
|
||||
$domain = htmlspecialchars($domain, ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
if ($http) {
|
||||
- $domain = static::getProtocol((bool) Configuration::get('PS_SSL_ENABLED')) . $domain;
|
||||
+ $domain = static::getProtocol(Tools::usingSecureMode()) . $domain;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
@@ -2246,7 +2250,7 @@
|
||||
$rewrite_settings = (int) Configuration::get('PS_REWRITING_SETTINGS', null, null, (int) $uri['id_shop']);
|
||||
}
|
||||
|
||||
- $domain_rewrite_cond = 'RewriteCond %{HTTP_HOST} ^' . $domain . '$' . PHP_EOL;
|
||||
+ $domain_rewrite_cond = ''; // EduBox: removed HTTP_HOST condition for dynamic domains
|
||||
// Rewrite virtual multishop uri
|
||||
if ($uri['virtual']) {
|
||||
if (!$rewrite_settings) {
|
||||
@@ -1,10 +0,0 @@
|
||||
# EduBox reverse proxy handling
|
||||
# Apache sees HTTP requests from the EduBox resolver. The public request is HTTPS.
|
||||
SetEnvIf X-Forwarded-Proto ^https$ HTTPS=on
|
||||
SetEnvIf X-Forwarded-Proto ^https$ SERVER_PORT=443
|
||||
|
||||
# Enable .htaccess overrides for PrestaShop URL rewriting (images, products, etc.)
|
||||
<Directory /var/www/html>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
Binary file not shown.
@@ -6,7 +6,7 @@ export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-50">
|
||||
<div className="w-full max-w-md p-8 space-y-6 bg-white rounded-lg shadow-md">
|
||||
<h1 className="text-2xl font-bold text-center text-gray-900">studioE5</h1>
|
||||
<h1 className="text-2xl font-bold text-center text-gray-900">EduBox V2</h1>
|
||||
<p className="text-center text-muted-foreground">Connexion à la plateforme</p>
|
||||
<LoginForm />
|
||||
</div>
|
||||
|
||||
@@ -11,10 +11,7 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ ok: false }, { status: 400 });
|
||||
}
|
||||
|
||||
if (
|
||||
domain === MAIN_DOMAIN ||
|
||||
domain === `headscale.${MAIN_DOMAIN}`
|
||||
) {
|
||||
if (domain === MAIN_DOMAIN || domain === `headscale.${MAIN_DOMAIN}`) {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const AGENT_VERSION = "0.3.0";
|
||||
const AGENT_BIN_NAME = "studioE5-agent";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
version: AGENT_VERSION,
|
||||
windows: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}.exe`,
|
||||
linux: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}`,
|
||||
mac: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}-mac`,
|
||||
windows: `/edubox-agent-v${AGENT_VERSION}.exe`,
|
||||
linux: `/edubox-agent-v${AGENT_VERSION}`,
|
||||
mac: `/edubox-agent-v${AGENT_VERSION}-mac`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { sendToNode } from "@/lib/websocket";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const { nodeId, message } = body;
|
||||
if (!nodeId || !message) {
|
||||
return NextResponse.json({ error: "Missing nodeId or message" }, { status: 400 });
|
||||
}
|
||||
const sent = sendToNode(nodeId, message);
|
||||
return NextResponse.json({ sent });
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export default function DashboardNav({ role }: { role: string }) {
|
||||
return (
|
||||
<nav className="w-64 bg-white border-r flex flex-col">
|
||||
<div className="p-6 border-b">
|
||||
<h2 className="text-xl font-bold text-primary">studioE5</h2>
|
||||
<h2 className="text-xl font-bold text-primary">EduBox</h2>
|
||||
</div>
|
||||
<div className="flex-1 p-4 space-y-1">
|
||||
{links.map((link) => (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
|
||||
const AGENT_VERSION = "0.3.0";
|
||||
const AGENT_BIN_NAME = "studioE5-agent";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -16,8 +15,8 @@ export default function DownloadPage() {
|
||||
<CardTitle>Windows</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">Agent studioE5 pour Windows (64 bits)</p>
|
||||
<a href={`/${AGENT_BIN_NAME}-v${AGENT_VERSION}.exe`} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (.exe)</a>
|
||||
<p className="text-sm text-muted-foreground mb-4">Agent EduBox pour Windows (64 bits)</p>
|
||||
<a href={`/edubox-agent-v${AGENT_VERSION}.exe`} download className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">Télécharger (.exe)</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import "./globals.css";
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "studioE5",
|
||||
title: "EduBox V2",
|
||||
description: "Plateforme de gestion d'instances pour l'enseignement BTS",
|
||||
};
|
||||
|
||||
|
||||
@@ -54,13 +54,7 @@ export function initWebSocketServer(wss: WebSocketServer) {
|
||||
create: { id: nodeId, studentId: student.id, status: "online", lastSeen: new Date() },
|
||||
});
|
||||
console.log("[WS] Activated:", student.firstName, student.lastName, "on", nodeId);
|
||||
ws.send(JSON.stringify({
|
||||
action: "activated",
|
||||
studentId: student.id,
|
||||
studentName: `${student.firstName} ${student.lastName}`,
|
||||
headscaleUrl: process.env.HEADSCALE_URL,
|
||||
headscaleAuthKey: process.env.HEADSCALE_AUTH_KEY,
|
||||
}));
|
||||
ws.send(JSON.stringify({ action: "activated", studentId: student.id, studentName: `${student.firstName} ${student.lastName}` }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+5
-35
@@ -19,6 +19,9 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
// Remove obsolete PrestaShop templates from previous seeds
|
||||
await prisma.template.deleteMany({ where: { type: "prestashop" } });
|
||||
|
||||
const templates = [
|
||||
{
|
||||
name: "WordPress latest vierge",
|
||||
@@ -50,53 +53,20 @@ async function main() {
|
||||
dbPassword: "wordpress",
|
||||
dbRootPassword: "rootpassword",
|
||||
},
|
||||
{
|
||||
name: "PrestaShop 9 vierge (edubox)",
|
||||
type: "prestashop",
|
||||
dockerImage: "151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-9",
|
||||
dbImage: "mariadb:10.11",
|
||||
dbName: "prestashop",
|
||||
dbUser: "prestashop",
|
||||
dbPassword: "prestashop",
|
||||
dbRootPassword: "rootpassword",
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of templates) {
|
||||
const dbHost = "db";
|
||||
const dbPort = "3306";
|
||||
const isPrestaShop = t.type === "prestashop";
|
||||
|
||||
const appEnv = isPrestaShop
|
||||
? ` DB_SERVER: ${dbHost}
|
||||
DB_PORT: ${dbPort}
|
||||
DB_NAME: ${t.dbName}
|
||||
DB_USER: ${t.dbUser}
|
||||
DB_PASSWD: ${t.dbPassword}
|
||||
DB_PREFIX: ps_
|
||||
PS_DOMAIN: {PUBLIC_DOMAIN}
|
||||
PS_SHOP_NAME: ${t.name}
|
||||
PS_INSTALL_AUTO: "1"
|
||||
PS_INSTALL_DB: "0"
|
||||
PS_ENABLE_SSL: "0"
|
||||
PS_LANGUAGE: fr
|
||||
PS_COUNTRY: fr
|
||||
ADMIN_MAIL: admin@edubox.local
|
||||
ADMIN_PASSWD: EduboxPrestashop2024!
|
||||
PS_FOLDER_ADMIN: admin-edubox
|
||||
PS_FOLDER_INSTALL: install
|
||||
PS_DEV_MODE: "1"`
|
||||
: ` WORDPRESS_DB_HOST: ${dbHost}:${dbPort}
|
||||
const appEnv = ` WORDPRESS_DB_HOST: ${dbHost}:${dbPort}
|
||||
WORDPRESS_DB_NAME: ${t.dbName}
|
||||
WORDPRESS_DB_USER: ${t.dbUser}
|
||||
WORDPRESS_DB_PASSWORD: ${t.dbPassword}
|
||||
WORDPRESS_DB_PREFIX: wp_
|
||||
# No hardcoded WP_HOME/WP_SITEURL so WordPress auto-detects from the Host header`;
|
||||
|
||||
const appVolumes = isPrestaShop
|
||||
? ` volumes:
|
||||
- app_data:/var/www/html`
|
||||
: ` volumes:
|
||||
const appVolumes = ` volumes:
|
||||
- app_data:/var/www/html
|
||||
- {MU_PLUGINS_DIR}/edubox-public-url.php:/var/www/html/wp-content/mu-plugins/edubox-public-url.php:ro`;
|
||||
|
||||
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user