@imenam/self-register-proxy 1.0.42 → 1.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -8
- package/config.js +40 -2
- package/package.json +1 -1
- package/proxy.js +39 -2
- package/routes/dashboard.js +1 -1
- package/routes/metaConfig.js +2 -2
- package/services/auth.js +20 -2
- package/services/portManager.js +59 -4
- package/views/dashboard.js +309 -803
- package/views/login.js +61 -130
- package/views/metaConfigs.js +403 -643
- package/views/terminal.html +272 -691
- package/views/terminal.js +34 -30
- package/views/theme.js +431 -0
package/README.md
CHANGED
|
@@ -472,10 +472,18 @@ Le proxy charge automatiquement le fichier `.env` s'il existe (voir `.env.exampl
|
|
|
472
472
|
|----------|--------|-------------|
|
|
473
473
|
| `PROXY_NAME` | `Proxy Server` | Nom affiché sur la page d'accueil |
|
|
474
474
|
| `PROXY_PORT` | `3000` | Port d'écoute du proxy |
|
|
475
|
+
| `PROXY_HOST` | *(toutes les interfaces)* | Adresse d'écoute. `127.0.0.1` confine le proxy — dashboard et terminal compris — à la machine locale. Voir [Sécurité](#sécurité). |
|
|
476
|
+
| `PROXY_INTERNAL_PORT_START` | `4000` | Premier port attribuable aux applications enregistrées |
|
|
477
|
+
| `PROXY_INTERNAL_PORT_END` | `5000` | Dernier port attribuable |
|
|
478
|
+
| `PROXY_ADMIN_PASSWORD` | *(aucun)* | Mot de passe du dashboard. **Sans lui, le dashboard, son terminal et les routes shell ne sont pas protégés.** |
|
|
479
|
+
| `PROXY_SESSION_SECRET` | *(généré)* | Clé de signature des sessions. **Obligatoire** dès que `PROXY_ADMIN_PASSWORD` est défini, sinon le proxy sort en `exit(1)`. |
|
|
475
480
|
| `HEALTH_CHECK_INTERVAL` | `30000` | Intervalle du health check polling (ms) |
|
|
476
481
|
| `HEALTH_CHECK_TIMEOUT` | `5000` | Timeout pour chaque health check (ms) |
|
|
477
482
|
| `HEALTH_CHECK_GRACE_PERIOD` | `60000` | Période de grâce pour les nouveaux serveurs (ms) |
|
|
478
483
|
| `PROXY_CONFIG_DIR` | `.` | Dossier de stockage des configurations (meta-configs.json) |
|
|
484
|
+
| `PROXY_TERMINAL_CWD` | `$HOME` / `%USERPROFILE%` | Répertoire de travail du terminal en ligne |
|
|
485
|
+
| `PROXY_TERMINAL_COMMAND` | *(shell par défaut)* | Commande lancée par le terminal en ligne |
|
|
486
|
+
| `PROXY_OPENCODE_CLEAN_UP` | *(aucune)* | Commande exécutée par `POST /proxy/cleanup` |
|
|
479
487
|
|
|
480
488
|
### Variables pour les applications clientes
|
|
481
489
|
|
|
@@ -484,18 +492,69 @@ Le proxy charge automatiquement le fichier `.env` s'il existe (voir `.env.exampl
|
|
|
484
492
|
| `PROXY_URL` | `http://localhost:3000` | URL du proxy (pour les apps) |
|
|
485
493
|
| `APP_PATH` | `/example` | Chemin de l'application |
|
|
486
494
|
| `APP_NAME` | `Example App` | Nom de l'application |
|
|
495
|
+
| `APP_GROUP` | *(aucun)* | Groupe d'affichage dans le dashboard. Absent : l'application apparaît sous « Non groupées ». |
|
|
487
496
|
|
|
488
|
-
##
|
|
497
|
+
## Sécurité
|
|
489
498
|
|
|
490
|
-
|
|
499
|
+
**Le dashboard n'est pas qu'une liste de liens.** Il sert un terminal interactif
|
|
500
|
+
(`/proxy/terminal`, un vrai PTY sur la machine) et deux routes qui exécutent du
|
|
501
|
+
shell : `POST /proxy/kill-port` et `POST /proxy/cleanup`. Elles sont derrière
|
|
502
|
+
`requireAuth` — **qui laisse tout passer quand `PROXY_ADMIN_PASSWORD` n'est pas
|
|
503
|
+
défini** (rétrocompatibilité historique). Le `verifyClient` du WebSocket du
|
|
504
|
+
terminal a la même porte ouverte.
|
|
491
505
|
|
|
492
|
-
|
|
506
|
+
Le proxy écoute par défaut sur **toutes les interfaces**. Sans mot de passe, il
|
|
507
|
+
offre donc un shell sans identifiants à quiconque atteint son port depuis le
|
|
508
|
+
réseau. Deux postures saines, à choisir explicitement :
|
|
493
509
|
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
|
|
510
|
+
```bash
|
|
511
|
+
# Poste local : rien n'est exposé hors de la machine.
|
|
512
|
+
PROXY_HOST=127.0.0.1
|
|
513
|
+
|
|
514
|
+
# Accès distant / tunnel : le dashboard est protégé.
|
|
515
|
+
PROXY_ADMIN_PASSWORD=...
|
|
516
|
+
PROXY_SESSION_SECRET=... # 32 octets hex ; obligatoire avec le mot de passe
|
|
497
517
|
```
|
|
498
518
|
|
|
519
|
+
Le proxy affiche au démarrage l'adresse à laquelle il se lie et avertit
|
|
520
|
+
bruyamment quand ces deux protections manquent toutes les deux.
|
|
521
|
+
|
|
522
|
+
**HSTS.** Jusqu'à la 1.0.43, un en-tête `Strict-Transport-Security` était émis dès
|
|
523
|
+
qu'un mot de passe était défini, même sur des réponses servies en clair : le
|
|
524
|
+
navigateur forçait ensuite `https://` sur cette origine pour un an, et le proxy —
|
|
525
|
+
qui ne sert pas de TLS — répondait `ERR_SSL_PROTOCOL_ERROR`. Depuis la 1.0.44,
|
|
526
|
+
HSTS n'est posé que sur une requête réellement en HTTPS (`req.secure`, ou un
|
|
527
|
+
`X-Forwarded-Proto: https` de terminaison TLS). Le cookie de session suit la même
|
|
528
|
+
règle pour son attribut `Secure`, qui empêchait sinon toute connexion sur un
|
|
529
|
+
domaine servi en clair. Si votre navigateur a mémorisé l'ordre, purgez l'origine
|
|
530
|
+
dans `chrome://net-internals/#hsts`.
|
|
531
|
+
|
|
532
|
+
## Plage de ports
|
|
533
|
+
|
|
534
|
+
Par défaut, le proxy attribue des ports entre **4000** et **5000** (1000 ports
|
|
535
|
+
disponibles). Depuis la 1.0.44 la plage se règle par variables d'environnement,
|
|
536
|
+
sans toucher au code :
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
PROXY_INTERNAL_PORT_START=5200
|
|
540
|
+
PROXY_INTERNAL_PORT_END=5400
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Une valeur invalide, ou une plage inversée, est ignorée au profit des valeurs par
|
|
544
|
+
défaut, avec un avertissement au démarrage.
|
|
545
|
+
|
|
546
|
+
**Faites-le si deux proxys tournent sur la même machine.** Chaque proxy gère sa
|
|
547
|
+
plage pour lui seul et ignore l'existence de l'autre : avec la même plage des deux
|
|
548
|
+
côtés, le second peut attribuer à une application un port déjà occupé par une
|
|
549
|
+
application du premier. Sa sonde de santé interroge alors le service de l'autre,
|
|
550
|
+
qui répond — la route ne meurt jamais et les requêtes partent au mauvais endroit.
|
|
551
|
+
|
|
552
|
+
> **Correctif 1.0.44 — sonde de disponibilité.** Elle se contentait d'essayer de
|
|
553
|
+
> se lier au port. Sous Windows, lier `127.0.0.1:<port>` **réussit** alors qu'un
|
|
554
|
+
> autre process écoute déjà sur `0.0.0.0:<port>` : un port occupé était déclaré
|
|
555
|
+
> libre, puis attribué. La sonde tente désormais aussi une **connexion**, ce qui
|
|
556
|
+
> donne la même réponse sur tous les systèmes.
|
|
557
|
+
|
|
499
558
|
## Fonctionnalités
|
|
500
559
|
|
|
501
560
|
- **Attribution sécurisée des ports** : utilisation d'un mutex pour éviter les conflits lors d'enregistrements simultanés
|
|
@@ -514,9 +573,16 @@ Le format des logs inclut la date, l'heure, le PID et le tag `[PROXY]`.
|
|
|
514
573
|
|
|
515
574
|
## Limitations
|
|
516
575
|
|
|
517
|
-
- Les routes sont stockées en mémoire (perdues au redémarrage du proxy)
|
|
576
|
+
- Les routes sont stockées en mémoire (perdues au redémarrage du proxy), et **les
|
|
577
|
+
applications ne se réenregistrent pas d'elles-mêmes** : après un redémarrage du
|
|
578
|
+
proxy, il faut relancer chaque application
|
|
518
579
|
- Pas de persistance des enregistrements
|
|
519
|
-
-
|
|
580
|
+
- `POST /proxy/register` et `DELETE /proxy/unregister` sont ouvertes sans mot de
|
|
581
|
+
passe, mais **restreintes à localhost**. Le dashboard et ses routes shell, eux,
|
|
582
|
+
ne sont protégés que par `PROXY_ADMIN_PASSWORD` — voir [Sécurité](#sécurité)
|
|
583
|
+
- `group` est un **libellé d'affichage**, pas un espace de noms : il ne dédoublonne
|
|
584
|
+
pas les chemins. Deux applications au même chemin restent en conflit (409), quel
|
|
585
|
+
que soit leur groupe
|
|
520
586
|
- Les applications doivent implémenter `GET /proxy/health` pour ne pas être désenregistrées automatiquement
|
|
521
587
|
|
|
522
588
|
## Licence
|
package/config.js
CHANGED
|
@@ -2,8 +2,45 @@ const crypto = require('crypto');
|
|
|
2
2
|
|
|
3
3
|
const PROXY_PORT = process.env.PROXY_PORT || 3000;
|
|
4
4
|
const PROXY_NAME = process.env.PROXY_NAME || 'Proxy Server';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface d'écoute. Non définie = comportement historique : Express écoute sur
|
|
8
|
+
* **toutes** les interfaces (0.0.0.0), le dashboard et son terminal PTY sont donc
|
|
9
|
+
* joignables depuis le réseau. `PROXY_HOST=127.0.0.1` restreint l'écoute à la
|
|
10
|
+
* boucle locale — c'est ce que pose un superviseur qui démarre un proxy sans
|
|
11
|
+
* `PROXY_ADMIN_PASSWORD`.
|
|
12
|
+
*/
|
|
13
|
+
const PROXY_HOST = process.env.PROXY_HOST || undefined;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Plage des ports attribués aux applications enregistrées.
|
|
17
|
+
*
|
|
18
|
+
* Historiquement codée en dur à 4000-5000, ce qui rendait impossible de faire
|
|
19
|
+
* cohabiter deux proxys sur une même machine sans qu'ils se disputent les mêmes
|
|
20
|
+
* ports. Elle est désormais configurable ; **les valeurs par défaut ne changent
|
|
21
|
+
* pas**, et une valeur invalide est ignorée au profit du défaut.
|
|
22
|
+
*/
|
|
23
|
+
function readPortBound(name, fallback) {
|
|
24
|
+
const raw = process.env[name];
|
|
25
|
+
if (raw === undefined || raw === '') return fallback;
|
|
26
|
+
const value = Number.parseInt(raw, 10);
|
|
27
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
28
|
+
console.warn(`[CONFIG] ${name}="${raw}" invalide — valeur par défaut ${fallback} conservée.`);
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let INTERNAL_PORT_START = readPortBound('PROXY_INTERNAL_PORT_START', 4000);
|
|
35
|
+
let INTERNAL_PORT_END = readPortBound('PROXY_INTERNAL_PORT_END', 5000);
|
|
36
|
+
if (INTERNAL_PORT_START > INTERNAL_PORT_END) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`[CONFIG] Plage de ports internes inversée (${INTERNAL_PORT_START}-${INTERNAL_PORT_END}) — ` +
|
|
39
|
+
'retour aux valeurs par défaut 4000-5000.'
|
|
40
|
+
);
|
|
41
|
+
INTERNAL_PORT_START = 4000;
|
|
42
|
+
INTERNAL_PORT_END = 5000;
|
|
43
|
+
}
|
|
7
44
|
const HEALTH_CHECK_INTERVAL = process.env.HEALTH_CHECK_INTERVAL || 30000;
|
|
8
45
|
const HEALTH_CHECK_TIMEOUT = process.env.HEALTH_CHECK_TIMEOUT || 5000;
|
|
9
46
|
const HEALTH_CHECK_GRACE_PERIOD = process.env.HEALTH_CHECK_GRACE_PERIOD || 60000;
|
|
@@ -20,6 +57,7 @@ const MINIMAL_DASHBOARD = process.argv.includes('--minimal-dashboard');
|
|
|
20
57
|
module.exports = {
|
|
21
58
|
PROXY_PORT,
|
|
22
59
|
PROXY_NAME,
|
|
60
|
+
PROXY_HOST,
|
|
23
61
|
INTERNAL_PORT_START,
|
|
24
62
|
INTERNAL_PORT_END,
|
|
25
63
|
HEALTH_CHECK_INTERVAL,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/self-register-proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
4
4
|
"description": "Dynamic reverse proxy with self-registration API - applications can register themselves and receive an automatically assigned port",
|
|
5
5
|
"main": "proxy-client.js",
|
|
6
6
|
"bin": {
|
package/proxy.js
CHANGED
|
@@ -14,6 +14,7 @@ setupLogging('PROXY');
|
|
|
14
14
|
const {
|
|
15
15
|
PROXY_PORT,
|
|
16
16
|
PROXY_NAME,
|
|
17
|
+
PROXY_HOST,
|
|
17
18
|
INTERNAL_PORT_START,
|
|
18
19
|
INTERNAL_PORT_END,
|
|
19
20
|
HEALTH_CHECK_INTERVAL,
|
|
@@ -42,6 +43,26 @@ const dashboardRoutes = require('./routes/dashboard');
|
|
|
42
43
|
|
|
43
44
|
const app = express();
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Vrai si la requête est arrivée en HTTPS — directement, ou via un terminaison
|
|
48
|
+
* TLS en amont qui l'annonce (`X-Forwarded-Proto`, posé par ngrok, nginx, etc.).
|
|
49
|
+
*/
|
|
50
|
+
const isHttpsRequest = (req) =>
|
|
51
|
+
req.secure || String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim() === 'https';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* HSTS **uniquement sur une requête réellement servie en HTTPS**.
|
|
55
|
+
*
|
|
56
|
+
* Avant : HSTS était activé dès qu'un mot de passe était défini, quel que soit le
|
|
57
|
+
* protocole. Un `http://127.0.0.1:3000` renvoyait donc un `Strict-Transport-Security`
|
|
58
|
+
* que le navigateur mémorisait pour un an : toute visite suivante était forcée en
|
|
59
|
+
* `https://`, et le proxy — qui ne sert pas de TLS — répondait `ERR_SSL_PROTOCOL_ERROR`.
|
|
60
|
+
* Le même piège est franchement bloquant sur un domaine servi en clair.
|
|
61
|
+
*
|
|
62
|
+
* Un en-tête HSTS émis sur une réponse HTTP est de toute façon ignoré par les
|
|
63
|
+
* navigateurs conformes (RFC 6797 §7.2) : le restreindre à HTTPS ne retire donc
|
|
64
|
+
* aucune protection réelle aux déploiements sous TLS.
|
|
65
|
+
*/
|
|
45
66
|
app.use(helmet({
|
|
46
67
|
contentSecurityPolicy: {
|
|
47
68
|
directives: {
|
|
@@ -53,10 +74,16 @@ app.use(helmet({
|
|
|
53
74
|
imgSrc: ["'self'", "data:", "https://i.ytimg.com"],
|
|
54
75
|
},
|
|
55
76
|
},
|
|
56
|
-
hsts:
|
|
77
|
+
hsts: false,
|
|
57
78
|
referrerPolicy: { policy: 'same-origin' },
|
|
58
79
|
}));
|
|
59
80
|
|
|
81
|
+
const hstsMiddleware = helmet.hsts({ maxAge: 31536000, includeSubDomains: true });
|
|
82
|
+
app.use((req, res, next) => {
|
|
83
|
+
if (ADMIN_PASSWORD && isHttpsRequest(req)) return hstsMiddleware(req, res, next);
|
|
84
|
+
next();
|
|
85
|
+
});
|
|
86
|
+
|
|
60
87
|
const isLocalRequest = (req) => {
|
|
61
88
|
const ip = req.ip || req.connection?.remoteAddress || '';
|
|
62
89
|
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
@@ -97,11 +124,15 @@ app.use(routingMiddleware);
|
|
|
97
124
|
// DÉMARRAGE DU SERVEUR
|
|
98
125
|
// ============================================
|
|
99
126
|
|
|
100
|
-
|
|
127
|
+
// `PROXY_HOST` non défini = comportement historique (toutes les interfaces).
|
|
128
|
+
const listenArgs = PROXY_HOST ? [PROXY_PORT, PROXY_HOST] : [PROXY_PORT];
|
|
129
|
+
|
|
130
|
+
const server = app.listen(...listenArgs, () => {
|
|
101
131
|
console.log('='.repeat(50));
|
|
102
132
|
console.log(`🚀 ${PROXY_NAME}`);
|
|
103
133
|
console.log('='.repeat(50));
|
|
104
134
|
console.log(`Listening on port ${PROXY_PORT}`);
|
|
135
|
+
console.log(`Bind address: ${PROXY_HOST || '0.0.0.0 (toutes les interfaces)'}`);
|
|
105
136
|
console.log(`Internal ports range: ${INTERNAL_PORT_START}-${INTERNAL_PORT_END}`);
|
|
106
137
|
console.log(`Health check interval: ${HEALTH_CHECK_INTERVAL}ms`);
|
|
107
138
|
console.log(`Health check grace period: ${HEALTH_CHECK_GRACE_PERIOD}ms`);
|
|
@@ -109,6 +140,12 @@ const server = app.listen(PROXY_PORT, () => {
|
|
|
109
140
|
|
|
110
141
|
if (!ADMIN_PASSWORD) {
|
|
111
142
|
console.log('⚠️ WARNING: ADMIN_PASSWORD is not set — web interface is unprotected');
|
|
143
|
+
console.log('⚠️ Le dashboard expose un terminal PTY et les routes kill-port / cleanup,');
|
|
144
|
+
console.log('⚠️ qui exécutent du shell sans authentification.');
|
|
145
|
+
if (!PROXY_HOST) {
|
|
146
|
+
console.log('⚠️ Écoute sur toutes les interfaces : cette surface est joignable depuis le réseau.');
|
|
147
|
+
console.log('⚠️ Posez PROXY_ADMIN_PASSWORD, ou PROXY_HOST=127.0.0.1 pour la boucle locale.');
|
|
148
|
+
}
|
|
112
149
|
} else {
|
|
113
150
|
console.log('🔒 Web interface protected by admin password');
|
|
114
151
|
console.log('🔒 Session secret configured');
|
package/routes/dashboard.js
CHANGED
package/routes/metaConfig.js
CHANGED
|
@@ -3,7 +3,7 @@ const metaConfig = require('../meta-config');
|
|
|
3
3
|
const { requireAuth } = require('../services/auth');
|
|
4
4
|
const { registry } = require('../services/registry');
|
|
5
5
|
const { renderMetaConfigs } = require('../views/metaConfigs');
|
|
6
|
-
const { PROXY_NAME } = require('../config');
|
|
6
|
+
const { PROXY_NAME, OPENCODE_CLEAN_UP, ADMIN_PASSWORD } = require('../config');
|
|
7
7
|
|
|
8
8
|
const router = Router();
|
|
9
9
|
|
|
@@ -173,7 +173,7 @@ router.get('/proxy/meta-configs/active/sync', requireAuth, async (req, res) => {
|
|
|
173
173
|
*/
|
|
174
174
|
router.get('/proxy/meta-configs-ui', requireAuth, (req, res) => {
|
|
175
175
|
const data = metaConfig.getAll();
|
|
176
|
-
const html = renderMetaConfigs(PROXY_NAME, data.metaConfigs, data.activeMetaConfig);
|
|
176
|
+
const html = renderMetaConfigs(PROXY_NAME, data.metaConfigs, data.activeMetaConfig, OPENCODE_CLEAN_UP, !!ADMIN_PASSWORD);
|
|
177
177
|
res.type('html').send(html);
|
|
178
178
|
});
|
|
179
179
|
|
package/services/auth.js
CHANGED
|
@@ -89,9 +89,27 @@ function isValidSession(token) {
|
|
|
89
89
|
return true;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* L'attribut `Secure` n'est posé que si la requête est **réellement** en HTTPS.
|
|
94
|
+
*
|
|
95
|
+
* Il l'était auparavant dès qu'un mot de passe était configuré : sur un domaine
|
|
96
|
+
* servi en clair, le navigateur refusait alors d'enregistrer le cookie et la
|
|
97
|
+
* connexion échouait en boucle, sans message. (En `http://localhost` le problème
|
|
98
|
+
* ne se voyait pas : les navigateurs y traitent l'origine comme sûre.)
|
|
99
|
+
*
|
|
100
|
+
* `req` est optionnel pour rester rétrocompatible avec les appelants existants,
|
|
101
|
+
* qui retrouvent alors l'ancien comportement.
|
|
102
|
+
*/
|
|
103
|
+
function isSecureRequest(req) {
|
|
104
|
+
if (!req) return !!ADMIN_PASSWORD;
|
|
105
|
+
if (req.secure) return true;
|
|
106
|
+
const forwarded = String((req.headers && req.headers['x-forwarded-proto']) || '');
|
|
107
|
+
return forwarded.split(',')[0].trim() === 'https';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setSessionCookie(res, token, req) {
|
|
93
111
|
const signed = signToken(token);
|
|
94
|
-
const secure =
|
|
112
|
+
const secure = isSecureRequest(req) ? '; Secure' : '';
|
|
95
113
|
res.setHeader('Set-Cookie', `proxy_session=${encodeURIComponent(signed)}; HttpOnly; SameSite=Lax; Path=/${secure}`);
|
|
96
114
|
}
|
|
97
115
|
|
package/services/portManager.js
CHANGED
|
@@ -14,20 +14,67 @@ function isLocalTarget(target) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Délai au-delà duquel une tentative de connexion locale est abandonnée. */
|
|
18
|
+
const CONNECT_PROBE_TIMEOUT = 300;
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
21
|
+
* Vrai si l'OS nous laisse écouter sur ce port.
|
|
22
|
+
*
|
|
23
|
+
* Attention : ce test **ne suffit pas seul** (voir `isSomeoneListening`).
|
|
19
24
|
*/
|
|
20
|
-
function
|
|
25
|
+
function canBind(port, host) {
|
|
21
26
|
return new Promise((resolve) => {
|
|
22
27
|
const server = net.createServer();
|
|
23
28
|
server.once('error', () => resolve(false));
|
|
24
29
|
server.once('listening', () => {
|
|
25
30
|
server.close(() => resolve(true));
|
|
26
31
|
});
|
|
27
|
-
server.listen(port);
|
|
32
|
+
if (host) server.listen(port, host);
|
|
33
|
+
else server.listen(port);
|
|
28
34
|
});
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Vrai si **quelqu'un accepte déjà** des connexions sur `host:port`.
|
|
39
|
+
*
|
|
40
|
+
* Pourquoi cette seconde sonde : sous Windows, lier `127.0.0.1:<port>` **réussit**
|
|
41
|
+
* alors qu'un process tiers écoute déjà sur `0.0.0.0:<port>` — le bind d'une
|
|
42
|
+
* adresse précise n'entre pas en conflit avec un bind générique d'un autre
|
|
43
|
+
* process. Le test de bind seul déclarait donc libre un port occupé, et le proxy
|
|
44
|
+
* attribuait ce port à une application ; sa sonde de santé interrogeait ensuite
|
|
45
|
+
* le service de l'autre process, qui répondait, et la route ne mourait jamais.
|
|
46
|
+
* Sous Linux le même bind échoue en `EADDRINUSE` : le défaut était invisible
|
|
47
|
+
* hors Windows.
|
|
48
|
+
*
|
|
49
|
+
* Une tentative de connexion, elle, répond la même chose partout. Un timeout est
|
|
50
|
+
* traité comme « occupé » : en local, une connexion sans réponse n'est pas normale.
|
|
51
|
+
*/
|
|
52
|
+
function isSomeoneListening(port, host = '127.0.0.1', timeoutMs = CONNECT_PROBE_TIMEOUT) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const socket = new net.Socket();
|
|
55
|
+
const done = (value) => {
|
|
56
|
+
socket.removeAllListeners();
|
|
57
|
+
socket.destroy();
|
|
58
|
+
resolve(value);
|
|
59
|
+
};
|
|
60
|
+
socket.setTimeout(timeoutMs);
|
|
61
|
+
socket.once('connect', () => done(true));
|
|
62
|
+
socket.once('timeout', () => done(true));
|
|
63
|
+
// ECONNREFUSED (personne n'écoute) et les autres erreurs : port utilisable.
|
|
64
|
+
socket.once('error', () => done(false));
|
|
65
|
+
socket.connect(port, host);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Vérifie si un port est réellement libre au niveau OS : personne ne peut s'y
|
|
71
|
+
* lier **et** personne n'y répond. Les deux conditions sont nécessaires.
|
|
72
|
+
*/
|
|
73
|
+
async function isPortFree(port) {
|
|
74
|
+
if (!(await canBind(port))) return false;
|
|
75
|
+
return !(await isSomeoneListening(port));
|
|
76
|
+
}
|
|
77
|
+
|
|
31
78
|
/**
|
|
32
79
|
* Trouve le prochain port disponible et le réserve atomiquement
|
|
33
80
|
* DOIT être appelé avec le mutex acquis
|
|
@@ -79,4 +126,12 @@ function findRouteForPath(requestPath) {
|
|
|
79
126
|
return null;
|
|
80
127
|
}
|
|
81
128
|
|
|
82
|
-
module.exports = {
|
|
129
|
+
module.exports = {
|
|
130
|
+
isLocalTarget,
|
|
131
|
+
canBind,
|
|
132
|
+
isSomeoneListening,
|
|
133
|
+
isPortFree,
|
|
134
|
+
getNextAvailablePort,
|
|
135
|
+
releasePort,
|
|
136
|
+
findRouteForPath,
|
|
137
|
+
};
|