roster-server 1.7.6 → 1.8.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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node:*)",
5
+ "Bash(timeout:*)",
6
+ "Bash(rm:*)",
7
+ "Bash(git checkout:*)"
8
+ ],
9
+ "deny": []
10
+ }
11
+ }
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
@@ -2,6 +2,7 @@ const Roster = require('../../index.js');
2
2
  const path = require('path');
3
3
 
4
4
  const roster = new Roster({
5
+ local: true,
5
6
  email: 'admin@example.com',
6
7
  wwwPath: path.join(__dirname, '..', 'www'),
7
8
  });
@@ -1,6 +1,6 @@
1
1
  module.exports = (httpsServer) => {
2
2
  return (req, res) => {
3
3
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
4
- res.end('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
4
+ res.end('"example.com: Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
5
5
  };
6
6
  };
@@ -4,7 +4,7 @@ module.exports = (httpsServer) => {
4
4
  const app = express();
5
5
  app.get('/', (req, res) => {
6
6
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
7
- res.send('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
7
+ res.send('"subdomain.example.com: Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
8
8
  });
9
9
 
10
10
  return app;
@@ -1,6 +1,6 @@
1
1
  module.exports = (httpsServer) => {
2
2
  return (req, res) => {
3
3
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
4
- res.end('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
4
+ res.end('"subdomain.example.com: Crazy from thinking, wanting to be reasonable, and the heart has reasons that reason itself will never understand."');
5
5
  };
6
6
  };
package/index.js CHANGED
@@ -1,7 +1,77 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const tls = require('tls');
6
+ const { EventEmitter } = require('events');
3
7
  const Greenlock = require('greenlock-express');
4
8
 
9
+ // Virtual Server that completely isolates applications
10
+ class VirtualServer extends EventEmitter {
11
+ constructor(domain) {
12
+ super();
13
+ this.domain = domain;
14
+ this.requestListeners = [];
15
+
16
+ // Simulate http.Server properties
17
+ this.listening = false;
18
+ this.address = () => ({ port: 443, family: 'IPv4', address: '0.0.0.0' });
19
+ this.timeout = 0;
20
+ this.keepAliveTimeout = 5000;
21
+ this.headersTimeout = 60000;
22
+ this.maxHeadersCount = null;
23
+ }
24
+
25
+ // Override listener methods to capture them
26
+ on(event, listener) {
27
+ if (event === 'request') {
28
+ this.requestListeners.push(listener);
29
+ }
30
+ return super.on(event, listener);
31
+ }
32
+
33
+ addListener(event, listener) {
34
+ return this.on(event, listener);
35
+ }
36
+
37
+ // Socket.IO compatibility methods
38
+ listeners(event) {
39
+ if (event === 'request') {
40
+ return this.requestListeners.slice();
41
+ }
42
+ return super.listeners(event);
43
+ }
44
+
45
+ removeListener(event, listener) {
46
+ if (event === 'request') {
47
+ const index = this.requestListeners.indexOf(listener);
48
+ if (index !== -1) {
49
+ this.requestListeners.splice(index, 1);
50
+ }
51
+ }
52
+ return super.removeListener(event, listener);
53
+ }
54
+
55
+ removeAllListeners(event) {
56
+ if (event === 'request') {
57
+ this.requestListeners = [];
58
+ }
59
+ return super.removeAllListeners(event);
60
+ }
61
+
62
+ // Simulate other http.Server methods
63
+ listen() { this.listening = true; return this; }
64
+ close() { this.listening = false; return this; }
65
+ setTimeout() { return this; }
66
+
67
+ // Process request with this virtual server's listeners
68
+ processRequest(req, res) {
69
+ for (const listener of this.requestListeners) {
70
+ listener(req, res);
71
+ }
72
+ }
73
+ }
74
+
5
75
  class Roster {
6
76
  constructor(options = {}) {
7
77
  this.email = options.email || 'admin@example.com';
@@ -10,16 +80,19 @@ class Roster {
10
80
  this.greenlockStorePath = options.greenlockStorePath || path.join(basePath, 'greenlock.d');
11
81
  this.staging = options.staging || false;
12
82
  this.cluster = options.cluster || false;
83
+ this.local = options.local || false; // New local mode option
13
84
  this.domains = [];
14
85
  this.sites = {};
86
+ this.domainServers = {}; // Store separate servers for each domain
87
+ this.portServers = {}; // Store servers by port
15
88
  this.hostname = options.hostname || '0.0.0.0';
16
89
  this.filename = options.filename || 'index';
17
90
 
18
91
  const port = options.port === undefined ? 443 : options.port;
19
- if (port === 80) {
92
+ if (port === 80 && !this.local) {
20
93
  throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
21
94
  }
22
- this.port = port;
95
+ this.defaultPort = port;
23
96
  }
24
97
 
25
98
  async loadSites() {
@@ -38,7 +111,6 @@ class Roster {
38
111
 
39
112
  const possibleIndexFiles = ['js', 'mjs', 'cjs'].map(ext => `${this.filename}.${ext}`);
40
113
  let siteApp;
41
- let loadedFile;
42
114
 
43
115
  for (const indexFile of possibleIndexFiles) {
44
116
  const indexPath = path.join(domainPath, indexFile);
@@ -51,7 +123,6 @@ class Roster {
51
123
  });
52
124
  // Handle default exports
53
125
  siteApp = siteApp.default || siteApp;
54
- loadedFile = indexFile;
55
126
  break;
56
127
  } catch (err) {
57
128
  console.warn(`⚠️ Error loading ${indexPath}:`, err);
@@ -66,7 +137,7 @@ class Roster {
66
137
  this.sites[d] = siteApp;
67
138
  });
68
139
 
69
- console.log(`✅ Loaded site: ${domain} (using ${loadedFile})`);
140
+ console.log(`✅ Loaded site: ${domain}`);
70
141
  } else {
71
142
  console.warn(`⚠️ No index file (js/mjs/cjs) found in ${domainPath}`);
72
143
  }
@@ -177,14 +248,16 @@ class Roster {
177
248
  }
178
249
  }
179
250
 
180
- register(domain, requestHandler) {
181
- if (!domain) {
251
+ register(domainString, requestHandler) {
252
+ if (!domainString) {
182
253
  throw new Error('Domain is required');
183
254
  }
184
255
  if (typeof requestHandler !== 'function') {
185
256
  throw new Error('requestHandler must be a function');
186
257
  }
187
258
 
259
+ const { domain, port } = this.parseDomainWithPort(domainString);
260
+
188
261
  const domainEntries = [domain];
189
262
  if ((domain.match(/\./g) || []).length < 2) {
190
263
  domainEntries.push(`www.${domain}`);
@@ -192,16 +265,116 @@ class Roster {
192
265
 
193
266
  this.domains.push(...domainEntries);
194
267
  domainEntries.forEach(d => {
195
- this.sites[d] = requestHandler;
268
+ // Store with port information
269
+ const domainKey = port === this.defaultPort ? d : `${d}:${port}`;
270
+ this.sites[domainKey] = requestHandler;
196
271
  });
197
272
 
198
- console.log(`✅ Manually registered site: ${domain}`);
273
+ console.log(`✅ Registered site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
199
274
  return this;
200
275
  }
201
276
 
277
+ parseDomainWithPort(domainString) {
278
+ const parts = domainString.split(':');
279
+ if (parts.length === 2) {
280
+ const domain = parts[0];
281
+ const port = parseInt(parts[1]);
282
+ if (port === 80 && !this.local) {
283
+ throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
284
+ }
285
+ return { domain, port };
286
+ }
287
+ return { domain: domainString, port: this.defaultPort };
288
+ }
289
+
290
+ createVirtualServer(domain) {
291
+ return new VirtualServer(domain);
292
+ }
293
+
294
+ // Get SSL context from Greenlock for custom ports
295
+ async getSSLContext(domain, greenlock) {
296
+ try {
297
+ // Try to get existing certificate for the domain
298
+ const site = await greenlock.get({ servername: domain });
299
+ if (site && site.pems) {
300
+ return {
301
+ key: site.pems.privkey,
302
+ cert: site.pems.cert + site.pems.chain
303
+ };
304
+ }
305
+ } catch (error) {
306
+ }
307
+
308
+ // Return undefined to let HTTPS server handle SNI callback
309
+ return null;
310
+ }
311
+
312
+ // Start server in local mode with HTTP - simplified version
313
+ startLocalMode() {
314
+ const startPort = 3000;
315
+ let currentPort = startPort;
316
+
317
+ // Create a simple HTTP server for each domain with sequential ports
318
+ for (const [hostKey, siteApp] of Object.entries(this.sites)) {
319
+ const domain = hostKey.split(':')[0]; // Remove port if present
320
+
321
+ // Skip www domains in local mode
322
+ if (domain.startsWith('www.')) {
323
+ continue;
324
+ }
325
+
326
+ const port = currentPort; // Capture current port value
327
+
328
+ // Create virtual server for the domain
329
+ const virtualServer = this.createVirtualServer(domain);
330
+ this.domainServers[domain] = virtualServer;
331
+
332
+ // Initialize app with virtual server
333
+ const appHandler = siteApp(virtualServer);
334
+
335
+ // Create simple dispatcher for this domain
336
+ const dispatcher = (req, res) => {
337
+ if (virtualServer.requestListeners.length > 0) {
338
+ virtualServer.processRequest(req, res);
339
+ } else if (appHandler) {
340
+ appHandler(req, res);
341
+ } else {
342
+ res.writeHead(404);
343
+ res.end('Site not found');
344
+ }
345
+ };
346
+
347
+ // Create HTTP server for this domain
348
+ const httpServer = http.createServer(dispatcher);
349
+ this.portServers[port] = httpServer;
350
+
351
+ httpServer.listen(port, 'localhost', () => {
352
+ console.log(`🌐 ${domain} → http://localhost:${port}`);
353
+ });
354
+
355
+ httpServer.on('error', (error) => {
356
+ console.error(`❌ Error on port ${port} for ${domain}:`, error.message);
357
+ });
358
+
359
+ currentPort++;
360
+ }
361
+
362
+ console.log(`✅ Started ${currentPort - startPort} sites in local mode`);
363
+ return Promise.resolve();
364
+ }
365
+
202
366
  async start() {
203
367
  await this.loadSites();
204
- this.generateConfigJson();
368
+
369
+ // Skip Greenlock configuration generation in local mode
370
+ if (!this.local) {
371
+ this.generateConfigJson();
372
+ }
373
+
374
+ // Handle local mode with simple HTTP server
375
+ if (this.local) {
376
+ return this.startLocalMode();
377
+ }
205
378
 
206
379
  const greenlock = Greenlock.init({
207
380
  packageRoot: __dirname,
@@ -211,31 +384,135 @@ class Roster {
211
384
  staging: this.staging
212
385
  });
213
386
 
214
- const app = (req, res) => {
215
- this.handleRequest(req, res);
216
- };
217
-
218
387
  return greenlock.ready(glx => {
219
- // Obtener los servidores sin iniciarlos
220
- const httpsServer = glx.httpsServer(null, app);
221
388
  const httpServer = glx.httpServer();
222
-
223
- for (const [host, siteApp] of Object.entries(this.sites)) {
224
- if (!host.startsWith('www.')) {
225
- const appInstance = siteApp(httpsServer);
226
- this.sites[host] = appInstance;
227
- this.sites[`www.${host}`] = appInstance;
228
- console.log(`🔧 Initialized server for ${host}`);
389
+
390
+ // Group sites by port
391
+ const sitesByPort = {};
392
+ for (const [hostKey, siteApp] of Object.entries(this.sites)) {
393
+ if (!hostKey.startsWith('www.')) {
394
+ const { domain, port } = this.parseDomainWithPort(hostKey);
395
+ if (!sitesByPort[port]) {
396
+ sitesByPort[port] = {
397
+ virtualServers: {},
398
+ appHandlers: {}
399
+ };
400
+ }
401
+
402
+ // Create completely isolated virtual server
403
+ const virtualServer = this.createVirtualServer(domain);
404
+ sitesByPort[port].virtualServers[domain] = virtualServer;
405
+ this.domainServers[domain] = virtualServer;
406
+
407
+ // Initialize app with virtual server
408
+ const appHandler = siteApp(virtualServer);
409
+ sitesByPort[port].appHandlers[domain] = appHandler;
410
+ sitesByPort[port].appHandlers[`www.${domain}`] = appHandler;
229
411
  }
230
412
  }
231
413
 
414
+ // Create dispatcher for each port
415
+ const createDispatcher = (portData) => {
416
+ return (req, res) => {
417
+ const host = req.headers.host || '';
418
+
419
+ // Remove port from host header if present (e.g., "domain.com:8080" -> "domain.com")
420
+ const hostWithoutPort = host.split(':')[0];
421
+ const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
422
+
423
+ // Handle www redirects
424
+ if (hostWithoutPort.startsWith('www.')) {
425
+ res.writeHead(301, { Location: `https://${domain}${req.url}` });
426
+ res.end();
427
+ return;
428
+ }
429
+
430
+ const virtualServer = portData.virtualServers[domain];
431
+ const appHandler = portData.appHandlers[domain];
432
+
433
+ if (virtualServer && virtualServer.requestListeners.length > 0) {
434
+ // App registered listeners on virtual server - use them
435
+ virtualServer.processRequest(req, res);
436
+ } else if (appHandler) {
437
+ // App returned a handler function - use it
438
+ appHandler(req, res);
439
+ } else {
440
+ res.writeHead(404);
441
+ res.end('Site not found');
442
+ }
443
+ };
444
+ };
445
+
232
446
  httpServer.listen(80, this.hostname, () => {
233
- console.log('ℹ️ HTTP server listening on port 80');
447
+ console.log('HTTP server listening on port 80');
234
448
  });
235
449
 
236
- httpsServer.listen(this.port, this.hostname, () => {
237
- console.log('ℹ️ HTTPS server listening on port ' + this.port);
238
- });
450
+ // Handle different port types
451
+ for (const [port, portData] of Object.entries(sitesByPort)) {
452
+ const portNum = parseInt(port);
453
+ const dispatcher = createDispatcher(portData);
454
+
455
+ if (portNum === this.defaultPort) {
456
+ // Use Greenlock for default port (443) with SSL
457
+ const httpsServer = glx.httpsServer(null, dispatcher);
458
+ this.portServers[portNum] = httpsServer;
459
+
460
+ httpsServer.listen(portNum, this.hostname, () => {
461
+ console.log(`HTTPS server listening on port ${portNum}`);
462
+ });
463
+ } else {
464
+ // Create HTTPS server for custom ports using Greenlock certificates
465
+ const httpsOptions = {
466
+ // SNI callback to get certificates dynamically
467
+ SNICallback: (domain, callback) => {
468
+ try {
469
+ const certPath = path.join(this.greenlockStorePath, 'live', domain);
470
+ const keyPath = path.join(certPath, 'privkey.pem');
471
+ const certFilePath = path.join(certPath, 'cert.pem');
472
+ const chainPath = path.join(certPath, 'chain.pem');
473
+
474
+ if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
475
+ const key = fs.readFileSync(keyPath, 'utf8');
476
+ const cert = fs.readFileSync(certFilePath, 'utf8');
477
+ const chain = fs.readFileSync(chainPath, 'utf8');
478
+
479
+ callback(null, tls.createSecureContext({
480
+ key: key,
481
+ cert: cert + chain
482
+ }));
483
+ } else {
484
+ callback(new Error(`No certificate files available for ${domain}`));
485
+ }
486
+ } catch (error) {
487
+ callback(error);
488
+ }
489
+ }
490
+ };
491
+
492
+ const httpsServer = https.createServer(httpsOptions, dispatcher);
493
+
494
+ httpsServer.on('error', (error) => {
495
+ console.error(`HTTPS server error on port ${portNum}:`, error.message);
496
+ });
497
+
498
+ httpsServer.on('tlsClientError', (error) => {
499
+ // Suppress HTTP request errors to avoid log spam
500
+ if (!error.message.includes('http request')) {
501
+ console.error(`TLS error on port ${portNum}:`, error.message);
502
+ }
503
+ });
504
+
505
+ this.portServers[portNum] = httpsServer;
506
+
507
+ httpsServer.listen(portNum, this.hostname, (error) => {
508
+ if (error) {
509
+ console.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
510
+ } else {
511
+ console.log(`HTTPS server listening on port ${portNum}`);
512
+ }
513
+ });
514
+ }
515
+ }
239
516
  });
240
517
  }
241
518
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "1.7.6",
3
+ "version": "1.8.2",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {