@imenam/self-register-proxy 1.0.41 → 1.0.43
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 +3 -1
- package/package.json +1 -1
- package/routes/dashboard.js +1 -0
- package/routes/metaConfig.js +2 -2
- package/routes/registration.js +19 -6
- package/services/proxyRouter.js +99 -99
- package/services/registry.js +1 -1
- package/views/dashboard.js +344 -696
- 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
|
@@ -82,7 +82,8 @@ const response = await fetch('http://localhost:3000/proxy/register', {
|
|
|
82
82
|
headers: { 'Content-Type': 'application/json' },
|
|
83
83
|
body: JSON.stringify({
|
|
84
84
|
path: '/myapp', // Chemin d'accès via le proxy
|
|
85
|
-
name: 'My App'
|
|
85
|
+
name: 'My App', // Nom de l'application (optionnel)
|
|
86
|
+
group: 'Google' // Section repliable du dashboard (optionnel)
|
|
86
87
|
})
|
|
87
88
|
});
|
|
88
89
|
|
|
@@ -163,6 +164,7 @@ Enregistre une nouvelle route et attribue un port.
|
|
|
163
164
|
| `path` | string | ✅ | Chemin URL pour accéder à l'app |
|
|
164
165
|
| `name` | string | ❌ | Nom descriptif |
|
|
165
166
|
| `port` | number | ❌ | Port spécifique (sinon auto-attribué) |
|
|
167
|
+
| `group` | string | ❌ | Section repliable du dashboard (repliée par défaut). Vide/absent ⇒ section « Non groupées » |
|
|
166
168
|
|
|
167
169
|
**Response (201):**
|
|
168
170
|
```json
|
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.43",
|
|
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/routes/dashboard.js
CHANGED
|
@@ -77,6 +77,7 @@ router.get('/', requireAuth, (req, res) => {
|
|
|
77
77
|
target: value.target || 'http://localhost',
|
|
78
78
|
name: value.name,
|
|
79
79
|
healthCheck: value.healthCheck,
|
|
80
|
+
group: value.group || null,
|
|
80
81
|
registeredAt: value.registeredAt,
|
|
81
82
|
fullTarget: `${value.target || 'http://localhost'}:${value.port}`
|
|
82
83
|
});
|
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/routes/registration.js
CHANGED
|
@@ -10,8 +10,18 @@ const router = Router();
|
|
|
10
10
|
/**
|
|
11
11
|
* Logique commune d'enregistrement (auto et manuel)
|
|
12
12
|
*/
|
|
13
|
-
|
|
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 }) {
|
|
14
23
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
24
|
+
const normalizedGroup = normalizeGroup(group);
|
|
15
25
|
|
|
16
26
|
let finalTarget = target || 'http://localhost';
|
|
17
27
|
if (!finalTarget.startsWith('http://') && !finalTarget.startsWith('https://')) {
|
|
@@ -63,10 +73,11 @@ async function registerRouteInternal({ path, name, port, target, healthCheck, au
|
|
|
63
73
|
target: finalTarget,
|
|
64
74
|
name: name || normalizedPath,
|
|
65
75
|
healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
|
|
76
|
+
group: normalizedGroup,
|
|
66
77
|
registeredAt: new Date().toISOString()
|
|
67
78
|
});
|
|
68
79
|
|
|
69
|
-
console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]`);
|
|
80
|
+
console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]${normalizedGroup ? ` [Group: ${normalizedGroup}]` : ''}`);
|
|
70
81
|
|
|
71
82
|
return {
|
|
72
83
|
success: true,
|
|
@@ -75,6 +86,7 @@ async function registerRouteInternal({ path, name, port, target, healthCheck, au
|
|
|
75
86
|
target: finalTarget,
|
|
76
87
|
name: name || normalizedPath,
|
|
77
88
|
healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
|
|
89
|
+
group: normalizedGroup,
|
|
78
90
|
message: `Route registered. Target: ${finalTarget}:${assignedPort}`
|
|
79
91
|
};
|
|
80
92
|
|
|
@@ -95,12 +107,12 @@ async function registerRouteInternal({ path, name, port, target, healthCheck, au
|
|
|
95
107
|
* Enregistrement automatique (rétro-compatibilité)
|
|
96
108
|
*/
|
|
97
109
|
router.post('/proxy/register', requireLocalOnly, async (req, res) => {
|
|
98
|
-
const { path, name, target, healthCheck } = req.body;
|
|
110
|
+
const { path, name, target, healthCheck, group } = req.body;
|
|
99
111
|
|
|
100
112
|
if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
|
|
101
113
|
|
|
102
114
|
try {
|
|
103
|
-
const result = await registerRouteInternal({ path, name, target, healthCheck, autoAssignPort: true });
|
|
115
|
+
const result = await registerRouteInternal({ path, name, target, healthCheck, group, autoAssignPort: true });
|
|
104
116
|
res.json(result);
|
|
105
117
|
} catch (error) {
|
|
106
118
|
console.error('[REGISTER AUTO ERROR]', error.message);
|
|
@@ -114,13 +126,13 @@ router.post('/proxy/register', requireLocalOnly, async (req, res) => {
|
|
|
114
126
|
* Enregistrement manuel via l'interface graphique (port obligatoire)
|
|
115
127
|
*/
|
|
116
128
|
router.post('/proxy/register-manual', requireAuth, async (req, res) => {
|
|
117
|
-
const { path, name, port, target, healthCheck } = req.body;
|
|
129
|
+
const { path, name, port, target, healthCheck, group } = req.body;
|
|
118
130
|
|
|
119
131
|
if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
|
|
120
132
|
if (!port) return res.status(400).json({ success: false, error: 'Port is required for manual registration' });
|
|
121
133
|
|
|
122
134
|
try {
|
|
123
|
-
const result = await registerRouteInternal({ path, name, port, target, healthCheck, autoAssignPort: false });
|
|
135
|
+
const result = await registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort: false });
|
|
124
136
|
res.json(result);
|
|
125
137
|
} catch (error) {
|
|
126
138
|
console.error('[REGISTER MANUAL ERROR]', error.message);
|
|
@@ -178,6 +190,7 @@ router.get('/proxy/routes', requireAuth, (req, res) => {
|
|
|
178
190
|
target: value.target || 'http://localhost',
|
|
179
191
|
name: value.name,
|
|
180
192
|
healthCheck: value.healthCheck,
|
|
193
|
+
group: value.group || null,
|
|
181
194
|
registeredAt: value.registeredAt,
|
|
182
195
|
fullTarget: `${value.target || 'http://localhost'}:${value.port}`
|
|
183
196
|
});
|
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
|
@@ -31,7 +31,7 @@ class Mutex {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const registry = {
|
|
34
|
-
routes: new Map(), // path -> { port, target, name, healthCheck, registeredAt }
|
|
34
|
+
routes: new Map(), // path -> { port, target, name, healthCheck, group, registeredAt }
|
|
35
35
|
usedPorts: new Set(), // Ports déjà attribués
|
|
36
36
|
nextPort: INTERNAL_PORT_START
|
|
37
37
|
};
|