roster-server 1.9.0 → 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,6 +7,32 @@ 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) {
|
|
@@ -14,7 +40,7 @@ class VirtualServer extends EventEmitter {
|
|
|
14
40
|
this.domain = domain;
|
|
15
41
|
this.requestListeners = [];
|
|
16
42
|
this.upgradeListeners = [];
|
|
17
|
-
|
|
43
|
+
|
|
18
44
|
// Simulate http.Server properties
|
|
19
45
|
this.listening = false;
|
|
20
46
|
this.address = () => ({ port: 443, family: 'IPv4', address: '0.0.0.0' });
|
|
@@ -23,7 +49,7 @@ class VirtualServer extends EventEmitter {
|
|
|
23
49
|
this.headersTimeout = 60000;
|
|
24
50
|
this.maxHeadersCount = null;
|
|
25
51
|
}
|
|
26
|
-
|
|
52
|
+
|
|
27
53
|
// Override listener methods to capture them
|
|
28
54
|
on(event, listener) {
|
|
29
55
|
if (event === 'request') {
|
|
@@ -33,11 +59,11 @@ class VirtualServer extends EventEmitter {
|
|
|
33
59
|
}
|
|
34
60
|
return super.on(event, listener);
|
|
35
61
|
}
|
|
36
|
-
|
|
62
|
+
|
|
37
63
|
addListener(event, listener) {
|
|
38
64
|
return this.on(event, listener);
|
|
39
65
|
}
|
|
40
|
-
|
|
66
|
+
|
|
41
67
|
// Socket.IO compatibility methods
|
|
42
68
|
listeners(event) {
|
|
43
69
|
if (event === 'request') {
|
|
@@ -47,7 +73,7 @@ class VirtualServer extends EventEmitter {
|
|
|
47
73
|
}
|
|
48
74
|
return super.listeners(event);
|
|
49
75
|
}
|
|
50
|
-
|
|
76
|
+
|
|
51
77
|
removeListener(event, listener) {
|
|
52
78
|
if (event === 'request') {
|
|
53
79
|
const index = this.requestListeners.indexOf(listener);
|
|
@@ -62,7 +88,7 @@ class VirtualServer extends EventEmitter {
|
|
|
62
88
|
}
|
|
63
89
|
return super.removeListener(event, listener);
|
|
64
90
|
}
|
|
65
|
-
|
|
91
|
+
|
|
66
92
|
removeAllListeners(event) {
|
|
67
93
|
if (event === 'request') {
|
|
68
94
|
this.requestListeners = [];
|
|
@@ -71,33 +97,33 @@ class VirtualServer extends EventEmitter {
|
|
|
71
97
|
}
|
|
72
98
|
return super.removeAllListeners(event);
|
|
73
99
|
}
|
|
74
|
-
|
|
100
|
+
|
|
75
101
|
// Simulate other http.Server methods
|
|
76
102
|
listen() { this.listening = true; return this; }
|
|
77
103
|
close() { this.listening = false; return this; }
|
|
78
104
|
setTimeout() { return this; }
|
|
79
|
-
|
|
105
|
+
|
|
80
106
|
// Process request with this virtual server's listeners
|
|
81
107
|
processRequest(req, res) {
|
|
82
108
|
let handled = false;
|
|
83
|
-
|
|
109
|
+
|
|
84
110
|
// Track if response was handled
|
|
85
111
|
const originalEnd = res.end;
|
|
86
|
-
res.end = function(...args) {
|
|
112
|
+
res.end = function (...args) {
|
|
87
113
|
handled = true;
|
|
88
114
|
return originalEnd.apply(this, args);
|
|
89
115
|
};
|
|
90
|
-
|
|
116
|
+
|
|
91
117
|
// Try all listeners
|
|
92
118
|
for (const listener of this.requestListeners) {
|
|
93
119
|
if (!handled) {
|
|
94
120
|
listener(req, res);
|
|
95
121
|
}
|
|
96
122
|
}
|
|
97
|
-
|
|
123
|
+
|
|
98
124
|
// Restore original end method
|
|
99
125
|
res.end = originalEnd;
|
|
100
|
-
|
|
126
|
+
|
|
101
127
|
// If no listener handled the request, try fallback handler
|
|
102
128
|
if (!handled && this.fallbackHandler) {
|
|
103
129
|
this.fallbackHandler(req, res);
|
|
@@ -106,14 +132,14 @@ class VirtualServer extends EventEmitter {
|
|
|
106
132
|
res.end('No handler found');
|
|
107
133
|
}
|
|
108
134
|
}
|
|
109
|
-
|
|
135
|
+
|
|
110
136
|
// Process upgrade events (WebSocket)
|
|
111
137
|
processUpgrade(req, socket, head) {
|
|
112
138
|
// Emit to all registered upgrade listeners
|
|
113
139
|
for (const listener of this.upgradeListeners) {
|
|
114
140
|
listener(req, socket, head);
|
|
115
141
|
}
|
|
116
|
-
|
|
142
|
+
|
|
117
143
|
// If no listeners, destroy the socket
|
|
118
144
|
if (this.upgradeListeners.length === 0) {
|
|
119
145
|
socket.destroy();
|
|
@@ -134,6 +160,7 @@ class Roster {
|
|
|
134
160
|
this.sites = {};
|
|
135
161
|
this.domainServers = {}; // Store separate servers for each domain
|
|
136
162
|
this.portServers = {}; // Store servers by port
|
|
163
|
+
this.assignedPorts = new Set(); // Track ports assigned to domains (not OS availability)
|
|
137
164
|
this.hostname = options.hostname || '0.0.0.0';
|
|
138
165
|
this.filename = options.filename || 'index';
|
|
139
166
|
|
|
@@ -306,7 +333,7 @@ class Roster {
|
|
|
306
333
|
}
|
|
307
334
|
|
|
308
335
|
const { domain, port } = this.parseDomainWithPort(domainString);
|
|
309
|
-
|
|
336
|
+
|
|
310
337
|
const domainEntries = [domain];
|
|
311
338
|
if ((domain.match(/\./g) || []).length < 2) {
|
|
312
339
|
domainEntries.push(`www.${domain}`);
|
|
@@ -340,6 +367,22 @@ class Roster {
|
|
|
340
367
|
return new VirtualServer(domain);
|
|
341
368
|
}
|
|
342
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
|
+
|
|
343
386
|
// Get SSL context from Greenlock for custom ports
|
|
344
387
|
async getSSLContext(domain, greenlock) {
|
|
345
388
|
try {
|
|
@@ -353,39 +396,37 @@ class Roster {
|
|
|
353
396
|
}
|
|
354
397
|
} catch (error) {
|
|
355
398
|
}
|
|
356
|
-
|
|
399
|
+
|
|
357
400
|
// Return undefined to let HTTPS server handle SNI callback
|
|
358
401
|
return null;
|
|
359
402
|
}
|
|
360
403
|
|
|
361
404
|
// Start server in local mode with HTTP - simplified version
|
|
362
405
|
startLocalMode() {
|
|
363
|
-
|
|
364
|
-
let currentPort = startPort;
|
|
365
|
-
|
|
366
|
-
// Create a simple HTTP server for each domain with sequential ports
|
|
406
|
+
// Create a simple HTTP server for each domain with CRC32-based ports
|
|
367
407
|
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
368
408
|
const domain = hostKey.split(':')[0]; // Remove port if present
|
|
369
|
-
|
|
409
|
+
|
|
370
410
|
// Skip www domains in local mode
|
|
371
411
|
if (domain.startsWith('www.')) {
|
|
372
412
|
continue;
|
|
373
413
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
414
|
+
|
|
415
|
+
// Calculate deterministic port based on domain CRC32, with collision detection
|
|
416
|
+
const port = this.assignPortToDomain(domain, 4000, 9999);
|
|
417
|
+
|
|
377
418
|
// Create virtual server for the domain
|
|
378
419
|
const virtualServer = this.createVirtualServer(domain);
|
|
379
420
|
this.domainServers[domain] = virtualServer;
|
|
380
|
-
|
|
421
|
+
|
|
381
422
|
// Initialize app with virtual server
|
|
382
423
|
const appHandler = siteApp(virtualServer);
|
|
383
|
-
|
|
424
|
+
|
|
384
425
|
// Create simple dispatcher for this domain
|
|
385
426
|
const dispatcher = (req, res) => {
|
|
386
427
|
// Set fallback handler on virtual server for non-Socket.IO requests
|
|
387
428
|
virtualServer.fallbackHandler = appHandler;
|
|
388
|
-
|
|
429
|
+
|
|
389
430
|
if (virtualServer.requestListeners.length > 0) {
|
|
390
431
|
virtualServer.processRequest(req, res);
|
|
391
432
|
} else if (appHandler) {
|
|
@@ -395,34 +436,32 @@ class Roster {
|
|
|
395
436
|
res.end('Site not found');
|
|
396
437
|
}
|
|
397
438
|
};
|
|
398
|
-
|
|
439
|
+
|
|
399
440
|
// Create HTTP server for this domain
|
|
400
441
|
const httpServer = http.createServer(dispatcher);
|
|
401
442
|
this.portServers[port] = httpServer;
|
|
402
|
-
|
|
443
|
+
|
|
403
444
|
// Handle WebSocket upgrade events
|
|
404
445
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
405
446
|
virtualServer.processUpgrade(req, socket, head);
|
|
406
447
|
});
|
|
407
|
-
|
|
448
|
+
|
|
408
449
|
httpServer.listen(port, 'localhost', () => {
|
|
409
450
|
log.info(`🌐 ${domain} → http://localhost:${port}`);
|
|
410
451
|
});
|
|
411
|
-
|
|
452
|
+
|
|
412
453
|
httpServer.on('error', (error) => {
|
|
413
454
|
log.error(`❌ Error on port ${port} for ${domain}:`, error.message);
|
|
414
455
|
});
|
|
415
|
-
|
|
416
|
-
currentPort++;
|
|
417
456
|
}
|
|
418
|
-
|
|
419
|
-
log.info(`(✔) Started ${
|
|
457
|
+
|
|
458
|
+
log.info(`(✔) Started ${Object.keys(this.portServers).length} sites in local mode`);
|
|
420
459
|
return Promise.resolve();
|
|
421
460
|
}
|
|
422
461
|
|
|
423
462
|
async start() {
|
|
424
463
|
await this.loadSites();
|
|
425
|
-
|
|
464
|
+
|
|
426
465
|
// Skip Greenlock configuration generation in local mode
|
|
427
466
|
if (!this.local) {
|
|
428
467
|
this.generateConfigJson();
|
|
@@ -443,7 +482,7 @@ class Roster {
|
|
|
443
482
|
|
|
444
483
|
return greenlock.ready(glx => {
|
|
445
484
|
const httpServer = glx.httpServer();
|
|
446
|
-
|
|
485
|
+
|
|
447
486
|
// Group sites by port
|
|
448
487
|
const sitesByPort = {};
|
|
449
488
|
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
@@ -455,12 +494,12 @@ class Roster {
|
|
|
455
494
|
appHandlers: {}
|
|
456
495
|
};
|
|
457
496
|
}
|
|
458
|
-
|
|
497
|
+
|
|
459
498
|
// Create completely isolated virtual server
|
|
460
499
|
const virtualServer = this.createVirtualServer(domain);
|
|
461
500
|
sitesByPort[port].virtualServers[domain] = virtualServer;
|
|
462
501
|
this.domainServers[domain] = virtualServer;
|
|
463
|
-
|
|
502
|
+
|
|
464
503
|
// Initialize app with virtual server
|
|
465
504
|
const appHandler = siteApp(virtualServer);
|
|
466
505
|
sitesByPort[port].appHandlers[domain] = appHandler;
|
|
@@ -472,11 +511,11 @@ class Roster {
|
|
|
472
511
|
const createDispatcher = (portData) => {
|
|
473
512
|
return (req, res) => {
|
|
474
513
|
const host = req.headers.host || '';
|
|
475
|
-
|
|
514
|
+
|
|
476
515
|
// Remove port from host header if present (e.g., "domain.com:8080" -> "domain.com")
|
|
477
516
|
const hostWithoutPort = host.split(':')[0];
|
|
478
517
|
const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
|
|
479
|
-
|
|
518
|
+
|
|
480
519
|
// Handle www redirects
|
|
481
520
|
if (hostWithoutPort.startsWith('www.')) {
|
|
482
521
|
res.writeHead(301, { Location: `https://${domain}${req.url}` });
|
|
@@ -486,7 +525,7 @@ class Roster {
|
|
|
486
525
|
|
|
487
526
|
const virtualServer = portData.virtualServers[domain];
|
|
488
527
|
const appHandler = portData.appHandlers[domain];
|
|
489
|
-
|
|
528
|
+
|
|
490
529
|
if (virtualServer && virtualServer.requestListeners.length > 0) {
|
|
491
530
|
// Set fallback handler on virtual server for non-Socket.IO requests
|
|
492
531
|
virtualServer.fallbackHandler = appHandler;
|
|
@@ -512,9 +551,9 @@ class Roster {
|
|
|
512
551
|
const host = req.headers.host || '';
|
|
513
552
|
const hostWithoutPort = host.split(':')[0];
|
|
514
553
|
const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
|
|
515
|
-
|
|
554
|
+
|
|
516
555
|
const virtualServer = portData.virtualServers[domain];
|
|
517
|
-
|
|
556
|
+
|
|
518
557
|
if (virtualServer) {
|
|
519
558
|
virtualServer.processUpgrade(req, socket, head);
|
|
520
559
|
} else {
|
|
@@ -529,15 +568,15 @@ class Roster {
|
|
|
529
568
|
const portNum = parseInt(port);
|
|
530
569
|
const dispatcher = createDispatcher(portData);
|
|
531
570
|
const upgradeHandler = createUpgradeHandler(portData);
|
|
532
|
-
|
|
571
|
+
|
|
533
572
|
if (portNum === this.defaultPort) {
|
|
534
573
|
// Use Greenlock for default port (443) with SSL
|
|
535
574
|
const httpsServer = glx.httpsServer(null, dispatcher);
|
|
536
575
|
this.portServers[portNum] = httpsServer;
|
|
537
|
-
|
|
576
|
+
|
|
538
577
|
// Handle WebSocket upgrade events
|
|
539
578
|
httpsServer.on('upgrade', upgradeHandler);
|
|
540
|
-
|
|
579
|
+
|
|
541
580
|
httpsServer.listen(portNum, this.hostname, () => {
|
|
542
581
|
log.info(`HTTPS server listening on port ${portNum}`);
|
|
543
582
|
});
|
|
@@ -551,12 +590,12 @@ class Roster {
|
|
|
551
590
|
const keyPath = path.join(certPath, 'privkey.pem');
|
|
552
591
|
const certFilePath = path.join(certPath, 'cert.pem');
|
|
553
592
|
const chainPath = path.join(certPath, 'chain.pem');
|
|
554
|
-
|
|
593
|
+
|
|
555
594
|
if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
|
|
556
595
|
const key = fs.readFileSync(keyPath, 'utf8');
|
|
557
596
|
const cert = fs.readFileSync(certFilePath, 'utf8');
|
|
558
597
|
const chain = fs.readFileSync(chainPath, 'utf8');
|
|
559
|
-
|
|
598
|
+
|
|
560
599
|
callback(null, tls.createSecureContext({
|
|
561
600
|
key: key,
|
|
562
601
|
cert: cert + chain
|
|
@@ -569,25 +608,25 @@ class Roster {
|
|
|
569
608
|
}
|
|
570
609
|
}
|
|
571
610
|
};
|
|
572
|
-
|
|
611
|
+
|
|
573
612
|
const httpsServer = https.createServer(httpsOptions, dispatcher);
|
|
574
|
-
|
|
613
|
+
|
|
575
614
|
// Handle WebSocket upgrade events
|
|
576
615
|
httpsServer.on('upgrade', upgradeHandler);
|
|
577
|
-
|
|
616
|
+
|
|
578
617
|
httpsServer.on('error', (error) => {
|
|
579
618
|
log.error(`HTTPS server error on port ${portNum}:`, error.message);
|
|
580
619
|
});
|
|
581
|
-
|
|
620
|
+
|
|
582
621
|
httpsServer.on('tlsClientError', (error) => {
|
|
583
622
|
// Suppress HTTP request errors to avoid log spam
|
|
584
623
|
if (!error.message.includes('http request')) {
|
|
585
624
|
log.error(`TLS error on port ${portNum}:`, error.message);
|
|
586
625
|
}
|
|
587
626
|
});
|
|
588
|
-
|
|
627
|
+
|
|
589
628
|
this.portServers[portNum] = httpsServer;
|
|
590
|
-
|
|
629
|
+
|
|
591
630
|
httpsServer.listen(portNum, this.hostname, (error) => {
|
|
592
631
|
if (error) {
|
|
593
632
|
log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
|
package/package.json
CHANGED
|
File without changes
|