roster-server 1.8.0 → 1.8.4
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 +2 -1
- package/README.md +25 -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 +87 -18
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -162,6 +162,31 @@ When creating a new `RosterServer` instance, you can pass the following options:
|
|
|
162
162
|
- `wwwPath` (string): Path to your `www` directory containing your sites.
|
|
163
163
|
- `greenlockStorePath` (string): Directory for Greenlock configuration.
|
|
164
164
|
- `staging` (boolean): Set to `true` to use Let's Encrypt's staging environment (for testing).
|
|
165
|
+
- `local` (boolean): Set to `true` to run in local development mode.
|
|
166
|
+
|
|
167
|
+
## 🏠 Local Development Mode
|
|
168
|
+
|
|
169
|
+
For local development and testing, you can run RosterServer in local mode by setting `local: true`. This mode is perfect for development environments where you don't need SSL certificates or production features.
|
|
170
|
+
|
|
171
|
+
When `{ local: true }` is enabled, RosterServer **Skips SSL/HTTPS**: Runs pure HTTP servers instead of HTTPS.
|
|
172
|
+
|
|
173
|
+
### Setting Up Local Mode
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
const server = new Roster({
|
|
177
|
+
wwwPath: '/srv/www',
|
|
178
|
+
local: true // Enable local development mode
|
|
179
|
+
});
|
|
180
|
+
server.start();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Port Assignment
|
|
184
|
+
|
|
185
|
+
In local mode, domains are automatically assigned ports starting from 3000:
|
|
186
|
+
|
|
187
|
+
- `example.com` → `http://localhost:3000`
|
|
188
|
+
- `api.example.com` → `http://localhost:3001`
|
|
189
|
+
- And so on...
|
|
165
190
|
|
|
166
191
|
## 🧂 A Touch of Magic
|
|
167
192
|
|
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,9 +1,11 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const http = require('http');
|
|
3
4
|
const https = require('https');
|
|
4
5
|
const tls = require('tls');
|
|
5
6
|
const { EventEmitter } = require('events');
|
|
6
7
|
const Greenlock = require('greenlock-express');
|
|
8
|
+
const log = require('lemonlog')('roster');
|
|
7
9
|
|
|
8
10
|
// Virtual Server that completely isolates applications
|
|
9
11
|
class VirtualServer extends EventEmitter {
|
|
@@ -79,6 +81,7 @@ class Roster {
|
|
|
79
81
|
this.greenlockStorePath = options.greenlockStorePath || path.join(basePath, 'greenlock.d');
|
|
80
82
|
this.staging = options.staging || false;
|
|
81
83
|
this.cluster = options.cluster || false;
|
|
84
|
+
this.local = options.local || false;
|
|
82
85
|
this.domains = [];
|
|
83
86
|
this.sites = {};
|
|
84
87
|
this.domainServers = {}; // Store separate servers for each domain
|
|
@@ -87,7 +90,7 @@ class Roster {
|
|
|
87
90
|
this.filename = options.filename || 'index';
|
|
88
91
|
|
|
89
92
|
const port = options.port === undefined ? 443 : options.port;
|
|
90
|
-
if (port === 80) {
|
|
93
|
+
if (port === 80 && !this.local) {
|
|
91
94
|
throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
|
|
92
95
|
}
|
|
93
96
|
this.defaultPort = port;
|
|
@@ -96,7 +99,7 @@ class Roster {
|
|
|
96
99
|
async loadSites() {
|
|
97
100
|
// Check if wwwPath exists
|
|
98
101
|
if (!fs.existsSync(this.wwwPath)) {
|
|
99
|
-
|
|
102
|
+
log.warn(`⚠️ WWW path does not exist: ${this.wwwPath}`);
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
102
105
|
|
|
@@ -123,7 +126,7 @@ class Roster {
|
|
|
123
126
|
siteApp = siteApp.default || siteApp;
|
|
124
127
|
break;
|
|
125
128
|
} catch (err) {
|
|
126
|
-
|
|
129
|
+
log.warn(`⚠️ Error loading ${indexPath}:`, err);
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
}
|
|
@@ -135,9 +138,9 @@ class Roster {
|
|
|
135
138
|
this.sites[d] = siteApp;
|
|
136
139
|
});
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
log.info(`(✔) Loaded site: ${domain}`);
|
|
139
142
|
} else {
|
|
140
|
-
|
|
143
|
+
log.warn(`⚠️ No index file (js/mjs/cjs) found in ${domainPath}`);
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
}
|
|
@@ -215,16 +218,16 @@ class Roster {
|
|
|
215
218
|
const currentConfigContentFormatted = JSON.stringify(currentConfig, null, 2);
|
|
216
219
|
|
|
217
220
|
if (newConfigContent === currentConfigContentFormatted) {
|
|
218
|
-
|
|
221
|
+
log.info('ℹ️ Configuration has not changed. config.json will not be overwritten.');
|
|
219
222
|
return;
|
|
220
223
|
}
|
|
221
|
-
|
|
224
|
+
log.info('🔄 Configuration has changed. config.json will be updated.');
|
|
222
225
|
} else {
|
|
223
|
-
|
|
226
|
+
log.info('🆕 config.json does not exist. A new one will be created.');
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
|
227
|
-
|
|
230
|
+
log.info(`📁 config.json generated at ${configPath}`);
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
handleRequest(req, res) {
|
|
@@ -268,7 +271,7 @@ class Roster {
|
|
|
268
271
|
this.sites[domainKey] = requestHandler;
|
|
269
272
|
});
|
|
270
273
|
|
|
271
|
-
|
|
274
|
+
log.info(`(✔) Registered site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
|
|
272
275
|
return this;
|
|
273
276
|
}
|
|
274
277
|
|
|
@@ -277,7 +280,7 @@ class Roster {
|
|
|
277
280
|
if (parts.length === 2) {
|
|
278
281
|
const domain = parts[0];
|
|
279
282
|
const port = parseInt(parts[1]);
|
|
280
|
-
if (port === 80) {
|
|
283
|
+
if (port === 80 && !this.local) {
|
|
281
284
|
throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
|
|
282
285
|
}
|
|
283
286
|
return { domain, port };
|
|
@@ -307,9 +310,72 @@ class Roster {
|
|
|
307
310
|
return null;
|
|
308
311
|
}
|
|
309
312
|
|
|
313
|
+
// Start server in local mode with HTTP - simplified version
|
|
314
|
+
startLocalMode() {
|
|
315
|
+
const startPort = 3000;
|
|
316
|
+
let currentPort = startPort;
|
|
317
|
+
|
|
318
|
+
// Create a simple HTTP server for each domain with sequential ports
|
|
319
|
+
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
320
|
+
const domain = hostKey.split(':')[0]; // Remove port if present
|
|
321
|
+
|
|
322
|
+
// Skip www domains in local mode
|
|
323
|
+
if (domain.startsWith('www.')) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const port = currentPort; // Capture current port value
|
|
328
|
+
|
|
329
|
+
// Create virtual server for the domain
|
|
330
|
+
const virtualServer = this.createVirtualServer(domain);
|
|
331
|
+
this.domainServers[domain] = virtualServer;
|
|
332
|
+
|
|
333
|
+
// Initialize app with virtual server
|
|
334
|
+
const appHandler = siteApp(virtualServer);
|
|
335
|
+
|
|
336
|
+
// Create simple dispatcher for this domain
|
|
337
|
+
const dispatcher = (req, res) => {
|
|
338
|
+
if (virtualServer.requestListeners.length > 0) {
|
|
339
|
+
virtualServer.processRequest(req, res);
|
|
340
|
+
} else if (appHandler) {
|
|
341
|
+
appHandler(req, res);
|
|
342
|
+
} else {
|
|
343
|
+
res.writeHead(404);
|
|
344
|
+
res.end('Site not found');
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Create HTTP server for this domain
|
|
349
|
+
const httpServer = http.createServer(dispatcher);
|
|
350
|
+
this.portServers[port] = httpServer;
|
|
351
|
+
|
|
352
|
+
httpServer.listen(port, 'localhost', () => {
|
|
353
|
+
log.info(`🌐 ${domain} → http://localhost:${port}`);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
httpServer.on('error', (error) => {
|
|
357
|
+
log.error(`❌ Error on port ${port} for ${domain}:`, error.message);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
currentPort++;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
log.info(`(✔) Started ${currentPort - startPort} sites in local mode`);
|
|
364
|
+
return Promise.resolve();
|
|
365
|
+
}
|
|
366
|
+
|
|
310
367
|
async start() {
|
|
311
368
|
await this.loadSites();
|
|
312
|
-
|
|
369
|
+
|
|
370
|
+
// Skip Greenlock configuration generation in local mode
|
|
371
|
+
if (!this.local) {
|
|
372
|
+
this.generateConfigJson();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Handle local mode with simple HTTP server
|
|
376
|
+
if (this.local) {
|
|
377
|
+
return this.startLocalMode();
|
|
378
|
+
}
|
|
313
379
|
|
|
314
380
|
const greenlock = Greenlock.init({
|
|
315
381
|
packageRoot: __dirname,
|
|
@@ -379,7 +445,7 @@ class Roster {
|
|
|
379
445
|
};
|
|
380
446
|
|
|
381
447
|
httpServer.listen(80, this.hostname, () => {
|
|
382
|
-
|
|
448
|
+
log.info('HTTP server listening on port 80');
|
|
383
449
|
});
|
|
384
450
|
|
|
385
451
|
// Handle different port types
|
|
@@ -393,7 +459,7 @@ class Roster {
|
|
|
393
459
|
this.portServers[portNum] = httpsServer;
|
|
394
460
|
|
|
395
461
|
httpsServer.listen(portNum, this.hostname, () => {
|
|
396
|
-
|
|
462
|
+
log.info(`HTTPS server listening on port ${portNum}`);
|
|
397
463
|
});
|
|
398
464
|
} else {
|
|
399
465
|
// Create HTTPS server for custom ports using Greenlock certificates
|
|
@@ -427,20 +493,23 @@ class Roster {
|
|
|
427
493
|
const httpsServer = https.createServer(httpsOptions, dispatcher);
|
|
428
494
|
|
|
429
495
|
httpsServer.on('error', (error) => {
|
|
430
|
-
|
|
496
|
+
log.error(`HTTPS server error on port ${portNum}:`, error.message);
|
|
431
497
|
});
|
|
432
498
|
|
|
433
499
|
httpsServer.on('tlsClientError', (error) => {
|
|
434
|
-
|
|
500
|
+
// Suppress HTTP request errors to avoid log spam
|
|
501
|
+
if (!error.message.includes('http request')) {
|
|
502
|
+
log.error(`TLS error on port ${portNum}:`, error.message);
|
|
503
|
+
}
|
|
435
504
|
});
|
|
436
505
|
|
|
437
506
|
this.portServers[portNum] = httpsServer;
|
|
438
507
|
|
|
439
508
|
httpsServer.listen(portNum, this.hostname, (error) => {
|
|
440
509
|
if (error) {
|
|
441
|
-
|
|
510
|
+
log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
|
|
442
511
|
} else {
|
|
443
|
-
|
|
512
|
+
log.info(`HTTPS server listening on port ${portNum}`);
|
|
444
513
|
}
|
|
445
514
|
});
|
|
446
515
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roster-server",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://github.com/clasen/RosterServer#readme",
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"greenlock-express": "^4.0.3"
|
|
37
|
+
"greenlock-express": "^4.0.3",
|
|
38
|
+
"lemonlog": "^1.2.0"
|
|
38
39
|
}
|
|
39
40
|
}
|