@imenam/self-register-proxy 1.0.37

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.
@@ -0,0 +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 };
@@ -0,0 +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, 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 };
@@ -0,0 +1,154 @@
1
+ const os = require('os');
2
+ const { execSync } = require('child_process');
3
+ const pty = require('node-pty');
4
+ const { WebSocketServer } = require('ws');
5
+
6
+ const DEFAULT_SHELL = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
7
+
8
+ class TerminalServer {
9
+ /**
10
+ * @param {import('http').Server} httpServer
11
+ * @param {object} [options]
12
+ * @param {string} [options.shell] - Shell to spawn (default: auto-detected)
13
+ * @param {string[]} [options.shellArgs] - Shell arguments (default: [])
14
+ * @param {object} [options.ptyOptions] - node-pty spawn options overrides
15
+ * @param {number} [options.ptyOptions.cols] - Initial columns (default: 80)
16
+ * @param {number} [options.ptyOptions.rows] - Initial rows (default: 24)
17
+ * @param {string} [options.ptyOptions.cwd] - Working directory (default: user home)
18
+ * @param {object} [options.ptyOptions.env] - Environment variables (default: process.env)
19
+ * @param {string} [options.command] - Command to execute automatically after spawn
20
+ * @param {string} [options.path] - WebSocket endpoint path (default: '/terminal')
21
+ * @param {object} [options.wsOptions] - WebSocketServer options overrides
22
+ */
23
+ constructor(httpServer, options = {}) {
24
+ this.shell = options.shell || DEFAULT_SHELL;
25
+ this.shellArgs = options.shellArgs || [];
26
+ this.command = options.command || '';
27
+ this.ptyOptions = {
28
+ name: 'xterm-color',
29
+ cols: 80,
30
+ rows: 24,
31
+ cwd: process.env.HOME || process.env.USERPROFILE,
32
+ env: process.env,
33
+ ...options.ptyOptions,
34
+ };
35
+
36
+ this.connections = new Set();
37
+
38
+ this.wss = new WebSocketServer({
39
+ server: httpServer,
40
+ path: options.path || '/terminal',
41
+ ...options.wsOptions,
42
+ });
43
+ this.wss.on('connection', (ws, req) => this._handleConnection(ws, req));
44
+ }
45
+
46
+ _handleConnection(ws, req) {
47
+ const cwd = this.ptyOptions.cwd;
48
+
49
+ let ptyProcess;
50
+ try {
51
+ ptyProcess = pty.spawn(this.shell, this.shellArgs, {
52
+ ...this.ptyOptions,
53
+ cwd,
54
+ });
55
+ } catch (err) {
56
+ console.error(`[TERMINAL] Failed to spawn PTY (cwd: ${cwd}):`, err.message);
57
+ ws.close(1011, 'Failed to spawn terminal');
58
+ return;
59
+ }
60
+
61
+ const connection = { ws, ptyProcess };
62
+ this.connections.add(connection);
63
+
64
+ ptyProcess.onData((data) => {
65
+ try {
66
+ ws.send(data);
67
+ } catch (e) {
68
+ // WebSocket already closed
69
+ }
70
+ });
71
+
72
+ ws.on('message', (data) => {
73
+ const message = data.toString();
74
+ try {
75
+ const msg = JSON.parse(message);
76
+ if (msg.type === 'resize') {
77
+ ptyProcess.resize(msg.cols, msg.rows);
78
+ return;
79
+ }
80
+ } catch (e) {
81
+ // Not JSON, treat as terminal input
82
+ }
83
+ ptyProcess.write(message);
84
+ });
85
+
86
+ ws.on('close', () => {
87
+ this._gracefulKill(ptyProcess);
88
+ this.connections.delete(connection);
89
+ });
90
+
91
+ // Exécuter automatiquement la commande configurée
92
+ if (this.command) {
93
+ // Petit délai pour laisser le shell s'initialiser
94
+ setTimeout(() => {
95
+ ptyProcess.write(this.command + '\r');
96
+ console.log(`[TERMINAL] Auto-executed command: ${this.command}`);
97
+ }, 500);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Arrêt gracieux : envoie Ctrl+C deux fois pour interrompre proprement
103
+ * le programme en cours, attend qu'il se termine, puis tue le shell.
104
+ * @param {import('node-pty').IPty} ptyProcess
105
+ */
106
+ _gracefulKill(ptyProcess) {
107
+ const pid = ptyProcess.pid;
108
+ console.log(`[TERMINAL] Graceful shutdown (PID: ${pid})...`);
109
+
110
+ // 1er Ctrl+C
111
+ try { ptyProcess.write('\x03'); } catch (_) { /* ignore */ }
112
+
113
+ setTimeout(() => {
114
+ // 2e Ctrl+C
115
+ try { ptyProcess.write('\x03'); } catch (_) { /* ignore */ }
116
+
117
+ setTimeout(() => {
118
+ // Tuer tout l'arbre de processus
119
+ this._killProcessTree(pid);
120
+ }, 1000);
121
+ }, 500);
122
+ }
123
+
124
+ /**
125
+ * Tue l'arbre de processus par PID.
126
+ * Sur Windows : taskkill /F /T /PID
127
+ * Sur Linux/macOS : kill du groupe de processus
128
+ * @param {number} pid
129
+ */
130
+ _killProcessTree(pid) {
131
+ try {
132
+ if (os.platform() === 'win32') {
133
+ execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'ignore' });
134
+ } else {
135
+ process.kill(-pid, 'SIGKILL');
136
+ }
137
+ console.log(`[TERMINAL] Killed process tree (PID: ${pid})`);
138
+ } catch (e) {
139
+ // Le processus est peut-être déjà terminé
140
+ console.log(`[TERMINAL] Process tree already exited (PID: ${pid})`);
141
+ }
142
+ }
143
+
144
+ dispose() {
145
+ for (const { ws, ptyProcess } of this.connections) {
146
+ this._gracefulKill(ptyProcess);
147
+ ws.close();
148
+ }
149
+ this.connections.clear();
150
+ this.wss.close();
151
+ }
152
+ }
153
+
154
+ module.exports = { TerminalServer };