@imenam/self-register-proxy 1.0.47 → 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/routes/registration.js
CHANGED
|
@@ -1,240 +1,240 @@
|
|
|
1
|
-
const { Router } = require('express');
|
|
2
|
-
const { requireLocalOnly, requireAuth } = require('../services/auth');
|
|
3
|
-
const { registry, mutex } = require('../services/registry');
|
|
4
|
-
const { getNextAvailablePort, releasePort, isLocalTarget, isPortFree, findRouteForPath } = require('../services/portManager');
|
|
5
|
-
const { checkServerHealth, unregisterServer } = require('../services/healthCheck');
|
|
6
|
-
const { INTERNAL_PORT_START, INTERNAL_PORT_END } = require('../config');
|
|
7
|
-
|
|
8
|
-
const router = Router();
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Logique commune d'enregistrement (auto et manuel)
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Normalise un nom de groupe : trim, et chaîne vide => undefined (non groupé).
|
|
15
|
-
*/
|
|
16
|
-
function normalizeGroup(group) {
|
|
17
|
-
if (typeof group !== 'string') return undefined;
|
|
18
|
-
const trimmed = group.trim();
|
|
19
|
-
return trimmed === '' ? undefined : trimmed;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort }) {
|
|
23
|
-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
24
|
-
const normalizedGroup = normalizeGroup(group);
|
|
25
|
-
|
|
26
|
-
let finalTarget = target || 'http://localhost';
|
|
27
|
-
if (!finalTarget.startsWith('http://') && !finalTarget.startsWith('https://')) {
|
|
28
|
-
finalTarget = `http://${finalTarget}`;
|
|
29
|
-
}
|
|
30
|
-
if (finalTarget.endsWith('/')) {
|
|
31
|
-
finalTarget = finalTarget.slice(0, -1);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
await mutex.acquire();
|
|
35
|
-
|
|
36
|
-
let assignedPort = null;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
if (registry.routes.has(normalizedPath)) {
|
|
40
|
-
const existing = registry.routes.get(normalizedPath);
|
|
41
|
-
|
|
42
|
-
mutex.release();
|
|
43
|
-
const isAlive = await checkServerHealth(normalizedPath, existing);
|
|
44
|
-
await mutex.acquire();
|
|
45
|
-
|
|
46
|
-
if (!isAlive) {
|
|
47
|
-
console.log(`[REGISTER] Path ${normalizedPath} was occupied by a dead server. Overwriting...`);
|
|
48
|
-
registry.usedPorts.delete(existing.port);
|
|
49
|
-
registry.routes.delete(normalizedPath);
|
|
50
|
-
} else {
|
|
51
|
-
throw new Error('Path already registered');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!autoAssignPort) {
|
|
56
|
-
if (!port) throw new Error('Port is required for manual registration');
|
|
57
|
-
const manualPort = parseInt(port, 10);
|
|
58
|
-
|
|
59
|
-
if (registry.usedPorts.has(manualPort)) {
|
|
60
|
-
throw new Error(`Port ${manualPort} already in use`);
|
|
61
|
-
}
|
|
62
|
-
if (isLocalTarget(finalTarget) && !await isPortFree(manualPort)) {
|
|
63
|
-
throw new Error(`Port ${manualPort} is already in use by another process`);
|
|
64
|
-
}
|
|
65
|
-
assignedPort = manualPort;
|
|
66
|
-
registry.usedPorts.add(assignedPort);
|
|
67
|
-
} else {
|
|
68
|
-
assignedPort = await getNextAvailablePort(isLocalTarget(finalTarget));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
registry.routes.set(normalizedPath, {
|
|
72
|
-
port: assignedPort,
|
|
73
|
-
target: finalTarget,
|
|
74
|
-
name: name || normalizedPath,
|
|
75
|
-
healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
|
|
76
|
-
group: normalizedGroup,
|
|
77
|
-
registeredAt: new Date().toISOString()
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]${normalizedGroup ? ` [Group: ${normalizedGroup}]` : ''}`);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
success: true,
|
|
84
|
-
path: normalizedPath,
|
|
85
|
-
port: assignedPort,
|
|
86
|
-
target: finalTarget,
|
|
87
|
-
name: name || normalizedPath,
|
|
88
|
-
healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
|
|
89
|
-
group: normalizedGroup,
|
|
90
|
-
message: `Route registered. Target: ${finalTarget}:${assignedPort}`
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (assignedPort !== null) {
|
|
95
|
-
if (!registry.routes.has(normalizedPath) || registry.routes.get(normalizedPath).port !== assignedPort) {
|
|
96
|
-
releasePort(assignedPort);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
} finally {
|
|
101
|
-
mutex.release();
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* POST /proxy/register
|
|
107
|
-
* Enregistrement automatique (rétro-compatibilité)
|
|
108
|
-
*/
|
|
109
|
-
router.post('/proxy/register', requireLocalOnly, async (req, res) => {
|
|
110
|
-
const { path, name, target, healthCheck, group } = req.body;
|
|
111
|
-
|
|
112
|
-
if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const result = await registerRouteInternal({ path, name, target, healthCheck, group, autoAssignPort: true });
|
|
116
|
-
res.json(result);
|
|
117
|
-
} catch (error) {
|
|
118
|
-
console.error('[REGISTER AUTO ERROR]', error.message);
|
|
119
|
-
const status = error.message.includes('already') ? 409 : 500;
|
|
120
|
-
res.status(status).json({ success: false, error: error.message });
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* POST /proxy/register-manual
|
|
126
|
-
* Enregistrement manuel via l'interface graphique (port obligatoire)
|
|
127
|
-
*/
|
|
128
|
-
router.post('/proxy/register-manual', requireAuth, async (req, res) => {
|
|
129
|
-
const { path, name, port, target, healthCheck, group } = req.body;
|
|
130
|
-
|
|
131
|
-
if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
|
|
132
|
-
if (!port) return res.status(400).json({ success: false, error: 'Port is required for manual registration' });
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const result = await registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort: false });
|
|
136
|
-
res.json(result);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.error('[REGISTER MANUAL ERROR]', error.message);
|
|
139
|
-
const status = error.message.includes('already') ? 409 : 500;
|
|
140
|
-
res.status(status).json({ success: false, error: error.message });
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* DELETE /proxy/unregister
|
|
146
|
-
* Supprime une route
|
|
147
|
-
*/
|
|
148
|
-
router.delete('/proxy/unregister', requireLocalOnly, async (req, res) => {
|
|
149
|
-
const { path } = req.body;
|
|
150
|
-
|
|
151
|
-
if (!path) {
|
|
152
|
-
return res.status(400).json({ success: false, error: 'Path is required' });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
156
|
-
|
|
157
|
-
await mutex.acquire();
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
if (!registry.routes.has(normalizedPath)) {
|
|
161
|
-
return res.status(404).json({ success: false, error: 'Path not found' });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const route = registry.routes.get(normalizedPath);
|
|
165
|
-
registry.routes.delete(normalizedPath);
|
|
166
|
-
registry.usedPorts.delete(route.port);
|
|
167
|
-
|
|
168
|
-
console.log(`[UNREGISTER] ${normalizedPath} (was on port ${route.port})`);
|
|
169
|
-
|
|
170
|
-
res.json({ success: true, path: normalizedPath, freedPort: route.port });
|
|
171
|
-
|
|
172
|
-
} catch (error) {
|
|
173
|
-
console.error('[UNREGISTER ERROR]', error.message);
|
|
174
|
-
res.status(500).json({ success: false, error: error.message });
|
|
175
|
-
} finally {
|
|
176
|
-
mutex.release();
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* GET /proxy/routes
|
|
182
|
-
* Liste toutes les routes enregistrées
|
|
183
|
-
*/
|
|
184
|
-
router.get('/proxy/routes', requireAuth, (req, res) => {
|
|
185
|
-
const routes = [];
|
|
186
|
-
registry.routes.forEach((value, path) => {
|
|
187
|
-
routes.push({
|
|
188
|
-
path,
|
|
189
|
-
port: value.port,
|
|
190
|
-
target: value.target || 'http://localhost',
|
|
191
|
-
name: value.name,
|
|
192
|
-
healthCheck: value.healthCheck,
|
|
193
|
-
group: value.group || null,
|
|
194
|
-
registeredAt: value.registeredAt,
|
|
195
|
-
fullTarget: `${value.target || 'http://localhost'}:${value.port}`
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
res.json({
|
|
200
|
-
count: routes.length,
|
|
201
|
-
routes,
|
|
202
|
-
availablePorts: (INTERNAL_PORT_END - INTERNAL_PORT_START) - registry.usedPorts.size
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* POST /proxy/check
|
|
208
|
-
* Déclenche manuellement un health check pour une route spécifique
|
|
209
|
-
*/
|
|
210
|
-
router.post('/proxy/check', requireAuth, async (req, res) => {
|
|
211
|
-
const { path } = req.body;
|
|
212
|
-
|
|
213
|
-
if (!path) {
|
|
214
|
-
return res.status(400).json({ success: false, error: 'Path is required' });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
218
|
-
const route = registry.routes.get(normalizedPath);
|
|
219
|
-
|
|
220
|
-
if (!route) {
|
|
221
|
-
return res.status(404).json({ success: false, error: 'Path not found' });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
console.log(`[MANUAL HEALTH CHECK] Checking ${normalizedPath}...`);
|
|
225
|
-
const isHealthy = await checkServerHealth(normalizedPath, route);
|
|
226
|
-
|
|
227
|
-
if (!isHealthy) {
|
|
228
|
-
await unregisterServer(normalizedPath);
|
|
229
|
-
return res.json({
|
|
230
|
-
success: true,
|
|
231
|
-
path: normalizedPath,
|
|
232
|
-
healthy: false,
|
|
233
|
-
message: 'Server was unhealthy and has been removed'
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
res.json({ success: true, path: normalizedPath, healthy: true, message: 'Server is healthy' });
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
module.exports = router;
|
|
1
|
+
const { Router } = require('express');
|
|
2
|
+
const { requireLocalOnly, requireAuth } = require('../services/auth');
|
|
3
|
+
const { registry, mutex } = require('../services/registry');
|
|
4
|
+
const { getNextAvailablePort, releasePort, isLocalTarget, isPortFree, findRouteForPath } = require('../services/portManager');
|
|
5
|
+
const { checkServerHealth, unregisterServer } = require('../services/healthCheck');
|
|
6
|
+
const { INTERNAL_PORT_START, INTERNAL_PORT_END } = require('../config');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Logique commune d'enregistrement (auto et manuel)
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Normalise un nom de groupe : trim, et chaîne vide => undefined (non groupé).
|
|
15
|
+
*/
|
|
16
|
+
function normalizeGroup(group) {
|
|
17
|
+
if (typeof group !== 'string') return undefined;
|
|
18
|
+
const trimmed = group.trim();
|
|
19
|
+
return trimmed === '' ? undefined : trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort }) {
|
|
23
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
24
|
+
const normalizedGroup = normalizeGroup(group);
|
|
25
|
+
|
|
26
|
+
let finalTarget = target || 'http://localhost';
|
|
27
|
+
if (!finalTarget.startsWith('http://') && !finalTarget.startsWith('https://')) {
|
|
28
|
+
finalTarget = `http://${finalTarget}`;
|
|
29
|
+
}
|
|
30
|
+
if (finalTarget.endsWith('/')) {
|
|
31
|
+
finalTarget = finalTarget.slice(0, -1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await mutex.acquire();
|
|
35
|
+
|
|
36
|
+
let assignedPort = null;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (registry.routes.has(normalizedPath)) {
|
|
40
|
+
const existing = registry.routes.get(normalizedPath);
|
|
41
|
+
|
|
42
|
+
mutex.release();
|
|
43
|
+
const isAlive = await checkServerHealth(normalizedPath, existing);
|
|
44
|
+
await mutex.acquire();
|
|
45
|
+
|
|
46
|
+
if (!isAlive) {
|
|
47
|
+
console.log(`[REGISTER] Path ${normalizedPath} was occupied by a dead server. Overwriting...`);
|
|
48
|
+
registry.usedPorts.delete(existing.port);
|
|
49
|
+
registry.routes.delete(normalizedPath);
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error('Path already registered');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!autoAssignPort) {
|
|
56
|
+
if (!port) throw new Error('Port is required for manual registration');
|
|
57
|
+
const manualPort = parseInt(port, 10);
|
|
58
|
+
|
|
59
|
+
if (registry.usedPorts.has(manualPort)) {
|
|
60
|
+
throw new Error(`Port ${manualPort} already in use`);
|
|
61
|
+
}
|
|
62
|
+
if (isLocalTarget(finalTarget) && !await isPortFree(manualPort)) {
|
|
63
|
+
throw new Error(`Port ${manualPort} is already in use by another process`);
|
|
64
|
+
}
|
|
65
|
+
assignedPort = manualPort;
|
|
66
|
+
registry.usedPorts.add(assignedPort);
|
|
67
|
+
} else {
|
|
68
|
+
assignedPort = await getNextAvailablePort(isLocalTarget(finalTarget));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
registry.routes.set(normalizedPath, {
|
|
72
|
+
port: assignedPort,
|
|
73
|
+
target: finalTarget,
|
|
74
|
+
name: name || normalizedPath,
|
|
75
|
+
healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
|
|
76
|
+
group: normalizedGroup,
|
|
77
|
+
registeredAt: new Date().toISOString()
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]${normalizedGroup ? ` [Group: ${normalizedGroup}]` : ''}`);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
path: normalizedPath,
|
|
85
|
+
port: assignedPort,
|
|
86
|
+
target: finalTarget,
|
|
87
|
+
name: name || normalizedPath,
|
|
88
|
+
healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
|
|
89
|
+
group: normalizedGroup,
|
|
90
|
+
message: `Route registered. Target: ${finalTarget}:${assignedPort}`
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (assignedPort !== null) {
|
|
95
|
+
if (!registry.routes.has(normalizedPath) || registry.routes.get(normalizedPath).port !== assignedPort) {
|
|
96
|
+
releasePort(assignedPort);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
} finally {
|
|
101
|
+
mutex.release();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* POST /proxy/register
|
|
107
|
+
* Enregistrement automatique (rétro-compatibilité)
|
|
108
|
+
*/
|
|
109
|
+
router.post('/proxy/register', requireLocalOnly, async (req, res) => {
|
|
110
|
+
const { path, name, target, healthCheck, group } = req.body;
|
|
111
|
+
|
|
112
|
+
if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const result = await registerRouteInternal({ path, name, target, healthCheck, group, autoAssignPort: true });
|
|
116
|
+
res.json(result);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('[REGISTER AUTO ERROR]', error.message);
|
|
119
|
+
const status = error.message.includes('already') ? 409 : 500;
|
|
120
|
+
res.status(status).json({ success: false, error: error.message });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* POST /proxy/register-manual
|
|
126
|
+
* Enregistrement manuel via l'interface graphique (port obligatoire)
|
|
127
|
+
*/
|
|
128
|
+
router.post('/proxy/register-manual', requireAuth, async (req, res) => {
|
|
129
|
+
const { path, name, port, target, healthCheck, group } = req.body;
|
|
130
|
+
|
|
131
|
+
if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
|
|
132
|
+
if (!port) return res.status(400).json({ success: false, error: 'Port is required for manual registration' });
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const result = await registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort: false });
|
|
136
|
+
res.json(result);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('[REGISTER MANUAL ERROR]', error.message);
|
|
139
|
+
const status = error.message.includes('already') ? 409 : 500;
|
|
140
|
+
res.status(status).json({ success: false, error: error.message });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* DELETE /proxy/unregister
|
|
146
|
+
* Supprime une route
|
|
147
|
+
*/
|
|
148
|
+
router.delete('/proxy/unregister', requireLocalOnly, async (req, res) => {
|
|
149
|
+
const { path } = req.body;
|
|
150
|
+
|
|
151
|
+
if (!path) {
|
|
152
|
+
return res.status(400).json({ success: false, error: 'Path is required' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
156
|
+
|
|
157
|
+
await mutex.acquire();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
if (!registry.routes.has(normalizedPath)) {
|
|
161
|
+
return res.status(404).json({ success: false, error: 'Path not found' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const route = registry.routes.get(normalizedPath);
|
|
165
|
+
registry.routes.delete(normalizedPath);
|
|
166
|
+
registry.usedPorts.delete(route.port);
|
|
167
|
+
|
|
168
|
+
console.log(`[UNREGISTER] ${normalizedPath} (was on port ${route.port})`);
|
|
169
|
+
|
|
170
|
+
res.json({ success: true, path: normalizedPath, freedPort: route.port });
|
|
171
|
+
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('[UNREGISTER ERROR]', error.message);
|
|
174
|
+
res.status(500).json({ success: false, error: error.message });
|
|
175
|
+
} finally {
|
|
176
|
+
mutex.release();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* GET /proxy/routes
|
|
182
|
+
* Liste toutes les routes enregistrées
|
|
183
|
+
*/
|
|
184
|
+
router.get('/proxy/routes', requireAuth, (req, res) => {
|
|
185
|
+
const routes = [];
|
|
186
|
+
registry.routes.forEach((value, path) => {
|
|
187
|
+
routes.push({
|
|
188
|
+
path,
|
|
189
|
+
port: value.port,
|
|
190
|
+
target: value.target || 'http://localhost',
|
|
191
|
+
name: value.name,
|
|
192
|
+
healthCheck: value.healthCheck,
|
|
193
|
+
group: value.group || null,
|
|
194
|
+
registeredAt: value.registeredAt,
|
|
195
|
+
fullTarget: `${value.target || 'http://localhost'}:${value.port}`
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
res.json({
|
|
200
|
+
count: routes.length,
|
|
201
|
+
routes,
|
|
202
|
+
availablePorts: (INTERNAL_PORT_END - INTERNAL_PORT_START) - registry.usedPorts.size
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* POST /proxy/check
|
|
208
|
+
* Déclenche manuellement un health check pour une route spécifique
|
|
209
|
+
*/
|
|
210
|
+
router.post('/proxy/check', requireAuth, async (req, res) => {
|
|
211
|
+
const { path } = req.body;
|
|
212
|
+
|
|
213
|
+
if (!path) {
|
|
214
|
+
return res.status(400).json({ success: false, error: 'Path is required' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
218
|
+
const route = registry.routes.get(normalizedPath);
|
|
219
|
+
|
|
220
|
+
if (!route) {
|
|
221
|
+
return res.status(404).json({ success: false, error: 'Path not found' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`[MANUAL HEALTH CHECK] Checking ${normalizedPath}...`);
|
|
225
|
+
const isHealthy = await checkServerHealth(normalizedPath, route);
|
|
226
|
+
|
|
227
|
+
if (!isHealthy) {
|
|
228
|
+
await unregisterServer(normalizedPath);
|
|
229
|
+
return res.json({
|
|
230
|
+
success: true,
|
|
231
|
+
path: normalizedPath,
|
|
232
|
+
healthy: false,
|
|
233
|
+
message: 'Server was unhealthy and has been removed'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
res.json({ success: true, path: normalizedPath, healthy: true, message: 'Server is healthy' });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
module.exports = router;
|
package/services/healthCheck.js
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
// NOTE: le timeout est appliqué globalement, pas par route individuelle
|
|
2
|
-
const { HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT, HEALTH_CHECK_GRACE_PERIOD } = require('../config');
|
|
3
|
-
const { registry, mutex } = require('./registry');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Vérifie la santé d'un serveur enregistré
|
|
7
|
-
*/
|
|
8
|
-
async function checkServerHealth(path, route) {
|
|
9
|
-
const target = route.target || 'http://localhost';
|
|
10
|
-
const healthUrl = `${target}:${route.port}/proxy/health`;
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const controller = new AbortController();
|
|
14
|
-
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
|
|
15
|
-
const response = await fetch(healthUrl, { method: 'GET', signal: controller.signal });
|
|
16
|
-
clearTimeout(timeoutId);
|
|
17
|
-
return response.status === 200;
|
|
18
|
-
} catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Désenregistre un serveur via le mutex
|
|
25
|
-
*/
|
|
26
|
-
async function unregisterServer(path, reason = 'unhealthy') {
|
|
27
|
-
await mutex.acquire();
|
|
28
|
-
try {
|
|
29
|
-
const route = registry.routes.get(path);
|
|
30
|
-
if (route) {
|
|
31
|
-
registry.routes.delete(path);
|
|
32
|
-
registry.usedPorts.delete(route.port);
|
|
33
|
-
console.log(`[UNREGISTER] ${path} (was on port ${route.port}) - reason: ${reason}`);
|
|
34
|
-
}
|
|
35
|
-
} finally {
|
|
36
|
-
mutex.release();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Vérifie si un serveur est encore dans sa période de grâce
|
|
42
|
-
*/
|
|
43
|
-
function isInGracePeriod(route) {
|
|
44
|
-
const registeredAt = new Date(route.registeredAt).getTime();
|
|
45
|
-
return (Date.now() - registeredAt) < HEALTH_CHECK_GRACE_PERIOD;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Effectue un health check sur tous les serveurs enregistrés
|
|
50
|
-
*/
|
|
51
|
-
async function performHealthChecks() {
|
|
52
|
-
if (registry.routes.size === 0) return;
|
|
53
|
-
|
|
54
|
-
const checks = [];
|
|
55
|
-
let skippedCount = 0;
|
|
56
|
-
|
|
57
|
-
for (const [path, route] of registry.routes.entries()) {
|
|
58
|
-
if (route.healthCheck === false) continue;
|
|
59
|
-
if (isInGracePeriod(route)) { skippedCount++; continue; }
|
|
60
|
-
checks.push(
|
|
61
|
-
checkServerHealth(path, route).then(isHealthy => ({ path, route, isHealthy }))
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (checks.length === 0) {
|
|
66
|
-
if (skippedCount > 0) {
|
|
67
|
-
console.log(`[HEALTH CHECK] ${skippedCount} server(s) in grace period, skipping checks`);
|
|
68
|
-
}
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
console.log(`[HEALTH CHECK] Checking ${checks.length} server(s)...${skippedCount > 0 ? ` (${skippedCount} in grace period)` : ''}`);
|
|
73
|
-
|
|
74
|
-
const results = await Promise.all(checks);
|
|
75
|
-
|
|
76
|
-
for (const { path, isHealthy } of results) {
|
|
77
|
-
if (!isHealthy) await unregisterServer(path, 'failed health check');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const healthyCount = results.filter(r => r.isHealthy).length;
|
|
81
|
-
const unhealthyCount = results.length - healthyCount;
|
|
82
|
-
if (unhealthyCount > 0) {
|
|
83
|
-
console.log(`[HEALTH CHECK] Complete: ${healthyCount} healthy, ${unhealthyCount} removed`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Démarre le polling de health check
|
|
89
|
-
*/
|
|
90
|
-
function startHealthCheckPolling() {
|
|
91
|
-
console.log(`[HEALTH CHECK] Starting polling every ${HEALTH_CHECK_INTERVAL}ms`);
|
|
92
|
-
setInterval(performHealthChecks, HEALTH_CHECK_INTERVAL);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
module.exports = { checkServerHealth, unregisterServer, isInGracePeriod, performHealthChecks, startHealthCheckPolling };
|
|
1
|
+
// NOTE: le timeout est appliqué globalement, pas par route individuelle
|
|
2
|
+
const { HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT, HEALTH_CHECK_GRACE_PERIOD } = require('../config');
|
|
3
|
+
const { registry, mutex } = require('./registry');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vérifie la santé d'un serveur enregistré
|
|
7
|
+
*/
|
|
8
|
+
async function checkServerHealth(path, route) {
|
|
9
|
+
const target = route.target || 'http://localhost';
|
|
10
|
+
const healthUrl = `${target}:${route.port}/proxy/health`;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
|
|
15
|
+
const response = await fetch(healthUrl, { method: 'GET', signal: controller.signal });
|
|
16
|
+
clearTimeout(timeoutId);
|
|
17
|
+
return response.status === 200;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Désenregistre un serveur via le mutex
|
|
25
|
+
*/
|
|
26
|
+
async function unregisterServer(path, reason = 'unhealthy') {
|
|
27
|
+
await mutex.acquire();
|
|
28
|
+
try {
|
|
29
|
+
const route = registry.routes.get(path);
|
|
30
|
+
if (route) {
|
|
31
|
+
registry.routes.delete(path);
|
|
32
|
+
registry.usedPorts.delete(route.port);
|
|
33
|
+
console.log(`[UNREGISTER] ${path} (was on port ${route.port}) - reason: ${reason}`);
|
|
34
|
+
}
|
|
35
|
+
} finally {
|
|
36
|
+
mutex.release();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Vérifie si un serveur est encore dans sa période de grâce
|
|
42
|
+
*/
|
|
43
|
+
function isInGracePeriod(route) {
|
|
44
|
+
const registeredAt = new Date(route.registeredAt).getTime();
|
|
45
|
+
return (Date.now() - registeredAt) < HEALTH_CHECK_GRACE_PERIOD;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Effectue un health check sur tous les serveurs enregistrés
|
|
50
|
+
*/
|
|
51
|
+
async function performHealthChecks() {
|
|
52
|
+
if (registry.routes.size === 0) return;
|
|
53
|
+
|
|
54
|
+
const checks = [];
|
|
55
|
+
let skippedCount = 0;
|
|
56
|
+
|
|
57
|
+
for (const [path, route] of registry.routes.entries()) {
|
|
58
|
+
if (route.healthCheck === false) continue;
|
|
59
|
+
if (isInGracePeriod(route)) { skippedCount++; continue; }
|
|
60
|
+
checks.push(
|
|
61
|
+
checkServerHealth(path, route).then(isHealthy => ({ path, route, isHealthy }))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (checks.length === 0) {
|
|
66
|
+
if (skippedCount > 0) {
|
|
67
|
+
console.log(`[HEALTH CHECK] ${skippedCount} server(s) in grace period, skipping checks`);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`[HEALTH CHECK] Checking ${checks.length} server(s)...${skippedCount > 0 ? ` (${skippedCount} in grace period)` : ''}`);
|
|
73
|
+
|
|
74
|
+
const results = await Promise.all(checks);
|
|
75
|
+
|
|
76
|
+
for (const { path, isHealthy } of results) {
|
|
77
|
+
if (!isHealthy) await unregisterServer(path, 'failed health check');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const healthyCount = results.filter(r => r.isHealthy).length;
|
|
81
|
+
const unhealthyCount = results.length - healthyCount;
|
|
82
|
+
if (unhealthyCount > 0) {
|
|
83
|
+
console.log(`[HEALTH CHECK] Complete: ${healthyCount} healthy, ${unhealthyCount} removed`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Démarre le polling de health check
|
|
89
|
+
*/
|
|
90
|
+
function startHealthCheckPolling() {
|
|
91
|
+
console.log(`[HEALTH CHECK] Starting polling every ${HEALTH_CHECK_INTERVAL}ms`);
|
|
92
|
+
setInterval(performHealthChecks, HEALTH_CHECK_INTERVAL);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { checkServerHealth, unregisterServer, isInGracePeriod, performHealthChecks, startHealthCheckPolling };
|