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.
- package/.claude/settings.local.json +11 -0
- package/README.md +10 -0
- package/demo/roster/server.js +1 -0
- package/demo/www/example.com/index.js +1 -1
- package/demo/www/express.example.com/index.js +1 -1
- package/demo/www/subdomain.example.com/index.js +1 -1
- package/index.js +304 -27
- package/package.json +1 -1
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/demo/roster/server.js
CHANGED
|
@@ -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('"
|
|
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.
|
|
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}
|
|
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(
|
|
181
|
-
if (!
|
|
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
|
-
|
|
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(`✅
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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('
|
|
447
|
+
console.log('HTTP server listening on port 80');
|
|
234
448
|
});
|
|
235
449
|
|
|
236
|
-
|
|
237
|
-
|
|
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
|
}
|