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 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,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
- const startPort = 3000;
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
- const port = currentPort; // Capture current port value
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 ${currentPort - startPort} sites in local mode`);
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.8.8",
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",