@imenam/self-register-proxy 1.0.46 → 1.0.48
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 +6 -0
- package/config.js +72 -72
- package/meta-config.js +336 -336
- package/package.json +2 -3
- package/routes/metaConfig.js +180 -180
- package/routes/registration.js +240 -240
- package/services/healthCheck.js +95 -95
- package/services/portManager.js +137 -137
- package/services/proxyRouter.js +99 -99
- package/services/registry.js +41 -41
- package/terminal-server.js +154 -154
package/services/portManager.js
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
const net = require('net');
|
|
2
|
-
const { INTERNAL_PORT_START, INTERNAL_PORT_END } = require('../config');
|
|
3
|
-
const { registry } = require('./registry');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Vérifie si le target pointe vers la machine locale
|
|
7
|
-
*/
|
|
8
|
-
function isLocalTarget(target) {
|
|
9
|
-
try {
|
|
10
|
-
const { hostname } = new URL(target);
|
|
11
|
-
return ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname);
|
|
12
|
-
} catch {
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Délai au-delà duquel une tentative de connexion locale est abandonnée. */
|
|
18
|
-
const CONNECT_PROBE_TIMEOUT = 300;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Vrai si l'OS nous laisse écouter sur ce port.
|
|
22
|
-
*
|
|
23
|
-
* Attention : ce test **ne suffit pas seul** (voir `isSomeoneListening`).
|
|
24
|
-
*/
|
|
25
|
-
function canBind(port, host) {
|
|
26
|
-
return new Promise((resolve) => {
|
|
27
|
-
const server = net.createServer();
|
|
28
|
-
server.once('error', () => resolve(false));
|
|
29
|
-
server.once('listening', () => {
|
|
30
|
-
server.close(() => resolve(true));
|
|
31
|
-
});
|
|
32
|
-
if (host) server.listen(port, host);
|
|
33
|
-
else server.listen(port);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
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
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Trouve le prochain port disponible et le réserve atomiquement
|
|
80
|
-
* DOIT être appelé avec le mutex acquis
|
|
81
|
-
*/
|
|
82
|
-
async function getNextAvailablePort(checkSystem = true) {
|
|
83
|
-
while (true) {
|
|
84
|
-
while (registry.usedPorts.has(registry.nextPort)) {
|
|
85
|
-
registry.nextPort++;
|
|
86
|
-
if (registry.nextPort > INTERNAL_PORT_END) {
|
|
87
|
-
registry.nextPort = INTERNAL_PORT_START;
|
|
88
|
-
if (registry.usedPorts.size >= (INTERNAL_PORT_END - INTERNAL_PORT_START)) {
|
|
89
|
-
throw new Error('No available ports');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const port = registry.nextPort;
|
|
95
|
-
|
|
96
|
-
if (!checkSystem || await isPortFree(port)) {
|
|
97
|
-
registry.usedPorts.add(port);
|
|
98
|
-
registry.nextPort++;
|
|
99
|
-
return port;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
console.log(`[PORT] Port ${port} is in use by another process, skipping...`);
|
|
103
|
-
registry.usedPorts.add(port);
|
|
104
|
-
registry.nextPort++;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Libère un port réservé (en cas d'échec d'enregistrement)
|
|
110
|
-
*/
|
|
111
|
-
function releasePort(port) {
|
|
112
|
-
registry.usedPorts.delete(port);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Trouve la route correspondant à un chemin de requête
|
|
117
|
-
* Tri par longueur décroissante pour matcher le plus spécifique d'abord
|
|
118
|
-
*/
|
|
119
|
-
function findRouteForPath(requestPath) {
|
|
120
|
-
const sortedPaths = [...registry.routes.keys()].sort((a, b) => b.length - a.length);
|
|
121
|
-
for (const routePath of sortedPaths) {
|
|
122
|
-
if (requestPath.startsWith(routePath)) {
|
|
123
|
-
return registry.routes.get(routePath);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
module.exports = {
|
|
130
|
-
isLocalTarget,
|
|
131
|
-
canBind,
|
|
132
|
-
isSomeoneListening,
|
|
133
|
-
isPortFree,
|
|
134
|
-
getNextAvailablePort,
|
|
135
|
-
releasePort,
|
|
136
|
-
findRouteForPath,
|
|
137
|
-
};
|
|
1
|
+
const net = require('net');
|
|
2
|
+
const { INTERNAL_PORT_START, INTERNAL_PORT_END } = require('../config');
|
|
3
|
+
const { registry } = require('./registry');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vérifie si le target pointe vers la machine locale
|
|
7
|
+
*/
|
|
8
|
+
function isLocalTarget(target) {
|
|
9
|
+
try {
|
|
10
|
+
const { hostname } = new URL(target);
|
|
11
|
+
return ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname);
|
|
12
|
+
} catch {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Délai au-delà duquel une tentative de connexion locale est abandonnée. */
|
|
18
|
+
const CONNECT_PROBE_TIMEOUT = 300;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Vrai si l'OS nous laisse écouter sur ce port.
|
|
22
|
+
*
|
|
23
|
+
* Attention : ce test **ne suffit pas seul** (voir `isSomeoneListening`).
|
|
24
|
+
*/
|
|
25
|
+
function canBind(port, host) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const server = net.createServer();
|
|
28
|
+
server.once('error', () => resolve(false));
|
|
29
|
+
server.once('listening', () => {
|
|
30
|
+
server.close(() => resolve(true));
|
|
31
|
+
});
|
|
32
|
+
if (host) server.listen(port, host);
|
|
33
|
+
else server.listen(port);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
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
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Trouve le prochain port disponible et le réserve atomiquement
|
|
80
|
+
* DOIT être appelé avec le mutex acquis
|
|
81
|
+
*/
|
|
82
|
+
async function getNextAvailablePort(checkSystem = true) {
|
|
83
|
+
while (true) {
|
|
84
|
+
while (registry.usedPorts.has(registry.nextPort)) {
|
|
85
|
+
registry.nextPort++;
|
|
86
|
+
if (registry.nextPort > INTERNAL_PORT_END) {
|
|
87
|
+
registry.nextPort = INTERNAL_PORT_START;
|
|
88
|
+
if (registry.usedPorts.size >= (INTERNAL_PORT_END - INTERNAL_PORT_START)) {
|
|
89
|
+
throw new Error('No available ports');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const port = registry.nextPort;
|
|
95
|
+
|
|
96
|
+
if (!checkSystem || await isPortFree(port)) {
|
|
97
|
+
registry.usedPorts.add(port);
|
|
98
|
+
registry.nextPort++;
|
|
99
|
+
return port;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`[PORT] Port ${port} is in use by another process, skipping...`);
|
|
103
|
+
registry.usedPorts.add(port);
|
|
104
|
+
registry.nextPort++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Libère un port réservé (en cas d'échec d'enregistrement)
|
|
110
|
+
*/
|
|
111
|
+
function releasePort(port) {
|
|
112
|
+
registry.usedPorts.delete(port);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Trouve la route correspondant à un chemin de requête
|
|
117
|
+
* Tri par longueur décroissante pour matcher le plus spécifique d'abord
|
|
118
|
+
*/
|
|
119
|
+
function findRouteForPath(requestPath) {
|
|
120
|
+
const sortedPaths = [...registry.routes.keys()].sort((a, b) => b.length - a.length);
|
|
121
|
+
for (const routePath of sortedPaths) {
|
|
122
|
+
if (requestPath.startsWith(routePath)) {
|
|
123
|
+
return registry.routes.get(routePath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
isLocalTarget,
|
|
131
|
+
canBind,
|
|
132
|
+
isSomeoneListening,
|
|
133
|
+
isPortFree,
|
|
134
|
+
getNextAvailablePort,
|
|
135
|
+
releasePort,
|
|
136
|
+
findRouteForPath,
|
|
137
|
+
};
|
package/services/proxyRouter.js
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
2
|
-
const { registry } = require('./registry');
|
|
3
|
-
const { findRouteForPath } = require('./portManager');
|
|
4
|
-
const { unregisterServer } = require('./healthCheck');
|
|
5
|
-
const { requireAuth } = require('./auth');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Échappe les caractères HTML pour éviter les injections XSS
|
|
9
|
-
*/
|
|
10
|
-
function escapeHtml(text) {
|
|
11
|
-
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
12
|
-
return String(text).replace(/[&<>"']/g, char => map[char]);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Middleware de proxy persistant — créé une seule fois pour éviter les fuites mémoire
|
|
17
|
-
*/
|
|
18
|
-
const persistentProxyMiddleware = createProxyMiddleware({
|
|
19
|
-
target: 'http://localhost',
|
|
20
|
-
router: (req) => req.proxyTarget,
|
|
21
|
-
changeOrigin: true,
|
|
22
|
-
pathRewrite: (path, req) => {
|
|
23
|
-
const routePath = req.proxyRoutePath;
|
|
24
|
-
if (routePath && path.startsWith(routePath)) {
|
|
25
|
-
return path.slice(routePath.length) || '/';
|
|
26
|
-
}
|
|
27
|
-
return path;
|
|
28
|
-
},
|
|
29
|
-
on: {
|
|
30
|
-
proxyReq: (proxyReq, req) => {
|
|
31
|
-
if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {
|
|
32
|
-
const bodyData = JSON.stringify(req.body);
|
|
33
|
-
proxyReq.setHeader('Content-Type', 'application/json');
|
|
34
|
-
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
|
|
35
|
-
proxyReq.write(bodyData);
|
|
36
|
-
}
|
|
37
|
-
console.log(`[PROXY] ${req.method} ${req.path} -> ${req.proxyTarget}`);
|
|
38
|
-
},
|
|
39
|
-
error: (err, req, res) => {
|
|
40
|
-
if (res.headersSent) return;
|
|
41
|
-
console.error(`[PROXY ERROR] ${req.path}:`, err.message);
|
|
42
|
-
|
|
43
|
-
if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
|
|
44
|
-
const route = findRouteForPath(req.path);
|
|
45
|
-
if (route) {
|
|
46
|
-
const routePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
|
|
47
|
-
if (routePath) {
|
|
48
|
-
unregisterServer(routePath, `connection error (${err.code})`).catch(() => {});
|
|
49
|
-
console.log(`[PROXY] Auto-unregistered ${routePath} due to connection error`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
res.status(502).json({ error: 'Proxy error' });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Middleware principal de routage vers le proxy
|
|
61
|
-
* Inclut le Smart Routing (Referer Fallback)
|
|
62
|
-
*/
|
|
63
|
-
function routingMiddleware(req, res, next) {
|
|
64
|
-
if (req.path.startsWith('/proxy/')) return next();
|
|
65
|
-
|
|
66
|
-
requireAuth(req, res, () => {
|
|
67
|
-
let route = findRouteForPath(req.path);
|
|
68
|
-
|
|
69
|
-
if (!route && req.get('Referer')) {
|
|
70
|
-
try {
|
|
71
|
-
const refererUrl = new URL(req.get('Referer'));
|
|
72
|
-
if (refererUrl.host === req.get('host')) {
|
|
73
|
-
const refererPath = refererUrl.pathname;
|
|
74
|
-
route = findRouteForPath(refererPath);
|
|
75
|
-
if (route) {
|
|
76
|
-
console.log(`[SMART ROUTE] Inferred ${route.name} from referer: ${refererPath}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
// URL de referer invalide, on ignore
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (!route) {
|
|
85
|
-
return res.status(404).json({
|
|
86
|
-
error: 'No route registered for this path',
|
|
87
|
-
path: req.path,
|
|
88
|
-
hint: 'Register a route with POST /proxy/register or use the UI'
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const target = route.target || 'http://localhost';
|
|
93
|
-
req.proxyTarget = `${target}:${route.port}`;
|
|
94
|
-
req.proxyRoutePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
|
|
95
|
-
persistentProxyMiddleware(req, res, next);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
module.exports = { persistentProxyMiddleware, routingMiddleware, escapeHtml };
|
|
1
|
+
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
2
|
+
const { registry } = require('./registry');
|
|
3
|
+
const { findRouteForPath } = require('./portManager');
|
|
4
|
+
const { unregisterServer } = require('./healthCheck');
|
|
5
|
+
const { requireAuth } = require('./auth');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Échappe les caractères HTML pour éviter les injections XSS
|
|
9
|
+
*/
|
|
10
|
+
function escapeHtml(text) {
|
|
11
|
+
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
12
|
+
return String(text).replace(/[&<>"']/g, char => map[char]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Middleware de proxy persistant — créé une seule fois pour éviter les fuites mémoire
|
|
17
|
+
*/
|
|
18
|
+
const persistentProxyMiddleware = createProxyMiddleware({
|
|
19
|
+
target: 'http://localhost',
|
|
20
|
+
router: (req) => req.proxyTarget,
|
|
21
|
+
changeOrigin: true,
|
|
22
|
+
pathRewrite: (path, req) => {
|
|
23
|
+
const routePath = req.proxyRoutePath;
|
|
24
|
+
if (routePath && path.startsWith(routePath)) {
|
|
25
|
+
return path.slice(routePath.length) || '/';
|
|
26
|
+
}
|
|
27
|
+
return path;
|
|
28
|
+
},
|
|
29
|
+
on: {
|
|
30
|
+
proxyReq: (proxyReq, req) => {
|
|
31
|
+
if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {
|
|
32
|
+
const bodyData = JSON.stringify(req.body);
|
|
33
|
+
proxyReq.setHeader('Content-Type', 'application/json');
|
|
34
|
+
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
|
|
35
|
+
proxyReq.write(bodyData);
|
|
36
|
+
}
|
|
37
|
+
console.log(`[PROXY] ${req.method} ${req.path} -> ${req.proxyTarget}`);
|
|
38
|
+
},
|
|
39
|
+
error: (err, req, res) => {
|
|
40
|
+
if (res.headersSent) return;
|
|
41
|
+
console.error(`[PROXY ERROR] ${req.path}:`, err.message);
|
|
42
|
+
|
|
43
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
|
|
44
|
+
const route = findRouteForPath(req.path);
|
|
45
|
+
if (route) {
|
|
46
|
+
const routePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
|
|
47
|
+
if (routePath) {
|
|
48
|
+
unregisterServer(routePath, `connection error (${err.code})`).catch(() => {});
|
|
49
|
+
console.log(`[PROXY] Auto-unregistered ${routePath} due to connection error`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
res.status(502).json({ error: 'Proxy error' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Middleware principal de routage vers le proxy
|
|
61
|
+
* Inclut le Smart Routing (Referer Fallback)
|
|
62
|
+
*/
|
|
63
|
+
function routingMiddleware(req, res, next) {
|
|
64
|
+
if (req.path.startsWith('/proxy/')) return next();
|
|
65
|
+
|
|
66
|
+
requireAuth(req, res, () => {
|
|
67
|
+
let route = findRouteForPath(req.path);
|
|
68
|
+
|
|
69
|
+
if (!route && req.get('Referer')) {
|
|
70
|
+
try {
|
|
71
|
+
const refererUrl = new URL(req.get('Referer'));
|
|
72
|
+
if (refererUrl.host === req.get('host')) {
|
|
73
|
+
const refererPath = refererUrl.pathname;
|
|
74
|
+
route = findRouteForPath(refererPath);
|
|
75
|
+
if (route) {
|
|
76
|
+
console.log(`[SMART ROUTE] Inferred ${route.name} from referer: ${refererPath}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// URL de referer invalide, on ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!route) {
|
|
85
|
+
return res.status(404).json({
|
|
86
|
+
error: 'No route registered for this path',
|
|
87
|
+
path: req.path,
|
|
88
|
+
hint: 'Register a route with POST /proxy/register or use the UI'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const target = route.target || 'http://localhost';
|
|
93
|
+
req.proxyTarget = `${target}:${route.port}`;
|
|
94
|
+
req.proxyRoutePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
|
|
95
|
+
persistentProxyMiddleware(req, res, next);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { persistentProxyMiddleware, routingMiddleware, escapeHtml };
|
package/services/registry.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
const { INTERNAL_PORT_START } = require('../config');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Simple mutex pour sérialiser les opérations critiques
|
|
5
|
-
* Évite les race conditions lors d'inscriptions simultanées
|
|
6
|
-
*/
|
|
7
|
-
class Mutex {
|
|
8
|
-
constructor() {
|
|
9
|
-
this._locked = false;
|
|
10
|
-
this._queue = [];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async acquire() {
|
|
14
|
-
if (!this._locked) {
|
|
15
|
-
this._locked = true;
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
return new Promise(resolve => {
|
|
19
|
-
this._queue.push(resolve);
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
release() {
|
|
24
|
-
if (this._queue.length > 0) {
|
|
25
|
-
const next = this._queue.shift();
|
|
26
|
-
next();
|
|
27
|
-
} else {
|
|
28
|
-
this._locked = false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const registry = {
|
|
34
|
-
routes: new Map(), // path -> { port, target, name, healthCheck, group, registeredAt }
|
|
35
|
-
usedPorts: new Set(), // Ports déjà attribués
|
|
36
|
-
nextPort: INTERNAL_PORT_START
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const mutex = new Mutex();
|
|
40
|
-
|
|
41
|
-
module.exports = { registry, mutex };
|
|
1
|
+
const { INTERNAL_PORT_START } = require('../config');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple mutex pour sérialiser les opérations critiques
|
|
5
|
+
* Évite les race conditions lors d'inscriptions simultanées
|
|
6
|
+
*/
|
|
7
|
+
class Mutex {
|
|
8
|
+
constructor() {
|
|
9
|
+
this._locked = false;
|
|
10
|
+
this._queue = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async acquire() {
|
|
14
|
+
if (!this._locked) {
|
|
15
|
+
this._locked = true;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
return new Promise(resolve => {
|
|
19
|
+
this._queue.push(resolve);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
release() {
|
|
24
|
+
if (this._queue.length > 0) {
|
|
25
|
+
const next = this._queue.shift();
|
|
26
|
+
next();
|
|
27
|
+
} else {
|
|
28
|
+
this._locked = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const registry = {
|
|
34
|
+
routes: new Map(), // path -> { port, target, name, healthCheck, group, registeredAt }
|
|
35
|
+
usedPorts: new Set(), // Ports déjà attribués
|
|
36
|
+
nextPort: INTERNAL_PORT_START
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const mutex = new Mutex();
|
|
40
|
+
|
|
41
|
+
module.exports = { registry, mutex };
|