roster-server 1.7.4 → 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.
- package/.claude/settings.local.json +10 -0
- package/README.md +10 -0
- package/index.js +235 -25
- 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/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.
|
|
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}
|
|
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(
|
|
181
|
-
if (!
|
|
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,10 +263,48 @@ class Roster {
|
|
|
192
263
|
|
|
193
264
|
this.domains.push(...domainEntries);
|
|
194
265
|
domainEntries.forEach(d => {
|
|
195
|
-
|
|
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(`✅
|
|
271
|
+
console.log(`✅ Registered site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
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;
|
|
199
308
|
}
|
|
200
309
|
|
|
201
310
|
async start() {
|
|
@@ -210,31 +319,132 @@ class Roster {
|
|
|
210
319
|
staging: this.staging
|
|
211
320
|
});
|
|
212
321
|
|
|
213
|
-
const app = (req, res) => {
|
|
214
|
-
this.handleRequest(req, res);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
322
|
return greenlock.ready(glx => {
|
|
218
|
-
// Obtener los servidores sin iniciarlos
|
|
219
|
-
const httpsServer = glx.httpsServer(null, app);
|
|
220
323
|
const httpServer = glx.httpServer();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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;
|
|
228
346
|
}
|
|
229
347
|
}
|
|
230
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
|
+
|
|
231
381
|
httpServer.listen(80, this.hostname, () => {
|
|
232
|
-
console.log('
|
|
382
|
+
console.log('HTTP server listening on port 80');
|
|
233
383
|
});
|
|
234
384
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
}
|
|
238
448
|
});
|
|
239
449
|
}
|
|
240
450
|
}
|