@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.
@@ -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
+ };
@@ -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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
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,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 };