roster-server 1.7.6 → 1.8.0

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,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node:*)",
5
+ "Bash(timeout:*)",
6
+ "Bash(rm:*)"
7
+ ],
8
+ "deny": []
9
+ }
10
+ }
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
package/index.js CHANGED
@@ -1,7 +1,76 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const https = require('https');
4
+ const tls = require('tls');
5
+ const { EventEmitter } = require('events');
3
6
  const Greenlock = require('greenlock-express');
4
7
 
8
+ // Virtual Server that completely isolates applications
9
+ class VirtualServer extends EventEmitter {
10
+ constructor(domain) {
11
+ super();
12
+ this.domain = domain;
13
+ this.requestListeners = [];
14
+
15
+ // Simulate http.Server properties
16
+ this.listening = false;
17
+ this.address = () => ({ port: 443, family: 'IPv4', address: '0.0.0.0' });
18
+ this.timeout = 0;
19
+ this.keepAliveTimeout = 5000;
20
+ this.headersTimeout = 60000;
21
+ this.maxHeadersCount = null;
22
+ }
23
+
24
+ // Override listener methods to capture them
25
+ on(event, listener) {
26
+ if (event === 'request') {
27
+ this.requestListeners.push(listener);
28
+ }
29
+ return super.on(event, listener);
30
+ }
31
+
32
+ addListener(event, listener) {
33
+ return this.on(event, listener);
34
+ }
35
+
36
+ // Socket.IO compatibility methods
37
+ listeners(event) {
38
+ if (event === 'request') {
39
+ return this.requestListeners.slice();
40
+ }
41
+ return super.listeners(event);
42
+ }
43
+
44
+ removeListener(event, listener) {
45
+ if (event === 'request') {
46
+ const index = this.requestListeners.indexOf(listener);
47
+ if (index !== -1) {
48
+ this.requestListeners.splice(index, 1);
49
+ }
50
+ }
51
+ return super.removeListener(event, listener);
52
+ }
53
+
54
+ removeAllListeners(event) {
55
+ if (event === 'request') {
56
+ this.requestListeners = [];
57
+ }
58
+ return super.removeAllListeners(event);
59
+ }
60
+
61
+ // Simulate other http.Server methods
62
+ listen() { this.listening = true; return this; }
63
+ close() { this.listening = false; return this; }
64
+ setTimeout() { return this; }
65
+
66
+ // Process request with this virtual server's listeners
67
+ processRequest(req, res) {
68
+ for (const listener of this.requestListeners) {
69
+ listener(req, res);
70
+ }
71
+ }
72
+ }
73
+
5
74
  class Roster {
6
75
  constructor(options = {}) {
7
76
  this.email = options.email || 'admin@example.com';
@@ -12,6 +81,8 @@ class Roster {
12
81
  this.cluster = options.cluster || false;
13
82
  this.domains = [];
14
83
  this.sites = {};
84
+ this.domainServers = {}; // Store separate servers for each domain
85
+ this.portServers = {}; // Store servers by port
15
86
  this.hostname = options.hostname || '0.0.0.0';
16
87
  this.filename = options.filename || 'index';
17
88
 
@@ -19,7 +90,7 @@ class Roster {
19
90
  if (port === 80) {
20
91
  throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
21
92
  }
22
- this.port = port;
93
+ this.defaultPort = port;
23
94
  }
24
95
 
25
96
  async loadSites() {
@@ -38,7 +109,6 @@ class Roster {
38
109
 
39
110
  const possibleIndexFiles = ['js', 'mjs', 'cjs'].map(ext => `${this.filename}.${ext}`);
40
111
  let siteApp;
41
- let loadedFile;
42
112
 
43
113
  for (const indexFile of possibleIndexFiles) {
44
114
  const indexPath = path.join(domainPath, indexFile);
@@ -51,7 +121,6 @@ class Roster {
51
121
  });
52
122
  // Handle default exports
53
123
  siteApp = siteApp.default || siteApp;
54
- loadedFile = indexFile;
55
124
  break;
56
125
  } catch (err) {
57
126
  console.warn(`⚠️ Error loading ${indexPath}:`, err);
@@ -66,7 +135,7 @@ class Roster {
66
135
  this.sites[d] = siteApp;
67
136
  });
68
137
 
69
- console.log(`✅ Loaded site: ${domain} (using ${loadedFile})`);
138
+ console.log(`✅ Loaded site: ${domain}`);
70
139
  } else {
71
140
  console.warn(`⚠️ No index file (js/mjs/cjs) found in ${domainPath}`);
72
141
  }
@@ -177,14 +246,16 @@ class Roster {
177
246
  }
178
247
  }
179
248
 
180
- register(domain, requestHandler) {
181
- if (!domain) {
249
+ register(domainString, requestHandler) {
250
+ if (!domainString) {
182
251
  throw new Error('Domain is required');
183
252
  }
184
253
  if (typeof requestHandler !== 'function') {
185
254
  throw new Error('requestHandler must be a function');
186
255
  }
187
256
 
257
+ const { domain, port } = this.parseDomainWithPort(domainString);
258
+
188
259
  const domainEntries = [domain];
189
260
  if ((domain.match(/\./g) || []).length < 2) {
190
261
  domainEntries.push(`www.${domain}`);
@@ -192,13 +263,50 @@ class Roster {
192
263
 
193
264
  this.domains.push(...domainEntries);
194
265
  domainEntries.forEach(d => {
195
- this.sites[d] = requestHandler;
266
+ // Store with port information
267
+ const domainKey = port === this.defaultPort ? d : `${d}:${port}`;
268
+ this.sites[domainKey] = requestHandler;
196
269
  });
197
270
 
198
- console.log(`✅ Manually registered site: ${domain}`);
271
+ console.log(`✅ Registered site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
199
272
  return this;
200
273
  }
201
274
 
275
+ parseDomainWithPort(domainString) {
276
+ const parts = domainString.split(':');
277
+ if (parts.length === 2) {
278
+ const domain = parts[0];
279
+ const port = parseInt(parts[1]);
280
+ if (port === 80) {
281
+ throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
282
+ }
283
+ return { domain, port };
284
+ }
285
+ return { domain: domainString, port: this.defaultPort };
286
+ }
287
+
288
+ createVirtualServer(domain) {
289
+ return new VirtualServer(domain);
290
+ }
291
+
292
+ // Get SSL context from Greenlock for custom ports
293
+ async getSSLContext(domain, greenlock) {
294
+ try {
295
+ // Try to get existing certificate for the domain
296
+ const site = await greenlock.get({ servername: domain });
297
+ if (site && site.pems) {
298
+ return {
299
+ key: site.pems.privkey,
300
+ cert: site.pems.cert + site.pems.chain
301
+ };
302
+ }
303
+ } catch (error) {
304
+ }
305
+
306
+ // Return undefined to let HTTPS server handle SNI callback
307
+ return null;
308
+ }
309
+
202
310
  async start() {
203
311
  await this.loadSites();
204
312
  this.generateConfigJson();
@@ -211,31 +319,132 @@ class Roster {
211
319
  staging: this.staging
212
320
  });
213
321
 
214
- const app = (req, res) => {
215
- this.handleRequest(req, res);
216
- };
217
-
218
322
  return greenlock.ready(glx => {
219
- // Obtener los servidores sin iniciarlos
220
- const httpsServer = glx.httpsServer(null, app);
221
323
  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}`);
324
+
325
+ // Group sites by port
326
+ const sitesByPort = {};
327
+ for (const [hostKey, siteApp] of Object.entries(this.sites)) {
328
+ if (!hostKey.startsWith('www.')) {
329
+ const { domain, port } = this.parseDomainWithPort(hostKey);
330
+ if (!sitesByPort[port]) {
331
+ sitesByPort[port] = {
332
+ virtualServers: {},
333
+ appHandlers: {}
334
+ };
335
+ }
336
+
337
+ // Create completely isolated virtual server
338
+ const virtualServer = this.createVirtualServer(domain);
339
+ sitesByPort[port].virtualServers[domain] = virtualServer;
340
+ this.domainServers[domain] = virtualServer;
341
+
342
+ // Initialize app with virtual server
343
+ const appHandler = siteApp(virtualServer);
344
+ sitesByPort[port].appHandlers[domain] = appHandler;
345
+ sitesByPort[port].appHandlers[`www.${domain}`] = appHandler;
229
346
  }
230
347
  }
231
348
 
349
+ // Create dispatcher for each port
350
+ const createDispatcher = (portData) => {
351
+ return (req, res) => {
352
+ const host = req.headers.host || '';
353
+
354
+ // Remove port from host header if present (e.g., "domain.com:8080" -> "domain.com")
355
+ const hostWithoutPort = host.split(':')[0];
356
+ const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
357
+
358
+ // Handle www redirects
359
+ if (hostWithoutPort.startsWith('www.')) {
360
+ res.writeHead(301, { Location: `https://${domain}${req.url}` });
361
+ res.end();
362
+ return;
363
+ }
364
+
365
+ const virtualServer = portData.virtualServers[domain];
366
+ const appHandler = portData.appHandlers[domain];
367
+
368
+ if (virtualServer && virtualServer.requestListeners.length > 0) {
369
+ // App registered listeners on virtual server - use them
370
+ virtualServer.processRequest(req, res);
371
+ } else if (appHandler) {
372
+ // App returned a handler function - use it
373
+ appHandler(req, res);
374
+ } else {
375
+ res.writeHead(404);
376
+ res.end('Site not found');
377
+ }
378
+ };
379
+ };
380
+
232
381
  httpServer.listen(80, this.hostname, () => {
233
- console.log('ℹ️ HTTP server listening on port 80');
382
+ console.log('HTTP server listening on port 80');
234
383
  });
235
384
 
236
- httpsServer.listen(this.port, this.hostname, () => {
237
- console.log('ℹ️ HTTPS server listening on port ' + this.port);
238
- });
385
+ // Handle different port types
386
+ for (const [port, portData] of Object.entries(sitesByPort)) {
387
+ const portNum = parseInt(port);
388
+ const dispatcher = createDispatcher(portData);
389
+
390
+ if (portNum === this.defaultPort) {
391
+ // Use Greenlock for default port (443) with SSL
392
+ const httpsServer = glx.httpsServer(null, dispatcher);
393
+ this.portServers[portNum] = httpsServer;
394
+
395
+ httpsServer.listen(portNum, this.hostname, () => {
396
+ console.log(`HTTPS server listening on port ${portNum}`);
397
+ });
398
+ } else {
399
+ // Create HTTPS server for custom ports using Greenlock certificates
400
+ const httpsOptions = {
401
+ // SNI callback to get certificates dynamically
402
+ SNICallback: (domain, callback) => {
403
+ try {
404
+ const certPath = path.join(this.greenlockStorePath, 'live', domain);
405
+ const keyPath = path.join(certPath, 'privkey.pem');
406
+ const certFilePath = path.join(certPath, 'cert.pem');
407
+ const chainPath = path.join(certPath, 'chain.pem');
408
+
409
+ if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
410
+ const key = fs.readFileSync(keyPath, 'utf8');
411
+ const cert = fs.readFileSync(certFilePath, 'utf8');
412
+ const chain = fs.readFileSync(chainPath, 'utf8');
413
+
414
+ callback(null, tls.createSecureContext({
415
+ key: key,
416
+ cert: cert + chain
417
+ }));
418
+ } else {
419
+ callback(new Error(`No certificate files available for ${domain}`));
420
+ }
421
+ } catch (error) {
422
+ callback(error);
423
+ }
424
+ }
425
+ };
426
+
427
+ const httpsServer = https.createServer(httpsOptions, dispatcher);
428
+
429
+ httpsServer.on('error', (error) => {
430
+ console.error(`HTTPS server error on port ${portNum}:`, error.message);
431
+ });
432
+
433
+ httpsServer.on('tlsClientError', (error) => {
434
+ console.error(`TLS error on port ${portNum}:`, error.message);
435
+ });
436
+
437
+ this.portServers[portNum] = httpsServer;
438
+
439
+ httpsServer.listen(portNum, this.hostname, (error) => {
440
+ if (error) {
441
+ console.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
442
+ } else {
443
+ console.log(`HTTPS server listening on port ${portNum}`);
444
+ }
445
+ });
446
+ }
447
+ }
239
448
  });
240
449
  }
241
450
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "1.7.6",
3
+ "version": "1.8.0",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {