roster-server 1.8.8 → 1.9.2
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
CHANGED
|
@@ -185,10 +185,10 @@ server.start();
|
|
|
185
185
|
|
|
186
186
|
### Port Assignment
|
|
187
187
|
|
|
188
|
-
In local mode, domains are automatically assigned ports
|
|
188
|
+
In local mode, domains are automatically assigned ports based on a CRC32 hash of the domain name (range 4000-9999):
|
|
189
189
|
|
|
190
|
-
- `example.com` → `http://localhost:
|
|
191
|
-
- `api.example.com` → `http://localhost:
|
|
190
|
+
- `example.com` → `http://localhost:9465`
|
|
191
|
+
- `api.example.com` → `http://localhost:9388`
|
|
192
192
|
- And so on...
|
|
193
193
|
|
|
194
194
|
## 🧂 A Touch of Magic
|
package/index.js
CHANGED
|
@@ -7,13 +7,40 @@ const { EventEmitter } = require('events');
|
|
|
7
7
|
const Greenlock = require('greenlock-express');
|
|
8
8
|
const log = require('lemonlog')('roster');
|
|
9
9
|
|
|
10
|
+
// CRC32 implementation for deterministic port assignment
|
|
11
|
+
function crc32(str) {
|
|
12
|
+
const crcTable = [];
|
|
13
|
+
for (let i = 0; i < 256; i++) {
|
|
14
|
+
let crc = i;
|
|
15
|
+
for (let j = 0; j < 8; j++) {
|
|
16
|
+
crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
|
|
17
|
+
}
|
|
18
|
+
crcTable[i] = crc;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let crc = 0xFFFFFFFF;
|
|
22
|
+
for (let i = 0; i < str.length; i++) {
|
|
23
|
+
const byte = str.charCodeAt(i);
|
|
24
|
+
crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xFF];
|
|
25
|
+
}
|
|
26
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Convert CRC32 hash to a port number in available range
|
|
30
|
+
function domainToPort(domain, minPort = 3000, maxPort = 65535) {
|
|
31
|
+
const hash = crc32(domain);
|
|
32
|
+
const portRange = maxPort - minPort + 1;
|
|
33
|
+
return minPort + (hash % portRange);
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
// Virtual Server that completely isolates applications
|
|
11
37
|
class VirtualServer extends EventEmitter {
|
|
12
38
|
constructor(domain) {
|
|
13
39
|
super();
|
|
14
40
|
this.domain = domain;
|
|
15
41
|
this.requestListeners = [];
|
|
16
|
-
|
|
42
|
+
this.upgradeListeners = [];
|
|
43
|
+
|
|
17
44
|
// Simulate http.Server properties
|
|
18
45
|
this.listening = false;
|
|
19
46
|
this.address = () => ({ port: 443, family: 'IPv4', address: '0.0.0.0' });
|
|
@@ -22,70 +49,81 @@ class VirtualServer extends EventEmitter {
|
|
|
22
49
|
this.headersTimeout = 60000;
|
|
23
50
|
this.maxHeadersCount = null;
|
|
24
51
|
}
|
|
25
|
-
|
|
52
|
+
|
|
26
53
|
// Override listener methods to capture them
|
|
27
54
|
on(event, listener) {
|
|
28
55
|
if (event === 'request') {
|
|
29
56
|
this.requestListeners.push(listener);
|
|
57
|
+
} else if (event === 'upgrade') {
|
|
58
|
+
this.upgradeListeners.push(listener);
|
|
30
59
|
}
|
|
31
60
|
return super.on(event, listener);
|
|
32
61
|
}
|
|
33
|
-
|
|
62
|
+
|
|
34
63
|
addListener(event, listener) {
|
|
35
64
|
return this.on(event, listener);
|
|
36
65
|
}
|
|
37
|
-
|
|
66
|
+
|
|
38
67
|
// Socket.IO compatibility methods
|
|
39
68
|
listeners(event) {
|
|
40
69
|
if (event === 'request') {
|
|
41
70
|
return this.requestListeners.slice();
|
|
71
|
+
} else if (event === 'upgrade') {
|
|
72
|
+
return this.upgradeListeners.slice();
|
|
42
73
|
}
|
|
43
74
|
return super.listeners(event);
|
|
44
75
|
}
|
|
45
|
-
|
|
76
|
+
|
|
46
77
|
removeListener(event, listener) {
|
|
47
78
|
if (event === 'request') {
|
|
48
79
|
const index = this.requestListeners.indexOf(listener);
|
|
49
80
|
if (index !== -1) {
|
|
50
81
|
this.requestListeners.splice(index, 1);
|
|
51
82
|
}
|
|
83
|
+
} else if (event === 'upgrade') {
|
|
84
|
+
const index = this.upgradeListeners.indexOf(listener);
|
|
85
|
+
if (index !== -1) {
|
|
86
|
+
this.upgradeListeners.splice(index, 1);
|
|
87
|
+
}
|
|
52
88
|
}
|
|
53
89
|
return super.removeListener(event, listener);
|
|
54
90
|
}
|
|
55
|
-
|
|
91
|
+
|
|
56
92
|
removeAllListeners(event) {
|
|
57
93
|
if (event === 'request') {
|
|
58
94
|
this.requestListeners = [];
|
|
95
|
+
} else if (event === 'upgrade') {
|
|
96
|
+
this.upgradeListeners = [];
|
|
59
97
|
}
|
|
60
98
|
return super.removeAllListeners(event);
|
|
61
99
|
}
|
|
62
|
-
|
|
100
|
+
|
|
63
101
|
// Simulate other http.Server methods
|
|
64
102
|
listen() { this.listening = true; return this; }
|
|
65
103
|
close() { this.listening = false; return this; }
|
|
66
104
|
setTimeout() { return this; }
|
|
67
|
-
|
|
105
|
+
|
|
68
106
|
// Process request with this virtual server's listeners
|
|
69
107
|
processRequest(req, res) {
|
|
70
108
|
let handled = false;
|
|
71
|
-
|
|
109
|
+
|
|
72
110
|
// Track if response was handled
|
|
73
111
|
const originalEnd = res.end;
|
|
74
|
-
res.end = function(...args) {
|
|
112
|
+
res.end = function (...args) {
|
|
75
113
|
handled = true;
|
|
76
114
|
return originalEnd.apply(this, args);
|
|
77
115
|
};
|
|
78
|
-
|
|
116
|
+
|
|
79
117
|
// Try all listeners
|
|
80
118
|
for (const listener of this.requestListeners) {
|
|
81
119
|
if (!handled) {
|
|
82
120
|
listener(req, res);
|
|
83
121
|
}
|
|
84
122
|
}
|
|
85
|
-
|
|
123
|
+
|
|
86
124
|
// Restore original end method
|
|
87
125
|
res.end = originalEnd;
|
|
88
|
-
|
|
126
|
+
|
|
89
127
|
// If no listener handled the request, try fallback handler
|
|
90
128
|
if (!handled && this.fallbackHandler) {
|
|
91
129
|
this.fallbackHandler(req, res);
|
|
@@ -94,6 +132,19 @@ class VirtualServer extends EventEmitter {
|
|
|
94
132
|
res.end('No handler found');
|
|
95
133
|
}
|
|
96
134
|
}
|
|
135
|
+
|
|
136
|
+
// Process upgrade events (WebSocket)
|
|
137
|
+
processUpgrade(req, socket, head) {
|
|
138
|
+
// Emit to all registered upgrade listeners
|
|
139
|
+
for (const listener of this.upgradeListeners) {
|
|
140
|
+
listener(req, socket, head);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If no listeners, destroy the socket
|
|
144
|
+
if (this.upgradeListeners.length === 0) {
|
|
145
|
+
socket.destroy();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
97
148
|
}
|
|
98
149
|
|
|
99
150
|
class Roster {
|
|
@@ -109,6 +160,7 @@ class Roster {
|
|
|
109
160
|
this.sites = {};
|
|
110
161
|
this.domainServers = {}; // Store separate servers for each domain
|
|
111
162
|
this.portServers = {}; // Store servers by port
|
|
163
|
+
this.assignedPorts = new Set(); // Track ports assigned to domains (not OS availability)
|
|
112
164
|
this.hostname = options.hostname || '0.0.0.0';
|
|
113
165
|
this.filename = options.filename || 'index';
|
|
114
166
|
|
|
@@ -281,7 +333,7 @@ class Roster {
|
|
|
281
333
|
}
|
|
282
334
|
|
|
283
335
|
const { domain, port } = this.parseDomainWithPort(domainString);
|
|
284
|
-
|
|
336
|
+
|
|
285
337
|
const domainEntries = [domain];
|
|
286
338
|
if ((domain.match(/\./g) || []).length < 2) {
|
|
287
339
|
domainEntries.push(`www.${domain}`);
|
|
@@ -315,6 +367,22 @@ class Roster {
|
|
|
315
367
|
return new VirtualServer(domain);
|
|
316
368
|
}
|
|
317
369
|
|
|
370
|
+
// Assign port to domain, detecting collisions with already assigned ports
|
|
371
|
+
assignPortToDomain(domain, minPort = 4000, maxPort = 9999) {
|
|
372
|
+
let port = domainToPort(domain, minPort, maxPort);
|
|
373
|
+
|
|
374
|
+
// If port is already assigned to another domain, increment until we find a free one
|
|
375
|
+
while (this.assignedPorts.has(port)) {
|
|
376
|
+
port++;
|
|
377
|
+
if (port > maxPort) {
|
|
378
|
+
port = minPort; // Wrap around if we exceed max port
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.assignedPorts.add(port);
|
|
383
|
+
return port;
|
|
384
|
+
}
|
|
385
|
+
|
|
318
386
|
// Get SSL context from Greenlock for custom ports
|
|
319
387
|
async getSSLContext(domain, greenlock) {
|
|
320
388
|
try {
|
|
@@ -328,39 +396,37 @@ class Roster {
|
|
|
328
396
|
}
|
|
329
397
|
} catch (error) {
|
|
330
398
|
}
|
|
331
|
-
|
|
399
|
+
|
|
332
400
|
// Return undefined to let HTTPS server handle SNI callback
|
|
333
401
|
return null;
|
|
334
402
|
}
|
|
335
403
|
|
|
336
404
|
// Start server in local mode with HTTP - simplified version
|
|
337
405
|
startLocalMode() {
|
|
338
|
-
|
|
339
|
-
let currentPort = startPort;
|
|
340
|
-
|
|
341
|
-
// Create a simple HTTP server for each domain with sequential ports
|
|
406
|
+
// Create a simple HTTP server for each domain with CRC32-based ports
|
|
342
407
|
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
343
408
|
const domain = hostKey.split(':')[0]; // Remove port if present
|
|
344
|
-
|
|
409
|
+
|
|
345
410
|
// Skip www domains in local mode
|
|
346
411
|
if (domain.startsWith('www.')) {
|
|
347
412
|
continue;
|
|
348
413
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
414
|
+
|
|
415
|
+
// Calculate deterministic port based on domain CRC32, with collision detection
|
|
416
|
+
const port = this.assignPortToDomain(domain, 4000, 9999);
|
|
417
|
+
|
|
352
418
|
// Create virtual server for the domain
|
|
353
419
|
const virtualServer = this.createVirtualServer(domain);
|
|
354
420
|
this.domainServers[domain] = virtualServer;
|
|
355
|
-
|
|
421
|
+
|
|
356
422
|
// Initialize app with virtual server
|
|
357
423
|
const appHandler = siteApp(virtualServer);
|
|
358
|
-
|
|
424
|
+
|
|
359
425
|
// Create simple dispatcher for this domain
|
|
360
426
|
const dispatcher = (req, res) => {
|
|
361
427
|
// Set fallback handler on virtual server for non-Socket.IO requests
|
|
362
428
|
virtualServer.fallbackHandler = appHandler;
|
|
363
|
-
|
|
429
|
+
|
|
364
430
|
if (virtualServer.requestListeners.length > 0) {
|
|
365
431
|
virtualServer.processRequest(req, res);
|
|
366
432
|
} else if (appHandler) {
|
|
@@ -370,29 +436,32 @@ class Roster {
|
|
|
370
436
|
res.end('Site not found');
|
|
371
437
|
}
|
|
372
438
|
};
|
|
373
|
-
|
|
439
|
+
|
|
374
440
|
// Create HTTP server for this domain
|
|
375
441
|
const httpServer = http.createServer(dispatcher);
|
|
376
442
|
this.portServers[port] = httpServer;
|
|
377
|
-
|
|
443
|
+
|
|
444
|
+
// Handle WebSocket upgrade events
|
|
445
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
446
|
+
virtualServer.processUpgrade(req, socket, head);
|
|
447
|
+
});
|
|
448
|
+
|
|
378
449
|
httpServer.listen(port, 'localhost', () => {
|
|
379
450
|
log.info(`🌐 ${domain} → http://localhost:${port}`);
|
|
380
451
|
});
|
|
381
|
-
|
|
452
|
+
|
|
382
453
|
httpServer.on('error', (error) => {
|
|
383
454
|
log.error(`❌ Error on port ${port} for ${domain}:`, error.message);
|
|
384
455
|
});
|
|
385
|
-
|
|
386
|
-
currentPort++;
|
|
387
456
|
}
|
|
388
|
-
|
|
389
|
-
log.info(`(✔) Started ${
|
|
457
|
+
|
|
458
|
+
log.info(`(✔) Started ${Object.keys(this.portServers).length} sites in local mode`);
|
|
390
459
|
return Promise.resolve();
|
|
391
460
|
}
|
|
392
461
|
|
|
393
462
|
async start() {
|
|
394
463
|
await this.loadSites();
|
|
395
|
-
|
|
464
|
+
|
|
396
465
|
// Skip Greenlock configuration generation in local mode
|
|
397
466
|
if (!this.local) {
|
|
398
467
|
this.generateConfigJson();
|
|
@@ -413,7 +482,7 @@ class Roster {
|
|
|
413
482
|
|
|
414
483
|
return greenlock.ready(glx => {
|
|
415
484
|
const httpServer = glx.httpServer();
|
|
416
|
-
|
|
485
|
+
|
|
417
486
|
// Group sites by port
|
|
418
487
|
const sitesByPort = {};
|
|
419
488
|
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
@@ -425,12 +494,12 @@ class Roster {
|
|
|
425
494
|
appHandlers: {}
|
|
426
495
|
};
|
|
427
496
|
}
|
|
428
|
-
|
|
497
|
+
|
|
429
498
|
// Create completely isolated virtual server
|
|
430
499
|
const virtualServer = this.createVirtualServer(domain);
|
|
431
500
|
sitesByPort[port].virtualServers[domain] = virtualServer;
|
|
432
501
|
this.domainServers[domain] = virtualServer;
|
|
433
|
-
|
|
502
|
+
|
|
434
503
|
// Initialize app with virtual server
|
|
435
504
|
const appHandler = siteApp(virtualServer);
|
|
436
505
|
sitesByPort[port].appHandlers[domain] = appHandler;
|
|
@@ -442,11 +511,11 @@ class Roster {
|
|
|
442
511
|
const createDispatcher = (portData) => {
|
|
443
512
|
return (req, res) => {
|
|
444
513
|
const host = req.headers.host || '';
|
|
445
|
-
|
|
514
|
+
|
|
446
515
|
// Remove port from host header if present (e.g., "domain.com:8080" -> "domain.com")
|
|
447
516
|
const hostWithoutPort = host.split(':')[0];
|
|
448
517
|
const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
|
|
449
|
-
|
|
518
|
+
|
|
450
519
|
// Handle www redirects
|
|
451
520
|
if (hostWithoutPort.startsWith('www.')) {
|
|
452
521
|
res.writeHead(301, { Location: `https://${domain}${req.url}` });
|
|
@@ -456,7 +525,7 @@ class Roster {
|
|
|
456
525
|
|
|
457
526
|
const virtualServer = portData.virtualServers[domain];
|
|
458
527
|
const appHandler = portData.appHandlers[domain];
|
|
459
|
-
|
|
528
|
+
|
|
460
529
|
if (virtualServer && virtualServer.requestListeners.length > 0) {
|
|
461
530
|
// Set fallback handler on virtual server for non-Socket.IO requests
|
|
462
531
|
virtualServer.fallbackHandler = appHandler;
|
|
@@ -476,16 +545,38 @@ class Roster {
|
|
|
476
545
|
log.info('HTTP server listening on port 80');
|
|
477
546
|
});
|
|
478
547
|
|
|
548
|
+
// Create upgrade handler for WebSocket connections
|
|
549
|
+
const createUpgradeHandler = (portData) => {
|
|
550
|
+
return (req, socket, head) => {
|
|
551
|
+
const host = req.headers.host || '';
|
|
552
|
+
const hostWithoutPort = host.split(':')[0];
|
|
553
|
+
const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
|
|
554
|
+
|
|
555
|
+
const virtualServer = portData.virtualServers[domain];
|
|
556
|
+
|
|
557
|
+
if (virtualServer) {
|
|
558
|
+
virtualServer.processUpgrade(req, socket, head);
|
|
559
|
+
} else {
|
|
560
|
+
// No virtual server found, destroy the socket
|
|
561
|
+
socket.destroy();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
|
|
479
566
|
// Handle different port types
|
|
480
567
|
for (const [port, portData] of Object.entries(sitesByPort)) {
|
|
481
568
|
const portNum = parseInt(port);
|
|
482
569
|
const dispatcher = createDispatcher(portData);
|
|
483
|
-
|
|
570
|
+
const upgradeHandler = createUpgradeHandler(portData);
|
|
571
|
+
|
|
484
572
|
if (portNum === this.defaultPort) {
|
|
485
573
|
// Use Greenlock for default port (443) with SSL
|
|
486
574
|
const httpsServer = glx.httpsServer(null, dispatcher);
|
|
487
575
|
this.portServers[portNum] = httpsServer;
|
|
488
|
-
|
|
576
|
+
|
|
577
|
+
// Handle WebSocket upgrade events
|
|
578
|
+
httpsServer.on('upgrade', upgradeHandler);
|
|
579
|
+
|
|
489
580
|
httpsServer.listen(portNum, this.hostname, () => {
|
|
490
581
|
log.info(`HTTPS server listening on port ${portNum}`);
|
|
491
582
|
});
|
|
@@ -499,12 +590,12 @@ class Roster {
|
|
|
499
590
|
const keyPath = path.join(certPath, 'privkey.pem');
|
|
500
591
|
const certFilePath = path.join(certPath, 'cert.pem');
|
|
501
592
|
const chainPath = path.join(certPath, 'chain.pem');
|
|
502
|
-
|
|
593
|
+
|
|
503
594
|
if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
|
|
504
595
|
const key = fs.readFileSync(keyPath, 'utf8');
|
|
505
596
|
const cert = fs.readFileSync(certFilePath, 'utf8');
|
|
506
597
|
const chain = fs.readFileSync(chainPath, 'utf8');
|
|
507
|
-
|
|
598
|
+
|
|
508
599
|
callback(null, tls.createSecureContext({
|
|
509
600
|
key: key,
|
|
510
601
|
cert: cert + chain
|
|
@@ -517,22 +608,25 @@ class Roster {
|
|
|
517
608
|
}
|
|
518
609
|
}
|
|
519
610
|
};
|
|
520
|
-
|
|
611
|
+
|
|
521
612
|
const httpsServer = https.createServer(httpsOptions, dispatcher);
|
|
522
|
-
|
|
613
|
+
|
|
614
|
+
// Handle WebSocket upgrade events
|
|
615
|
+
httpsServer.on('upgrade', upgradeHandler);
|
|
616
|
+
|
|
523
617
|
httpsServer.on('error', (error) => {
|
|
524
618
|
log.error(`HTTPS server error on port ${portNum}:`, error.message);
|
|
525
619
|
});
|
|
526
|
-
|
|
620
|
+
|
|
527
621
|
httpsServer.on('tlsClientError', (error) => {
|
|
528
622
|
// Suppress HTTP request errors to avoid log spam
|
|
529
623
|
if (!error.message.includes('http request')) {
|
|
530
624
|
log.error(`TLS error on port ${portNum}:`, error.message);
|
|
531
625
|
}
|
|
532
626
|
});
|
|
533
|
-
|
|
627
|
+
|
|
534
628
|
this.portServers[portNum] = httpsServer;
|
|
535
|
-
|
|
629
|
+
|
|
536
630
|
httpsServer.listen(portNum, this.hostname, (error) => {
|
|
537
631
|
if (error) {
|
|
538
632
|
log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roster-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"websocket",
|
|
26
26
|
"express",
|
|
27
27
|
"greenlock-express",
|
|
28
|
+
"shotx",
|
|
28
29
|
"clasen"
|
|
29
30
|
],
|
|
30
31
|
"author": "Martin Clasen",
|
|
File without changes
|