roster-server 1.7.6 → 1.8.0
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/.claude/settings.local.json +10 -0
- package/README.md +10 -0
- package/index.js +234 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,16 @@ roster.register('example.com', (httpsServer) => {
|
|
|
121
121
|
});
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
5. **Manual: Custom port**:
|
|
125
|
+
```javascript:demo/www/manual.js
|
|
126
|
+
roster.register('example.com:8080', (httpsServer) => {
|
|
127
|
+
return (req, res) => {
|
|
128
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
129
|
+
res.end('"Mad with thought, striving to embrace reason, yet the heart holds reasons that reason itself shall never comprehend."');
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
124
134
|
### Running the Server
|
|
125
135
|
|
|
126
136
|
```bash
|
package/index.js
CHANGED
|
@@ -1,7 +1,76 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const tls = require('tls');
|
|
5
|
+
const { EventEmitter } = require('events');
|
|
3
6
|
const Greenlock = require('greenlock-express');
|
|
4
7
|
|
|
8
|
+
// Virtual Server that completely isolates applications
|
|
9
|
+
class VirtualServer extends EventEmitter {
|
|
10
|
+
constructor(domain) {
|
|
11
|
+
super();
|
|
12
|
+
this.domain = domain;
|
|
13
|
+
this.requestListeners = [];
|
|
14
|
+
|
|
15
|
+
// Simulate http.Server properties
|
|
16
|
+
this.listening = false;
|
|
17
|
+
this.address = () => ({ port: 443, family: 'IPv4', address: '0.0.0.0' });
|
|
18
|
+
this.timeout = 0;
|
|
19
|
+
this.keepAliveTimeout = 5000;
|
|
20
|
+
this.headersTimeout = 60000;
|
|
21
|
+
this.maxHeadersCount = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Override listener methods to capture them
|
|
25
|
+
on(event, listener) {
|
|
26
|
+
if (event === 'request') {
|
|
27
|
+
this.requestListeners.push(listener);
|
|
28
|
+
}
|
|
29
|
+
return super.on(event, listener);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
addListener(event, listener) {
|
|
33
|
+
return this.on(event, listener);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Socket.IO compatibility methods
|
|
37
|
+
listeners(event) {
|
|
38
|
+
if (event === 'request') {
|
|
39
|
+
return this.requestListeners.slice();
|
|
40
|
+
}
|
|
41
|
+
return super.listeners(event);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
removeListener(event, listener) {
|
|
45
|
+
if (event === 'request') {
|
|
46
|
+
const index = this.requestListeners.indexOf(listener);
|
|
47
|
+
if (index !== -1) {
|
|
48
|
+
this.requestListeners.splice(index, 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return super.removeListener(event, listener);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
removeAllListeners(event) {
|
|
55
|
+
if (event === 'request') {
|
|
56
|
+
this.requestListeners = [];
|
|
57
|
+
}
|
|
58
|
+
return super.removeAllListeners(event);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Simulate other http.Server methods
|
|
62
|
+
listen() { this.listening = true; return this; }
|
|
63
|
+
close() { this.listening = false; return this; }
|
|
64
|
+
setTimeout() { return this; }
|
|
65
|
+
|
|
66
|
+
// Process request with this virtual server's listeners
|
|
67
|
+
processRequest(req, res) {
|
|
68
|
+
for (const listener of this.requestListeners) {
|
|
69
|
+
listener(req, res);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
5
74
|
class Roster {
|
|
6
75
|
constructor(options = {}) {
|
|
7
76
|
this.email = options.email || 'admin@example.com';
|
|
@@ -12,6 +81,8 @@ class Roster {
|
|
|
12
81
|
this.cluster = options.cluster || false;
|
|
13
82
|
this.domains = [];
|
|
14
83
|
this.sites = {};
|
|
84
|
+
this.domainServers = {}; // Store separate servers for each domain
|
|
85
|
+
this.portServers = {}; // Store servers by port
|
|
15
86
|
this.hostname = options.hostname || '0.0.0.0';
|
|
16
87
|
this.filename = options.filename || 'index';
|
|
17
88
|
|
|
@@ -19,7 +90,7 @@ class Roster {
|
|
|
19
90
|
if (port === 80) {
|
|
20
91
|
throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
|
|
21
92
|
}
|
|
22
|
-
this.
|
|
93
|
+
this.defaultPort = port;
|
|
23
94
|
}
|
|
24
95
|
|
|
25
96
|
async loadSites() {
|
|
@@ -38,7 +109,6 @@ class Roster {
|
|
|
38
109
|
|
|
39
110
|
const possibleIndexFiles = ['js', 'mjs', 'cjs'].map(ext => `${this.filename}.${ext}`);
|
|
40
111
|
let siteApp;
|
|
41
|
-
let loadedFile;
|
|
42
112
|
|
|
43
113
|
for (const indexFile of possibleIndexFiles) {
|
|
44
114
|
const indexPath = path.join(domainPath, indexFile);
|
|
@@ -51,7 +121,6 @@ class Roster {
|
|
|
51
121
|
});
|
|
52
122
|
// Handle default exports
|
|
53
123
|
siteApp = siteApp.default || siteApp;
|
|
54
|
-
loadedFile = indexFile;
|
|
55
124
|
break;
|
|
56
125
|
} catch (err) {
|
|
57
126
|
console.warn(`⚠️ Error loading ${indexPath}:`, err);
|
|
@@ -66,7 +135,7 @@ class Roster {
|
|
|
66
135
|
this.sites[d] = siteApp;
|
|
67
136
|
});
|
|
68
137
|
|
|
69
|
-
console.log(`✅ Loaded site: ${domain}
|
|
138
|
+
console.log(`✅ Loaded site: ${domain}`);
|
|
70
139
|
} else {
|
|
71
140
|
console.warn(`⚠️ No index file (js/mjs/cjs) found in ${domainPath}`);
|
|
72
141
|
}
|
|
@@ -177,14 +246,16 @@ class Roster {
|
|
|
177
246
|
}
|
|
178
247
|
}
|
|
179
248
|
|
|
180
|
-
register(
|
|
181
|
-
if (!
|
|
249
|
+
register(domainString, requestHandler) {
|
|
250
|
+
if (!domainString) {
|
|
182
251
|
throw new Error('Domain is required');
|
|
183
252
|
}
|
|
184
253
|
if (typeof requestHandler !== 'function') {
|
|
185
254
|
throw new Error('requestHandler must be a function');
|
|
186
255
|
}
|
|
187
256
|
|
|
257
|
+
const { domain, port } = this.parseDomainWithPort(domainString);
|
|
258
|
+
|
|
188
259
|
const domainEntries = [domain];
|
|
189
260
|
if ((domain.match(/\./g) || []).length < 2) {
|
|
190
261
|
domainEntries.push(`www.${domain}`);
|
|
@@ -192,13 +263,50 @@ class Roster {
|
|
|
192
263
|
|
|
193
264
|
this.domains.push(...domainEntries);
|
|
194
265
|
domainEntries.forEach(d => {
|
|
195
|
-
|
|
266
|
+
// Store with port information
|
|
267
|
+
const domainKey = port === this.defaultPort ? d : `${d}:${port}`;
|
|
268
|
+
this.sites[domainKey] = requestHandler;
|
|
196
269
|
});
|
|
197
270
|
|
|
198
|
-
console.log(`✅
|
|
271
|
+
console.log(`✅ Registered site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
|
|
199
272
|
return this;
|
|
200
273
|
}
|
|
201
274
|
|
|
275
|
+
parseDomainWithPort(domainString) {
|
|
276
|
+
const parts = domainString.split(':');
|
|
277
|
+
if (parts.length === 2) {
|
|
278
|
+
const domain = parts[0];
|
|
279
|
+
const port = parseInt(parts[1]);
|
|
280
|
+
if (port === 80) {
|
|
281
|
+
throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
|
|
282
|
+
}
|
|
283
|
+
return { domain, port };
|
|
284
|
+
}
|
|
285
|
+
return { domain: domainString, port: this.defaultPort };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
createVirtualServer(domain) {
|
|
289
|
+
return new VirtualServer(domain);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Get SSL context from Greenlock for custom ports
|
|
293
|
+
async getSSLContext(domain, greenlock) {
|
|
294
|
+
try {
|
|
295
|
+
// Try to get existing certificate for the domain
|
|
296
|
+
const site = await greenlock.get({ servername: domain });
|
|
297
|
+
if (site && site.pems) {
|
|
298
|
+
return {
|
|
299
|
+
key: site.pems.privkey,
|
|
300
|
+
cert: site.pems.cert + site.pems.chain
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Return undefined to let HTTPS server handle SNI callback
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
202
310
|
async start() {
|
|
203
311
|
await this.loadSites();
|
|
204
312
|
this.generateConfigJson();
|
|
@@ -211,31 +319,132 @@ class Roster {
|
|
|
211
319
|
staging: this.staging
|
|
212
320
|
});
|
|
213
321
|
|
|
214
|
-
const app = (req, res) => {
|
|
215
|
-
this.handleRequest(req, res);
|
|
216
|
-
};
|
|
217
|
-
|
|
218
322
|
return greenlock.ready(glx => {
|
|
219
|
-
// Obtener los servidores sin iniciarlos
|
|
220
|
-
const httpsServer = glx.httpsServer(null, app);
|
|
221
323
|
const httpServer = glx.httpServer();
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
324
|
+
|
|
325
|
+
// Group sites by port
|
|
326
|
+
const sitesByPort = {};
|
|
327
|
+
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
328
|
+
if (!hostKey.startsWith('www.')) {
|
|
329
|
+
const { domain, port } = this.parseDomainWithPort(hostKey);
|
|
330
|
+
if (!sitesByPort[port]) {
|
|
331
|
+
sitesByPort[port] = {
|
|
332
|
+
virtualServers: {},
|
|
333
|
+
appHandlers: {}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Create completely isolated virtual server
|
|
338
|
+
const virtualServer = this.createVirtualServer(domain);
|
|
339
|
+
sitesByPort[port].virtualServers[domain] = virtualServer;
|
|
340
|
+
this.domainServers[domain] = virtualServer;
|
|
341
|
+
|
|
342
|
+
// Initialize app with virtual server
|
|
343
|
+
const appHandler = siteApp(virtualServer);
|
|
344
|
+
sitesByPort[port].appHandlers[domain] = appHandler;
|
|
345
|
+
sitesByPort[port].appHandlers[`www.${domain}`] = appHandler;
|
|
229
346
|
}
|
|
230
347
|
}
|
|
231
348
|
|
|
349
|
+
// Create dispatcher for each port
|
|
350
|
+
const createDispatcher = (portData) => {
|
|
351
|
+
return (req, res) => {
|
|
352
|
+
const host = req.headers.host || '';
|
|
353
|
+
|
|
354
|
+
// Remove port from host header if present (e.g., "domain.com:8080" -> "domain.com")
|
|
355
|
+
const hostWithoutPort = host.split(':')[0];
|
|
356
|
+
const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
|
|
357
|
+
|
|
358
|
+
// Handle www redirects
|
|
359
|
+
if (hostWithoutPort.startsWith('www.')) {
|
|
360
|
+
res.writeHead(301, { Location: `https://${domain}${req.url}` });
|
|
361
|
+
res.end();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const virtualServer = portData.virtualServers[domain];
|
|
366
|
+
const appHandler = portData.appHandlers[domain];
|
|
367
|
+
|
|
368
|
+
if (virtualServer && virtualServer.requestListeners.length > 0) {
|
|
369
|
+
// App registered listeners on virtual server - use them
|
|
370
|
+
virtualServer.processRequest(req, res);
|
|
371
|
+
} else if (appHandler) {
|
|
372
|
+
// App returned a handler function - use it
|
|
373
|
+
appHandler(req, res);
|
|
374
|
+
} else {
|
|
375
|
+
res.writeHead(404);
|
|
376
|
+
res.end('Site not found');
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
|
|
232
381
|
httpServer.listen(80, this.hostname, () => {
|
|
233
|
-
console.log('
|
|
382
|
+
console.log('HTTP server listening on port 80');
|
|
234
383
|
});
|
|
235
384
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
385
|
+
// Handle different port types
|
|
386
|
+
for (const [port, portData] of Object.entries(sitesByPort)) {
|
|
387
|
+
const portNum = parseInt(port);
|
|
388
|
+
const dispatcher = createDispatcher(portData);
|
|
389
|
+
|
|
390
|
+
if (portNum === this.defaultPort) {
|
|
391
|
+
// Use Greenlock for default port (443) with SSL
|
|
392
|
+
const httpsServer = glx.httpsServer(null, dispatcher);
|
|
393
|
+
this.portServers[portNum] = httpsServer;
|
|
394
|
+
|
|
395
|
+
httpsServer.listen(portNum, this.hostname, () => {
|
|
396
|
+
console.log(`HTTPS server listening on port ${portNum}`);
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
// Create HTTPS server for custom ports using Greenlock certificates
|
|
400
|
+
const httpsOptions = {
|
|
401
|
+
// SNI callback to get certificates dynamically
|
|
402
|
+
SNICallback: (domain, callback) => {
|
|
403
|
+
try {
|
|
404
|
+
const certPath = path.join(this.greenlockStorePath, 'live', domain);
|
|
405
|
+
const keyPath = path.join(certPath, 'privkey.pem');
|
|
406
|
+
const certFilePath = path.join(certPath, 'cert.pem');
|
|
407
|
+
const chainPath = path.join(certPath, 'chain.pem');
|
|
408
|
+
|
|
409
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
|
|
410
|
+
const key = fs.readFileSync(keyPath, 'utf8');
|
|
411
|
+
const cert = fs.readFileSync(certFilePath, 'utf8');
|
|
412
|
+
const chain = fs.readFileSync(chainPath, 'utf8');
|
|
413
|
+
|
|
414
|
+
callback(null, tls.createSecureContext({
|
|
415
|
+
key: key,
|
|
416
|
+
cert: cert + chain
|
|
417
|
+
}));
|
|
418
|
+
} else {
|
|
419
|
+
callback(new Error(`No certificate files available for ${domain}`));
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
callback(error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const httpsServer = https.createServer(httpsOptions, dispatcher);
|
|
428
|
+
|
|
429
|
+
httpsServer.on('error', (error) => {
|
|
430
|
+
console.error(`HTTPS server error on port ${portNum}:`, error.message);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
httpsServer.on('tlsClientError', (error) => {
|
|
434
|
+
console.error(`TLS error on port ${portNum}:`, error.message);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
this.portServers[portNum] = httpsServer;
|
|
438
|
+
|
|
439
|
+
httpsServer.listen(portNum, this.hostname, (error) => {
|
|
440
|
+
if (error) {
|
|
441
|
+
console.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(`HTTPS server listening on port ${portNum}`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
239
448
|
});
|
|
240
449
|
}
|
|
241
450
|
}
|