feat(vpn): VPN on-demand Tailscale + agent studioE5 standalone
- Agent studioE5 standalone en Go (console + systray) - VPN on-demand via tailscaled + tailscale up (authkey Headscale) - Resolver/serveur dans le tailnet studioe5 - Caddy on-demand TLS pour les instances - Nouveaux endpoints serveur /api/internal/send-to-node - Suppression des anciens binaires edubox-agent - Suivi dans SUIVI_VPN_ONDEMAND.md
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
node_modules/
|
||||
.next/
|
||||
*.log
|
||||
studioE5-data/
|
||||
edubox-data/
|
||||
dist/
|
||||
coverage/
|
||||
@@ -9,9 +10,16 @@ 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
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
# Analyse : Intégration de PrestaShop dans EduBox
|
||||
|
||||
## 1. Architecture actuelle d'EduBox (vue d'ensemble)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Serveur cloud (ex: alfrednobel.edudeploy.com) │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
|
||||
│ │ Caddy │──▶│ Next.js │──▶│ Resolver Go │──▶│ PostgreSQL │ │
|
||||
│ │ TLS on-demand│ │ (dashboard) │ │ (proxy inst.)│ │ (état) │ │
|
||||
│ └─────────────┘ └──────────────┘ └──────────────┘ └─────────────┘ │
|
||||
│ │ │ WebSocket 3001 │
|
||||
│ │ ▼ │
|
||||
│ │ Agent EduBox (Go) sur PC étudiant via Tailscale/Headscale │
|
||||
│ │ ┌─────────────┐ │
|
||||
│ └─────────────▶│ WordPress │ (mu-plugin edubox-public-url.php) │
|
||||
│ │ Docker/Podman │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Flux d'une requête publique
|
||||
|
||||
1. Le navigateur demande `https://<instance-id>.alfrednobel.edudeploy.com/`
|
||||
2. Caddy termine TLS et transmet au resolver Go (`:443 → resolver:2020`)
|
||||
3. Le resolver lit PostgreSQL pour trouver le nœud (Tailscale IP) et le port de l'instance
|
||||
4. Le resolver fait du reverse proxy HTTP vers `http://<tailscale-ip>:<port>`
|
||||
5. Le resolver ajoute les headers `X-Forwarded-Proto: https`, `X-Forwarded-Host`, `X-Forwarded-Port: 443`
|
||||
6. L'agent Go sur le PC étudiant a lancé le conteneur WordPress sur `localhost:<port>` (bind `{PORT}:80`)
|
||||
7. WordPress reçoit la requête en HTTP interne mais le **mu-plugin** `edubox-public-url.php` détecte `HTTP_HOST`/`X-Forwarded-Proto` et redéfinit `WP_HOME`/`WP_SITEURL` à la volée.
|
||||
|
||||
### Pourquoi WordPress fonctionne
|
||||
|
||||
- WordPress permet de redéfinir `WP_HOME` et `WP_SITEURL` via `wp-config.php` ou un mu-plugin.
|
||||
- Le mu-plugin intercepte chaque requête et calcule l'URL publique depuis les headers proxy.
|
||||
- WordPress accepte d'être servi depuis plusieurs domaines simultanément (localhost + sous-domaine public).
|
||||
|
||||
---
|
||||
|
||||
## 2. Pourquoi PrestaShop est différent (et plus difficile)
|
||||
|
||||
### 2.1 Le domaine est stocké en base de données
|
||||
|
||||
PrestaShop enregistre l'URL canonique à plusieurs endroits :
|
||||
|
||||
- `ps_configuration` :
|
||||
- `PS_SHOP_DOMAIN`
|
||||
- `PS_SHOP_DOMAIN_SSL`
|
||||
- `PS_SSL_ENABLED`
|
||||
- `ps_shop_url` :
|
||||
- `domain`
|
||||
- `domain_ssl`
|
||||
|
||||
Ces valeurs sont écrites **une seule fois lors de l'installation automatique** (`PS_INSTALL_AUTO=1`) via `index_cli.php --domain=<PS_DOMAIN>`.
|
||||
|
||||
### 2.2 Redirections canoniques strictes
|
||||
|
||||
Dans `classes/controller/FrontController.php` :
|
||||
|
||||
```php
|
||||
$match_url = Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
|
||||
if (!preg_match('/^' . Tools::pRegexp(rawurldecode($canonical_url), '/') . '([&?].*)?$/', $match_url)) {
|
||||
// ... redirect vers $canonical_url
|
||||
}
|
||||
```
|
||||
|
||||
- Si l'URL demandée ne correspond pas exactement à l'URL canonique stockée, PrestaShop envoie une 301/302.
|
||||
- L'URL canonique est générée avec `Tools::getShopDomainSsl(true)` qui combine :
|
||||
- `ShopUrl::getMainShopDomainSSL()` (domaine figé en base)
|
||||
- `Configuration::get('PS_SSL_ENABLED')` (protocole figé en base)
|
||||
|
||||
### 2.3 Détection SSL
|
||||
|
||||
`Tools::usingSecureMode()` truste bien `HTTP_X_FORWARDED_PROTO: https` (testé et confirmé dans `classes/Tools.php`) :
|
||||
|
||||
```php
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||||
return Tools::strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https';
|
||||
}
|
||||
```
|
||||
|
||||
Donc le resolver transmet correctement le protocole. Le problème ne vient **pas** d'un défaut de PrestaShop sur la détection SSL en lui-même.
|
||||
|
||||
### 2.4 Le vrai problème : l'URL canonique est figée et ne correspond pas à l'URL demandée
|
||||
|
||||
Scénario avec `PS_ENABLE_SSL=1` et `PS_DOMAIN=<id>.alfrednobel.edudeploy.com` :
|
||||
|
||||
- URL demandée : `https://<id>.alfrednobel.edudeploy.com/`
|
||||
- Requête interne : `http://<tailscale-ip>:<port>/` avec `X-Forwarded-Proto: https`
|
||||
- PrestaShop détecte : secure mode = true
|
||||
- URL canonique : `https://<id>.alfrednobel.edudeploy.com/`
|
||||
- `match_url` : `https://<id>.alfrednobol.edudeploy.com/`
|
||||
- Devrait correspondre… mais dans les tests, une boucle a quand même eu lieu.
|
||||
|
||||
Causes probables de la boucle :
|
||||
|
||||
1. **Port dans `HTTP_HOST`** : le resolver envoie `req.Host = host` (sans `:443`), mais Apache/PHP peut parfois enrichir `HTTP_HOST` avec le port interne (`<id>.domain:80`) selon la configuration.
|
||||
2. **Cache navigateur** : une 301 est mise en cache, masquant le fix.
|
||||
3. **Apache `.htaccess` généré par PrestaShop** : contient des règles de rewrite qui peuvent interagir avec le proxy (ex: rediriger `/` vers `index.php` avec un protocole différent).
|
||||
4. **Cookie/SESSION** : la session PHP ou un cookie sécurisé peut forcer une reconnexion/redirection.
|
||||
|
||||
### 2.5 Accès `localhost:<port>` redirige vers le domaine public
|
||||
|
||||
C'est le comportement normal de PrestaShop quand `PS_DOMAIN` est le domaine public. Pour EduBox, ce n'est pas bloquant si l'accès étudiant se fait via l'URL publique ou via le Tailscale IP du professeur.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pistes de solutions
|
||||
|
||||
### Solution A — Utiliser le mécanisme natif `PS_HANDLE_DYNAMIC_DOMAIN=1` (recommandée à tester en premier)
|
||||
|
||||
L'image Docker officielle `prestashop/prestashop` embarque un script `docker_updt_ps_domains.php` qui est copié à la racine et utilisé comme `DirectoryIndex` quand `PS_HANDLE_DYNAMIC_DOMAIN=1`.
|
||||
|
||||
Ce script fait :
|
||||
|
||||
```php
|
||||
$domain = Tools::getHttpHost();
|
||||
$url = ShopUrl::getShopUrls(Configuration::get('PS_SHOP_DEFAULT'))->where('main', '=', 1)->getFirst();
|
||||
if ($url) {
|
||||
$url->domain = $domain;
|
||||
$url->domain_ssl = $domain;
|
||||
$url->save();
|
||||
Configuration::updateValue('PS_SHOP_DOMAIN', $domain);
|
||||
Configuration::updateValue('PS_SHOP_DOMAIN_SSL', $domain);
|
||||
Tools::generateHtaccess();
|
||||
Tools::generateRobotsFile();
|
||||
Tools::clearSmartyCache();
|
||||
Media::clearCache();
|
||||
}
|
||||
Tools::redirect("index.php");
|
||||
```
|
||||
|
||||
**Avantages**
|
||||
- Mécanisme officiel, pas de module à maintenir.
|
||||
- Met à jour le domaine dynamiquement à chaque requête sur `/`.
|
||||
|
||||
**Inconvénients**
|
||||
- Exécution PHP + requêtes SQL + régénération `.htaccess` à chaque requête sur `/` → latence perceptible.
|
||||
- Ne gère pas nativement le HTTPS vs HTTP (domain_ssl = domain, sans tenir compte de X-Forwarded-Proto).
|
||||
- Nécessite d'être combiné avec une confiance des headers proxy.
|
||||
|
||||
**Implémentation proposée**
|
||||
|
||||
Dans le `composeConfig` du template PrestaShop :
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
PS_DOMAIN: {PUBLIC_DOMAIN}
|
||||
PS_ENABLE_SSL: "1"
|
||||
PS_HANDLE_DYNAMIC_DOMAIN: "1"
|
||||
PS_INSTALL_AUTO: "1"
|
||||
PS_INSTALL_DB: "0"
|
||||
```
|
||||
|
||||
Et monter un fichier Apache `proxy.conf` dans `/etc/apache2/conf-enabled/` :
|
||||
|
||||
```apache
|
||||
SetEnvIf X-Forwarded-Proto https HTTPS=on
|
||||
SetEnvIf X-Forwarded-Proto https SERVER_PORT=443
|
||||
SetEnvIf X-Forwarded-Host ^(.+)$ HTTP_HOST=$1
|
||||
```
|
||||
|
||||
Cela permet à `Tools::usingSecureMode()` de retourner `true` et à `Tools::getHttpHost()` de retourner le bon host public.
|
||||
|
||||
**Risques**
|
||||
- Boucle possible si `PS_ENABLE_SSL=1` mais Apache ne reçoit pas `HTTPS=on` (d'où l'importance du `SetEnvIf`).
|
||||
- Performance : le `docker_updt_ps_domains.php` est exécuté à chaque hit sur `/`.
|
||||
|
||||
---
|
||||
|
||||
### Solution B — Créer un module/override PrestaShop "EduBox Public URL" (équivalent du mu-plugin WordPress)
|
||||
|
||||
Créer un override `override/classes/Configuration.php` (ou un module hooké tôt) qui surcharge `Configuration::get()` pour les clés `PS_SHOP_DOMAIN`, `PS_SHOP_DOMAIN_SSL` et `PS_SSL_ENABLED`.
|
||||
|
||||
Exemple d'override :
|
||||
|
||||
```php
|
||||
<?php
|
||||
class Configuration extends ConfigurationCore {
|
||||
public static function get($id_lang = null, $id_shop_group = null, $id_shop = null) {
|
||||
$key = func_num_args() > 0 && is_string(func_get_arg(0)) ? func_get_arg(0) : $id_lang;
|
||||
if ($key === 'PS_SHOP_DOMAIN' || $key === 'PS_SHOP_DOMAIN_SSL') {
|
||||
return Tools::getHttpHost(false, false, true); // host sans port
|
||||
}
|
||||
if ($key === 'PS_SSL_ENABLED') {
|
||||
return Tools::usingSecureMode() ? '1' : '0';
|
||||
}
|
||||
return call_user_func_array(['ConfigurationCore', 'get'], func_get_args());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Avantages**
|
||||
- Le plus proche du mu-plugin WordPress : aucune réécriture de réponses, pas de latence sur `/`.
|
||||
- Une fois l'override chargé, toutes les URLs générées par PrestaShop utilisent le domaine de la requête courante.
|
||||
|
||||
**Inconvénients / pièges**
|
||||
- L'override doit être pris en compte par l'autoloader. Sous PrestaShop 8, il faut vider `app/cache/prod/class_index.php`, `var/cache/*` et régénérer l'index avec `PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex()`.
|
||||
- L'image Docker officielle embarque un `class_index.php` pré-généré qu'il faut invalider.
|
||||
- Si l'installation se fait avec un override déjà présent, PrestaShop peut ne pas l'activer immédiatement.
|
||||
|
||||
**Implémentation proposée**
|
||||
|
||||
1. Créer `agent/psplugins/Configuration.php` (embarqué dans l'agent).
|
||||
2. Au `start`/`reset` d'une instance PrestaShop, l'agent :
|
||||
- copie l'override dans `/var/www/html/override/classes/Configuration.php`
|
||||
- vide les caches (`rm -rf app/cache/* var/cache/*`)
|
||||
- régénère l'autoloader (`php -r "require 'config/config.inc.php'; PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex();"`)
|
||||
3. Ajouter le montage de l'override dans le `composeConfig` PrestaShop via un placeholder `{PS_OVERRIDES_DIR}`.
|
||||
4. Garder la config Apache `SetEnvIf X-Forwarded-Proto https HTTPS=on` pour la détection SSL.
|
||||
|
||||
**Risques**
|
||||
- Fragilité des versions : l'override dépend de la signature de `ConfigurationCore::get()` qui peut changer.
|
||||
- Nécessite de bien gérer les caches à chaque redémarrage du conteneur.
|
||||
|
||||
---
|
||||
|
||||
### Solution C — Installation "localhost" + réécriture complète par le proxy
|
||||
|
||||
Installer PrestaShop avec `PS_DOMAIN=localhost:{PORT}` et `PS_ENABLE_SSL=0`.
|
||||
Le conteneur vit en HTTP interne. Le resolver réécrit :
|
||||
|
||||
- Le header `Location` : `http://localhost:<port>/...` → `https://<id>.domain/...`
|
||||
- Le body HTML/CSS/JS : toutes les occurrences de `http://localhost:<port>` et `//localhost:<port>`
|
||||
|
||||
C'est l'approche "WordPress-like".
|
||||
|
||||
**Avantages**
|
||||
- Pas besoin de modifier PrestaShop.
|
||||
- Le conteneur est totalement agnostique du domaine public.
|
||||
|
||||
**Inconvénients**
|
||||
- PrestaShop génère énormément d'URLs absolues (assets, liens admin, webhooks, modules, API). La réécriture body n'est jamais exhaustive.
|
||||
- Les requêtes AJAX/fetch peuvent pointer vers `localhost:<port>` et échouer côté client.
|
||||
- Le back-office (`/admin`) génère des redirections complexes.
|
||||
- Très fragile sur le long terme.
|
||||
|
||||
**Verdict** : **non recommandée** pour PrestaShop (contrairement à WordPress).
|
||||
|
||||
---
|
||||
|
||||
### Solution D — Image Docker PrestaShop personnalisée (patch durable)
|
||||
|
||||
Créer un `Dockerfile` dérivé de `prestashop/prestashop:8.1` qui :
|
||||
|
||||
1. Applique un patch à `classes/Configuration.php` ou `classes/Tools.php` pour supporter nativement `X-Forwarded-Host`/`X-Forwarded-Proto`.
|
||||
2. Ou embarque directement l'override EduBox + un script d'init qui vide les caches.
|
||||
3. Configure Apache pour trust les headers proxy.
|
||||
|
||||
**Avantages**
|
||||
- Totalement reproductible : pas d'opération manuelle sur le conteneur en cours de vie.
|
||||
- Peut être versionnée et testée indépendamment.
|
||||
|
||||
**Inconvénients**
|
||||
- Nécessite de maintenir et publier une image Docker.
|
||||
- Ajoute une étape de build CI/CD.
|
||||
- Si PrestaShop sort une nouvelle version, il faut rebaser le patch.
|
||||
|
||||
**Implémentation possible**
|
||||
|
||||
```dockerfile
|
||||
FROM prestashop/prestashop:8.1
|
||||
COPY edubox-proxy.conf /etc/apache2/conf-enabled/
|
||||
COPY edubox-override/ /var/www/html/override/
|
||||
RUN chown -R www-data:www-data /var/www/html/override
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Solution E — Proxy "intelligent" avec substitution à la volée
|
||||
|
||||
Remplacer le resolver Go par (ou ajouter devant) un proxy qui fait de la substitution HTML/CSS/JS beaucoup plus fine, par exemple :
|
||||
|
||||
- Nginx avec `sub_filter`
|
||||
- Apache `mod_substitute`
|
||||
- Un middleware Node.js type `http-proxy-middleware` avec `selfHandleResponse`
|
||||
|
||||
**Avantages**
|
||||
- Peut corriger les liens absolus que PrestaShop génère.
|
||||
|
||||
**Inconvénients**
|
||||
- Même problème que la solution C : impossible d'être exhaustif.
|
||||
- Ajoute de la latence et de la complexité.
|
||||
- Peut casser le JS (si on remplace des chaînes dans du code minifié).
|
||||
|
||||
**Verdict** : complément possible, mais pas solution principale.
|
||||
|
||||
---
|
||||
|
||||
### Solution F — Désactiver les redirections canoniques et SSL forcé
|
||||
|
||||
Forcer PrestaShop à ne plus faire de redirections canoniques (`PS_CANONICAL_REDIRECT=0`) et à désactiver SSL partout (`PS_SSL_ENABLED=0`, `PS_SSL_ENABLED_EVERYWHERE=0`).
|
||||
|
||||
**Avantages**
|
||||
- Élimine les boucles de redirection.
|
||||
|
||||
**Inconvénients**
|
||||
- Nécessite un accès au back-office pour modifier les paramètres.
|
||||
- Les liens générés restent en `http://` et pointent vers le domaine d'installation.
|
||||
- Mauvaise expérience utilisateur (avertissements navigateur, mixed-content si certains assets passent en http).
|
||||
|
||||
**Verdict** : à éviter en production.
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommandation
|
||||
|
||||
Je recommande une **combinaison des solutions A et B**, testée méthodiquement :
|
||||
|
||||
### Phase 1 — Solution A (test rapide)
|
||||
|
||||
1. Réintroduire un template PrestaShop 8.1 dans `server/prisma/seed.ts` avec :
|
||||
- `PS_DOMAIN: {PUBLIC_DOMAIN}`
|
||||
- `PS_ENABLE_SSL: "1"`
|
||||
- `PS_HANDLE_DYNAMIC_DOMAIN: "1"`
|
||||
- montage d'une config Apache `proxy.conf` pour trust `X-Forwarded-Proto`
|
||||
2. Rebuilder le serveur, relancer le seed.
|
||||
3. Créer une nouvelle instance PrestaShop (pas de reset d'ancienne instance).
|
||||
4. Tester avec `curl -v -L --max-redirs 5 https://<id>.domain/` et en navigation privée.
|
||||
|
||||
### Phase 2 — Solution B (solution cible)
|
||||
|
||||
Si la solution A est trop lente ou instable, passer à l'override `Configuration.php` :
|
||||
|
||||
1. Créer `agent/psplugins/Configuration.php`.
|
||||
2. Modifier l'agent pour copier l'override et vider/régénérer les caches au démarrage d'une instance PrestaShop.
|
||||
3. Ajouter le montage `{PS_OVERRIDES_DIR}` dans le compose template.
|
||||
4. Conserver la config Apache `SetEnvIf X-Forwarded-Proto`.
|
||||
5. Tester avec `localhost:<port>` ET `https://<id>.domain/`.
|
||||
|
||||
### Phase 3 (optionnel) — Solution D
|
||||
|
||||
Si les solutions A/B sont trop fragiles d'une version de PrestaShop à l'autre, créer une image Docker dérivée patchée et la référencer dans le template.
|
||||
|
||||
---
|
||||
|
||||
## 5. Points d'attention pour l'implémentation
|
||||
|
||||
### Headers proxy
|
||||
|
||||
Le resolver Go transmet déjà les bons headers. Vérifier qu'aucun middleware Next.js ne les modifie (le middleware actuel ne fait que `NextResponse.next()`).
|
||||
|
||||
### Binding de port
|
||||
|
||||
Le commit `dd49993` a changé le binding de `127.0.0.1:{PORT}:80` à `{PORT}:80`. Cela signifie que le conteneur PrestaShop est accessible depuis n'importe quelle interface sur le PC étudiant. C'est nécessaire car le Tailscale proxy de l'agent écoute sur toutes les interfaces. **Il ne faut pas revenir à `127.0.0.1` sauf si on change la chaîne de proxy.**
|
||||
|
||||
### Cache navigateur
|
||||
|
||||
Toujours tester en navigation privée et avec `curl -v -L --max-redirs 5` pour éviter les 301 mises en cache.
|
||||
|
||||
### Vider les caches PrestaShop
|
||||
|
||||
À chaque changement de domaine/config, il faut vider :
|
||||
|
||||
```bash
|
||||
rm -rf /var/www/html/app/cache/*
|
||||
rm -rf /var/www/html/var/cache/*
|
||||
php -r "require '/var/www/html/config/config.inc.php'; PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex();"
|
||||
```
|
||||
|
||||
### Logs utiles
|
||||
|
||||
Sur le PC étudiant :
|
||||
|
||||
```bash
|
||||
podman logs -f <id>-app-1
|
||||
podman exec <id>-app-1 cat /var/log/apache2/error.log
|
||||
```
|
||||
|
||||
Sur le serveur :
|
||||
|
||||
```bash
|
||||
docker logs -f edubox-resolver
|
||||
docker logs -f edubox-caddy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Synthèse comparative
|
||||
|
||||
| Solution | Complexité | Robustesse | Perf. | Maintien | Recommandation |
|
||||
|----------|-----------|------------|-------|----------|----------------|
|
||||
| A — `PS_HANDLE_DYNAMIC_DOMAIN` | Faible | Moyenne | Moyenne (latence `/`) | Faible | **À tester en premier** |
|
||||
| B — Override `Configuration` | Moyenne | Forte | Bonne | Moyen | **Solution cible** |
|
||||
| C — localhost + rewrite proxy | Moyenne | Faible | Bonne | Faible | Non recommandée |
|
||||
| D — Image Docker patchée | Forte | Très forte | Bonne | Fort | Option long terme |
|
||||
| E — Proxy substitution | Moyenne | Faible | Moyenne | Faible | Complément seulement |
|
||||
| F — Désactiver SSL/canonical | Faible | Faible | Bonne | Faible | À éviter |
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
:80 {
|
||||
route /edubox-agent* {
|
||||
route /studioE5-agent* {
|
||||
file_server {
|
||||
root /usr/share/caddy/agent
|
||||
}
|
||||
@@ -22,11 +22,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
headscale.alfrednobel.edudeploy.com {
|
||||
headscale.studioe5.edudeploy.com:443 {
|
||||
reverse_proxy headscale:8080
|
||||
}
|
||||
|
||||
alfrednobel.edudeploy.com {
|
||||
studioe5.edudeploy.com:443 {
|
||||
reverse_proxy /api/websocket* server:3001
|
||||
reverse_proxy server:3000
|
||||
}
|
||||
@@ -36,9 +36,9 @@ alfrednobel.edudeploy.com {
|
||||
on_demand
|
||||
}
|
||||
@instance {
|
||||
not host alfrednobel.edudeploy.com
|
||||
not host headscale.alfrednobel.edudeploy.com
|
||||
host *.alfrednobel.edudeploy.com
|
||||
not host studioe5.edudeploy.com
|
||||
not host headscale.studioe5.edudeploy.com
|
||||
host *.studioe5.edudeploy.com
|
||||
}
|
||||
handle @instance {
|
||||
reverse_proxy resolver:2020 {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# 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/`.
|
||||
|
||||
## ✅ 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/
|
||||
```
|
||||
|
||||
## 📋 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).
|
||||
- [ ] **Nettoyer l’instance test et l’agent test**, puis **committer les modifications** (il reste beaucoup de fichiers modifiés/non suivis ; voir `git status`).
|
||||
- [ ] **Si le wildcard DNS est stable**, envisager d’obtenir un certificat wildcard unique pour `*.studioe5.edudeploy.com` (évite d’émettre un certificat par instance et donc de retomber dans les rate limits). À étudier côté Caddy (`tls { on_demand }` vs certificat wildcard géré manuellement).
|
||||
- [ ] **Tester le flux complet depuis l’interface web** (activation d’un élève, création d’instance via l’UI).
|
||||
- [ ] **Packager les binaires Tailscale pour Windows** dans `agent/tailscale-bin/windows/`.
|
||||
- [ ] **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.).
|
||||
|
||||
## 🔧 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.
|
||||
+29
-16
@@ -2,33 +2,46 @@
|
||||
set -e
|
||||
|
||||
VERSION="0.3.0"
|
||||
APP_NAME="studioE5"
|
||||
BIN_NAME="studioE5-agent"
|
||||
LDFLAGS="-X main.version=${VERSION}"
|
||||
|
||||
echo "Building EduBox Agent v${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}..."
|
||||
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
|
||||
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=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=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)"
|
||||
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=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)"
|
||||
# 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
|
||||
|
||||
# Copy versioned binaries to server/public so the dashboard can serve them
|
||||
SERVER_PUBLIC="../server/public"
|
||||
if [ -d "${SERVER_PUBLIC}" ]; then
|
||||
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"
|
||||
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
|
||||
echo " Copied versioned binaries to ${SERVER_PUBLIC}"
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
|
||||
// defaultConfig returns sensible defaults for a first run.
|
||||
func defaultConfig(dataDir string) *AgentConfig {
|
||||
return &AgentConfig{
|
||||
Server: "ws://localhost:3001",
|
||||
HeadscaleURL: "",
|
||||
HeadscaleAuthKey: "",
|
||||
NodeID: defaultNodeID(),
|
||||
DataDir: dataDir,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
var cfg AgentConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
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)
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(cp, data, 0644)
|
||||
}
|
||||
+5
-3
@@ -20,18 +20,20 @@ func getContainerEngine() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
func writeCompose(dataDir, instanceID, compose string) error {
|
||||
func writeCompose(dataDir, instanceID, compose string, port int) error {
|
||||
dir := instanceDir(dataDir, instanceID)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the EduBox mu-plugin is available and substitute its path
|
||||
// Ensure the studioE5 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)
|
||||
@@ -115,7 +117,7 @@ fi
|
||||
}
|
||||
|
||||
// stripWordPressHardcodedURLs removes hardcoded WP_HOME/WP_SITEURL defines
|
||||
// from wp-config.php so the EduBox mu-plugin can compute them from the Host
|
||||
// from wp-config.php so the studioE5 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 {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
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")
|
||||
+1
-41
@@ -3,52 +3,12 @@ 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
|
||||
)
|
||||
|
||||
+2
-224
@@ -1,233 +1,11 @@
|
||||
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=
|
||||
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
|
||||
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
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.
|
After Width: | Height: | Size: 193 B |
+46
-12
@@ -5,21 +5,22 @@ 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"
|
||||
const (
|
||||
AGENT_VERSION = "0.3.0"
|
||||
APP_NAME = "studioE5"
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
dataDir = flag.String("data-dir", "./studioE5-data", "Répertoire de données")
|
||||
uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX")
|
||||
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")
|
||||
noTray = flag.Bool("no-tray", false, "Désactiver l'icône dans la barre système (mode console)")
|
||||
)
|
||||
|
||||
func defaultNodeID() string {
|
||||
@@ -49,19 +50,52 @@ func main() {
|
||||
log.Fatalf("Cannot create data-dir: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[EduBox Agent] version=%s node=%s data-dir=%s", AGENT_VERSION, *nodeID, *dataDir)
|
||||
cfg, created, err := loadOrCreateConfig(*dataDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Server == "" {
|
||||
cfg.Server = "ws://localhost:3001"
|
||||
}
|
||||
if cfg.NodeID == "" {
|
||||
cfg.NodeID = defaultNodeID()
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = *dataDir
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if *uiEnabled {
|
||||
go startUI(*dataDir, *nodeID, *serverAddr)
|
||||
go startUI(*dataDir, cfg.NodeID, cfg.Server)
|
||||
if created {
|
||||
go openBrowser(uiURL + "#settings")
|
||||
}
|
||||
}
|
||||
|
||||
go startWebSocket(*serverAddr, *nodeID, *dataDir)
|
||||
go startWebSocket(cfg.Server, cfg.NodeID, *dataDir, cfg.HeadscaleURL, cfg.HeadscaleAuthKey)
|
||||
|
||||
if *headscaleURL != "" && *headscaleAuthKey != "" {
|
||||
go startTailscaleAndReport(*dataDir, *nodeID, *headscaleURL, *headscaleAuthKey)
|
||||
shutdownCh := make(chan struct{})
|
||||
if *noTray {
|
||||
log.Printf("[%s Agent] running in console mode (no tray)", APP_NAME)
|
||||
<-shutdownCh
|
||||
return
|
||||
}
|
||||
|
||||
select {}
|
||||
// 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
|
||||
}
|
||||
|
||||
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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, " ", "")
|
||||
}
|
||||
+136
-62
@@ -2,104 +2,178 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
var globalTSServer *tsnet.Server
|
||||
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
|
||||
}
|
||||
|
||||
func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, error) {
|
||||
// Configure tsnet to use our Headscale server
|
||||
os.Setenv("TS_AUTHKEY", authKey)
|
||||
os.Setenv("TS_CONTROL_URL", headscaleURL)
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
|
||||
s := &tsnet.Server{
|
||||
Hostname: nodeID,
|
||||
Dir: dataDir,
|
||||
Logf: log.Printf,
|
||||
if tsCmd != nil {
|
||||
return tsIP, nil
|
||||
}
|
||||
|
||||
if err := s.Start(); err != nil {
|
||||
return "", fmt.Errorf("tailscale start: %w", err)
|
||||
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)
|
||||
}
|
||||
|
||||
globalTSServer = s
|
||||
// Give tailscaled a moment to start listening.
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Wait for Tailscale to come up and retrieve IP
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
// Bring the interface up with the auth key.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tailscale local client: %w", err)
|
||||
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)
|
||||
}
|
||||
|
||||
var tailscaleIP string
|
||||
// Wait for an IP address.
|
||||
for {
|
||||
status, err := lc.Status(ctx)
|
||||
out, err := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
"--socket="+tsSocket,
|
||||
"status", "--json",
|
||||
).Output()
|
||||
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
|
||||
}
|
||||
if status.Self != nil && len(status.Self.TailscaleIPs) > 0 {
|
||||
tailscaleIP = status.Self.TailscaleIPs[0].String()
|
||||
}
|
||||
var st tailscaleStatus
|
||||
if err := json.Unmarshal(out, &st); err == nil && len(st.Self.TailscaleIPs) > 0 {
|
||||
tsIP = st.Self.TailscaleIPs[0]
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("tailscale IP timeout")
|
||||
case <-time.After(1 * time.Second):
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Tailscale started with IP: %s", tailscaleIP)
|
||||
return tailscaleIP, nil
|
||||
log.Printf("Tailscale started with IP: %s", tsIP)
|
||||
return tsIP, nil
|
||||
}
|
||||
|
||||
func startTailscaleProxy(port int) (net.Listener, error) {
|
||||
if globalTSServer == nil {
|
||||
return nil, fmt.Errorf("tailscale server not started")
|
||||
}
|
||||
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)
|
||||
func stopTailscale() {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
stopTailscaleLocked()
|
||||
}
|
||||
|
||||
func stopTailscaleLocked() {
|
||||
if tsCmd == nil || tsCmd.Process == nil {
|
||||
return
|
||||
}
|
||||
go handleProxyConn(conn, port)
|
||||
if tsSocket != "" {
|
||||
_ = exec.Command(tailscaleBin("tailscale"), "--socket="+tsSocket, "down").Run()
|
||||
}
|
||||
}()
|
||||
log.Printf("Tailscale proxy started on port %d", port)
|
||||
return ln, nil
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
tsIP = ""
|
||||
log.Printf("Tailscale stopped")
|
||||
}
|
||||
|
||||
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
|
||||
func isTailscaleRunning() bool {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
if tsCmd == nil || tsCmd.Process == nil {
|
||||
return false
|
||||
}
|
||||
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
|
||||
// Signal 0 checks process existence without affecting it.
|
||||
return tsCmd.Process.Signal(syscall.Signal(0)) == nil
|
||||
}
|
||||
|
||||
func getTailscaleIP() string {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
return tsIP
|
||||
}
|
||||
|
||||
|
||||
|
||||
+54
-2
@@ -2,9 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -20,6 +23,55 @@ 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 {
|
||||
@@ -72,9 +124,9 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
})
|
||||
|
||||
port := "7070"
|
||||
log.Printf("UI starting on http://localhost:%s", port)
|
||||
log.Printf("%s UI starting on http://localhost:%s", APP_NAME, port)
|
||||
if err := http.ListenAndServe("127.0.0.1:"+port, nil); err != nil {
|
||||
log.Fatalf("UI server error: %v", err)
|
||||
log.Fatalf("%s UI server error: %v", APP_NAME, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+101
-3
@@ -2,7 +2,7 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>EduBox Agent</title>
|
||||
<title>studioE5 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,9 +10,13 @@
|
||||
.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; }
|
||||
@@ -30,16 +34,44 @@
|
||||
.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 class="card">
|
||||
<h1>EduBox Agent</h1>
|
||||
<div id="home-card" class="card">
|
||||
<h1>studioE5 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>
|
||||
@@ -49,6 +81,8 @@
|
||||
<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');
|
||||
|
||||
@@ -60,6 +94,7 @@
|
||||
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()">
|
||||
@@ -67,9 +102,13 @@
|
||||
<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'}));
|
||||
@@ -130,6 +169,65 @@
|
||||
}).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)
|
||||
|
||||
+64
-50
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -29,11 +28,6 @@ var (
|
||||
mainConnMu sync.Mutex
|
||||
)
|
||||
|
||||
var (
|
||||
tsProxies = make(map[int]net.Listener)
|
||||
tsProxiesMu sync.Mutex
|
||||
)
|
||||
|
||||
func sendMessage(msg WSMessage) error {
|
||||
mainConnMu.Lock()
|
||||
defer mainConnMu.Unlock()
|
||||
@@ -86,7 +80,7 @@ func notifyUI(msg map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
func startWebSocket(serverAddr, nodeID, dataDir, headscaleURL, headscaleAuthKey string) {
|
||||
for {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(serverAddr, nil)
|
||||
if err != nil {
|
||||
@@ -144,7 +138,7 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
break
|
||||
}
|
||||
log.Printf("WS received from server: action=%s", msg.Action)
|
||||
handleMessage(conn, msg, dataDir, nodeID)
|
||||
handleMessage(conn, msg, dataDir, nodeID, headscaleURL, headscaleAuthKey)
|
||||
}
|
||||
|
||||
close(done)
|
||||
@@ -157,7 +151,7 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) {
|
||||
func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID, headscaleURL, headscaleAuthKey string) {
|
||||
switch msg.Action {
|
||||
case "activated":
|
||||
log.Printf("handleMessage: activated received, student=%s", msg.StudentName)
|
||||
@@ -176,6 +170,34 @@ 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")
|
||||
if headscaleURL == "" || headscaleAuthKey == "" {
|
||||
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, headscaleURL, headscaleAuthKey)
|
||||
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{}{
|
||||
@@ -192,7 +214,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); err != nil {
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig, msg.Port); 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()})
|
||||
@@ -205,7 +227,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
return
|
||||
}
|
||||
// Repair older WordPress instances: remove hardcoded WP_HOME/WP_SITEURL
|
||||
// so the EduBox mu-plugin can compute the public URL from the Host header.
|
||||
// so the studioE5 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)
|
||||
@@ -213,16 +235,8 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("stripWordPressHardcodedURLs error: %v", err)
|
||||
}
|
||||
}()
|
||||
// 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()
|
||||
// Ensure Tailscale is running so the server can reach the node
|
||||
go ensureTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey, msg.Port)
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
@@ -230,15 +244,6 @@ 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)
|
||||
}
|
||||
@@ -249,21 +254,13 @@ 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); err != nil {
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig, msg.Port); 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()})
|
||||
@@ -276,7 +273,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
return
|
||||
}
|
||||
// Repair older WordPress instances: remove hardcoded WP_HOME/WP_SITEURL
|
||||
// so the EduBox mu-plugin can compute the public URL from the Host header.
|
||||
// so the studioE5 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)
|
||||
@@ -284,16 +281,8 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("stripWordPressHardcodedURLs error: %v", err)
|
||||
}
|
||||
}()
|
||||
// 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()
|
||||
// Ensure Tailscale is running so the server can reach the node
|
||||
go ensureTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey, msg.Port)
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
@@ -303,3 +292,28 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("Unknown action: %s", msg.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey string, port int) {
|
||||
if headscaleURL == "" || headscaleAuthKey == "" {
|
||||
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, headscaleURL, headscaleAuthKey)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+38
-44
@@ -1,18 +1,18 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
container_name: edubox-postgres
|
||||
container_name: studioe5-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: edubox
|
||||
POSTGRES_USER: studioe5
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: edubox
|
||||
POSTGRES_DB: studioe5
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql
|
||||
networks:
|
||||
- edubox
|
||||
- studioe5
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U edubox -d edubox"]
|
||||
test: ["CMD-SHELL", "pg_isready -U studioe5 -d studioe5"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -21,13 +21,9 @@ services:
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
container_name: edubox-server
|
||||
container_name: studioe5-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}
|
||||
@@ -35,28 +31,19 @@ services:
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL}
|
||||
SUPERADMIN_EMAIL: ${SUPERADMIN_EMAIL}
|
||||
SUPERADMIN_PASSWORD: ${SUPERADMIN_PASSWORD}
|
||||
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:
|
||||
- edubox
|
||||
|
||||
- studioe5
|
||||
|
||||
resolver:
|
||||
build:
|
||||
context: ./resolver
|
||||
dockerfile: Dockerfile
|
||||
container_name: edubox-resolver
|
||||
container_name: studioe5-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}
|
||||
@@ -64,11 +51,34 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- edubox
|
||||
- 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
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: edubox-caddy
|
||||
container_name: studioe5-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
@@ -79,11 +89,11 @@ services:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- edubox
|
||||
- studioe5
|
||||
|
||||
headscale:
|
||||
image: headscale/headscale:latest
|
||||
container_name: edubox-headscale
|
||||
container_name: studioe5-headscale
|
||||
restart: unless-stopped
|
||||
command: serve
|
||||
ports:
|
||||
@@ -92,31 +102,15 @@ services:
|
||||
volumes:
|
||||
- ./headscale:/etc/headscale
|
||||
networks:
|
||||
- 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
|
||||
- studioe5
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
headscale_data:
|
||||
gitea_data:
|
||||
resolver_ts_state:
|
||||
|
||||
networks:
|
||||
edubox:
|
||||
studioe5:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Headscale configuration for EduBox
|
||||
server_url: https://headscale.alfrednobel.edudeploy.com
|
||||
# Headscale configuration for studioE5 client A
|
||||
server_url: https://headscale.studioe5.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: edubox.local
|
||||
base_domain: studioe5.local
|
||||
nameservers:
|
||||
global:
|
||||
- 1.1.1.1
|
||||
|
||||
@@ -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">EduBox V2</h1>
|
||||
<h1 className="text-2xl font-bold text-center text-gray-900">studioE5</h1>
|
||||
<p className="text-center text-muted-foreground">Connexion à la plateforme</p>
|
||||
<LoginForm />
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,10 @@ 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,12 +1,13 @@
|
||||
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: `/edubox-agent-v${AGENT_VERSION}.exe`,
|
||||
linux: `/edubox-agent-v${AGENT_VERSION}`,
|
||||
mac: `/edubox-agent-v${AGENT_VERSION}-mac`,
|
||||
windows: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}.exe`,
|
||||
linux: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}`,
|
||||
mac: `/${AGENT_BIN_NAME}-v${AGENT_VERSION}-mac`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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">EduBox</h2>
|
||||
<h2 className="text-xl font-bold text-primary">studioE5</h2>
|
||||
</div>
|
||||
<div className="flex-1 p-4 space-y-1">
|
||||
{links.map((link) => (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
|
||||
@@ -15,8 +16,8 @@ export default function DownloadPage() {
|
||||
<CardTitle>Windows</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import "./globals.css";
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "EduBox V2",
|
||||
title: "studioE5",
|
||||
description: "Plateforme de gestion d'instances pour l'enseignement BTS",
|
||||
};
|
||||
|
||||
|
||||
+35
-5
@@ -19,9 +19,6 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
// Remove obsolete PrestaShop templates from previous seeds
|
||||
await prisma.template.deleteMany({ where: { type: "prestashop" } });
|
||||
|
||||
const templates = [
|
||||
{
|
||||
name: "WordPress latest vierge",
|
||||
@@ -53,20 +50,53 @@ 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-8",
|
||||
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 = ` WORDPRESS_DB_HOST: ${dbHost}:${dbPort}
|
||||
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}
|
||||
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 = ` volumes:
|
||||
const appVolumes = isPrestaShop
|
||||
? ` volumes:
|
||||
- app_data:/var/www/html`
|
||||
: ` 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`;
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user