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 starting from 3000:
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:3000`
191
- - `api.example.com` → `http://localhost:3001`
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
- const startPort = 3000;
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
- const port = currentPort; // Capture current port value
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 ${currentPort - startPort} sites in local mode`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "1.9.0",
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": {