roster-server 1.9.4 → 1.9.8
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/README.md +42 -36
- package/demo/custom-port-range.js +4 -12
- package/demo/environment-aware-url.js +55 -0
- package/demo/local-url-example.js +7 -11
- package/demo/socketio.js +2 -6
- package/demo/test-geturl.js +64 -0
- package/index.js +19 -28
- package/package.json +1 -1
- package/skills/roster-server/SKILL.md +260 -0
package/README.md
CHANGED
|
@@ -18,6 +18,14 @@ Welcome to **RosterServer**, the ultimate domain host router with automatic HTTP
|
|
|
18
18
|
npm install roster-server
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## 🤖 AI Skill
|
|
22
|
+
|
|
23
|
+
You can also add RosterServer as a skill for AI agentic development:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx skills add https://github.com/clasen/RosterServer --skill roster-server
|
|
27
|
+
```
|
|
28
|
+
|
|
21
29
|
## 🛠️ Usage
|
|
22
30
|
|
|
23
31
|
### Directory Structure
|
|
@@ -205,27 +213,11 @@ const roster = new Roster({
|
|
|
205
213
|
});
|
|
206
214
|
```
|
|
207
215
|
|
|
208
|
-
### Getting
|
|
209
|
-
|
|
210
|
-
RosterServer provides two methods to get the local URL for a domain:
|
|
211
|
-
|
|
212
|
-
**1. Static Method (Predictable, No Instance Required):**
|
|
213
|
-
|
|
214
|
-
```javascript
|
|
215
|
-
// Get the URL before starting the server (using default range 4000-9999)
|
|
216
|
-
const url = Roster.getLocalUrl('example.com');
|
|
217
|
-
console.log(url); // http://localhost:9465
|
|
218
|
-
|
|
219
|
-
// Or specify custom port range
|
|
220
|
-
const customUrl = Roster.getLocalUrl('example.com', {
|
|
221
|
-
minLocalPort: 5000,
|
|
222
|
-
maxLocalPort: 6000
|
|
223
|
-
});
|
|
224
|
-
```
|
|
216
|
+
### Getting URLs
|
|
225
217
|
|
|
226
|
-
|
|
218
|
+
RosterServer provides a method to get the URL for a domain that adapts automatically to your environment:
|
|
227
219
|
|
|
228
|
-
**
|
|
220
|
+
**Instance Method: `roster.getUrl(domain)`**
|
|
229
221
|
|
|
230
222
|
```javascript
|
|
231
223
|
const roster = new Roster({ local: true });
|
|
@@ -233,29 +225,43 @@ roster.register('example.com', handler);
|
|
|
233
225
|
|
|
234
226
|
await roster.start();
|
|
235
227
|
|
|
236
|
-
// Get the
|
|
237
|
-
const url = roster.
|
|
238
|
-
console.log(url);
|
|
228
|
+
// Get the URL - automatically adapts to environment
|
|
229
|
+
const url = roster.getUrl('example.com');
|
|
230
|
+
console.log(url);
|
|
231
|
+
// Local mode: http://localhost:9465
|
|
232
|
+
// Production mode: https://example.com
|
|
239
233
|
```
|
|
240
234
|
|
|
241
|
-
This
|
|
235
|
+
This method:
|
|
236
|
+
- Returns the correct URL based on your environment (`local: true/false`)
|
|
237
|
+
- In **local mode**: Returns `http://localhost:{port}` with the assigned port
|
|
238
|
+
- In **production mode**: Returns `https://{domain}` (or with custom port if configured)
|
|
239
|
+
- Handles `www.` prefix automatically (returns same URL)
|
|
240
|
+
- Returns `null` for domains that aren't registered
|
|
242
241
|
|
|
243
242
|
**Example Usage:**
|
|
244
243
|
|
|
245
244
|
```javascript
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
// Local development
|
|
246
|
+
const localRoster = new Roster({ local: true });
|
|
247
|
+
localRoster.register('example.com', handler);
|
|
248
|
+
await localRoster.start();
|
|
249
|
+
console.log(localRoster.getUrl('example.com'));
|
|
250
|
+
// → http://localhost:9465
|
|
251
|
+
|
|
252
|
+
// Production
|
|
253
|
+
const prodRoster = new Roster({ local: false });
|
|
254
|
+
prodRoster.register('example.com', handler);
|
|
255
|
+
await prodRoster.start();
|
|
256
|
+
console.log(prodRoster.getUrl('example.com'));
|
|
257
|
+
// → https://example.com
|
|
258
|
+
|
|
259
|
+
// Production with custom port
|
|
260
|
+
const customRoster = new Roster({ local: false, port: 8443 });
|
|
261
|
+
customRoster.register('api.example.com', handler);
|
|
262
|
+
await customRoster.start();
|
|
263
|
+
console.log(customRoster.getUrl('api.example.com'));
|
|
264
|
+
// → https://api.example.com:8443
|
|
259
265
|
```
|
|
260
266
|
|
|
261
267
|
## 🧂 A Touch of Magic
|
|
@@ -7,15 +7,7 @@ const roster = new Roster({
|
|
|
7
7
|
maxLocalPort: 5100 // Custom maximum port
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
console.log('\n
|
|
11
|
-
console.log('example.com →', Roster.getLocalUrl('example.com', {
|
|
12
|
-
minLocalPort: 5000,
|
|
13
|
-
maxLocalPort: 5100
|
|
14
|
-
}));
|
|
15
|
-
console.log('api.example.com →', Roster.getLocalUrl('api.example.com', {
|
|
16
|
-
minLocalPort: 5000,
|
|
17
|
-
maxLocalPort: 5100
|
|
18
|
-
}));
|
|
10
|
+
console.log('\n🔧 Creating server with custom port range (5000-5100)...\n');
|
|
19
11
|
|
|
20
12
|
roster.register('example.com', (httpsServer) => {
|
|
21
13
|
return (req, res) => {
|
|
@@ -32,9 +24,9 @@ roster.register('api.example.com', (httpsServer) => {
|
|
|
32
24
|
});
|
|
33
25
|
|
|
34
26
|
roster.start().then(() => {
|
|
35
|
-
console.log('
|
|
36
|
-
console.log('example.com →', roster.
|
|
37
|
-
console.log('api.example.com →', roster.
|
|
27
|
+
console.log('🚀 Server Started with custom port range:');
|
|
28
|
+
console.log('example.com →', roster.getUrl('example.com'));
|
|
29
|
+
console.log('api.example.com →', roster.getUrl('api.example.com'));
|
|
38
30
|
|
|
39
31
|
console.log('\n✅ Both domains running in custom port range (5000-5100)!');
|
|
40
32
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const Roster = require('../index.js');
|
|
2
|
+
|
|
3
|
+
// Example 1: Local mode
|
|
4
|
+
console.log('\n📍 EXAMPLE 1: Local Development Mode\n');
|
|
5
|
+
const localRoster = new Roster({ local: true });
|
|
6
|
+
|
|
7
|
+
localRoster.register('example.com', (httpsServer) => {
|
|
8
|
+
return (req, res) => {
|
|
9
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
10
|
+
res.end('Hello from local!');
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
localRoster.start().then(() => {
|
|
15
|
+
console.log('Local URL:', localRoster.getUrl('example.com'));
|
|
16
|
+
console.log('→ Returns: http://localhost:{port}\n');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Example 2: Production mode (simulated, without actually starting)
|
|
20
|
+
console.log('📍 EXAMPLE 2: Production Mode (simulated)\n');
|
|
21
|
+
const prodRoster = new Roster({
|
|
22
|
+
local: false,
|
|
23
|
+
email: 'admin@example.com'
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
prodRoster.register('example.com', (httpsServer) => {
|
|
27
|
+
return (req, res) => {
|
|
28
|
+
res.writeHead(200);
|
|
29
|
+
res.end('Hello from production!');
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Don't actually start (would need real SSL), just show what URL would be
|
|
34
|
+
console.log('Production URL (without starting):', 'https://example.com');
|
|
35
|
+
console.log('→ Would return: https://example.com\n');
|
|
36
|
+
|
|
37
|
+
// Example 3: Production with custom port
|
|
38
|
+
console.log('📍 EXAMPLE 3: Production with Custom Port (simulated)\n');
|
|
39
|
+
const customPortRoster = new Roster({
|
|
40
|
+
local: false,
|
|
41
|
+
port: 8443,
|
|
42
|
+
email: 'admin@example.com'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
customPortRoster.register('api.example.com', (httpsServer) => {
|
|
46
|
+
return (req, res) => {
|
|
47
|
+
res.writeHead(200);
|
|
48
|
+
res.end('API');
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log('Custom port URL (without starting):', 'https://api.example.com:8443');
|
|
53
|
+
console.log('→ Would return: https://api.example.com:8443\n');
|
|
54
|
+
|
|
55
|
+
console.log('✅ getUrl() adapts to the environment automatically!');
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
const Roster = require('../index.js');
|
|
2
2
|
|
|
3
|
-
// Example
|
|
4
|
-
console.log('\n
|
|
5
|
-
console.log('example.com →', Roster.getLocalUrl('example.com'));
|
|
6
|
-
console.log('api.example.com →', Roster.getLocalUrl('api.example.com'));
|
|
7
|
-
console.log('test.example.com →', Roster.getLocalUrl('test.example.com'));
|
|
3
|
+
// Example: Get URL after registration (adapts to environment)
|
|
4
|
+
console.log('\n🔧 Creating local development server...\n');
|
|
8
5
|
|
|
9
|
-
// Example 2: Get URL after registration (instance method)
|
|
10
6
|
const roster = new Roster({ local: true });
|
|
11
7
|
|
|
12
8
|
roster.register('example.com', (httpsServer) => {
|
|
@@ -24,17 +20,17 @@ roster.register('api.example.com', (httpsServer) => {
|
|
|
24
20
|
});
|
|
25
21
|
|
|
26
22
|
roster.start().then(() => {
|
|
27
|
-
console.log('
|
|
28
|
-
console.log('example.com →', roster.
|
|
29
|
-
console.log('api.example.com →', roster.
|
|
23
|
+
console.log('🚀 Server Started - URLs (based on environment):');
|
|
24
|
+
console.log('example.com →', roster.getUrl('example.com'));
|
|
25
|
+
console.log('api.example.com →', roster.getUrl('api.example.com'));
|
|
30
26
|
|
|
31
27
|
// Test with www prefix (should return same URL)
|
|
32
28
|
console.log('\n🔄 Testing www prefix handling:');
|
|
33
|
-
console.log('www.example.com →', roster.
|
|
29
|
+
console.log('www.example.com →', roster.getUrl('www.example.com'));
|
|
34
30
|
|
|
35
31
|
// Test non-existent domain
|
|
36
32
|
console.log('\n❌ Testing non-existent domain:');
|
|
37
|
-
console.log('nonexistent.com →', roster.
|
|
33
|
+
console.log('nonexistent.com →', roster.getUrl('nonexistent.com') || 'null (domain not registered)');
|
|
38
34
|
|
|
39
35
|
console.log('\n✅ All domains running!');
|
|
40
36
|
});
|
package/demo/socketio.js
CHANGED
|
@@ -27,11 +27,7 @@ roster.register('example.com', (httpsServer) => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
roster.start().then(() => {
|
|
30
|
-
// Get
|
|
31
|
-
const url = roster.
|
|
30
|
+
// Get URL for registered domain (adapts to environment)
|
|
31
|
+
const url = roster.getUrl('example.com');
|
|
32
32
|
console.log(`✅ Socket.IO server available at: ${url}`);
|
|
33
|
-
|
|
34
|
-
// Get local URL without instance (static method - predictable port)
|
|
35
|
-
const staticUrl = Roster.getLocalUrl('example.com');
|
|
36
|
-
console.log(`ℹ️ Static prediction: ${staticUrl}`);
|
|
37
33
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const Roster = require('../index.js');
|
|
2
|
+
|
|
3
|
+
console.log('\n🧪 Testing getUrl() method in different scenarios\n');
|
|
4
|
+
|
|
5
|
+
// Test 1: Local mode with default port
|
|
6
|
+
console.log('TEST 1: Local mode (default port)');
|
|
7
|
+
const local1 = new Roster({ local: true });
|
|
8
|
+
local1.register('example.com', () => (req, res) => res.end('OK'));
|
|
9
|
+
local1.start().then(() => {
|
|
10
|
+
const url = local1.getUrl('example.com');
|
|
11
|
+
console.log(`✓ example.com → ${url}`);
|
|
12
|
+
console.assert(url.startsWith('http://localhost:'), 'Should be localhost HTTP');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Test 2: Local mode with custom port range
|
|
16
|
+
console.log('\nTEST 2: Local mode (custom port range)');
|
|
17
|
+
const local2 = new Roster({ local: true, minLocalPort: 6000, maxLocalPort: 6100 });
|
|
18
|
+
local2.register('test.com', () => (req, res) => res.end('OK'));
|
|
19
|
+
local2.start().then(() => {
|
|
20
|
+
const url = local2.getUrl('test.com');
|
|
21
|
+
console.log(`✓ test.com → ${url}`);
|
|
22
|
+
const port = parseInt(url.split(':')[2]);
|
|
23
|
+
console.assert(port >= 6000 && port <= 6100, 'Port should be in custom range');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Test 3: Production mode (default HTTPS port)
|
|
27
|
+
console.log('\nTEST 3: Production mode (default HTTPS)');
|
|
28
|
+
const prod1 = new Roster({ local: false, email: 'admin@example.com' });
|
|
29
|
+
prod1.register('example.com', () => (req, res) => res.end('OK'));
|
|
30
|
+
const prodUrl1 = prod1.getUrl('example.com');
|
|
31
|
+
console.log(`✓ example.com → ${prodUrl1}`);
|
|
32
|
+
console.assert(prodUrl1 === 'https://example.com', 'Should be HTTPS without port');
|
|
33
|
+
|
|
34
|
+
// Test 4: Production mode (custom port)
|
|
35
|
+
console.log('\nTEST 4: Production mode (custom port)');
|
|
36
|
+
const prod2 = new Roster({ local: false, port: 8443, email: 'admin@example.com' });
|
|
37
|
+
prod2.register('api.example.com', () => (req, res) => res.end('OK'));
|
|
38
|
+
const prodUrl2 = prod2.getUrl('api.example.com');
|
|
39
|
+
console.log(`✓ api.example.com → ${prodUrl2}`);
|
|
40
|
+
console.assert(prodUrl2 === 'https://api.example.com:8443', 'Should include custom port');
|
|
41
|
+
|
|
42
|
+
// Test 5: www prefix handling
|
|
43
|
+
console.log('\nTEST 5: www prefix handling');
|
|
44
|
+
const local3 = new Roster({ local: true });
|
|
45
|
+
local3.register('example.com', () => (req, res) => res.end('OK'));
|
|
46
|
+
local3.start().then(() => {
|
|
47
|
+
const url1 = local3.getUrl('example.com');
|
|
48
|
+
const url2 = local3.getUrl('www.example.com');
|
|
49
|
+
console.log(`✓ example.com → ${url1}`);
|
|
50
|
+
console.log(`✓ www.example.com → ${url2}`);
|
|
51
|
+
console.assert(url1 === url2, 'www should return same URL');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Test 6: Non-existent domain
|
|
55
|
+
console.log('\nTEST 6: Non-existent domain');
|
|
56
|
+
const local4 = new Roster({ local: true });
|
|
57
|
+
local4.register('example.com', () => (req, res) => res.end('OK'));
|
|
58
|
+
local4.start().then(() => {
|
|
59
|
+
const url = local4.getUrl('nonexistent.com');
|
|
60
|
+
console.log(`✓ nonexistent.com → ${url}`);
|
|
61
|
+
console.assert(url === null, 'Should return null for unregistered domain');
|
|
62
|
+
|
|
63
|
+
console.log('\n✅ All tests passed!\n');
|
|
64
|
+
});
|
package/index.js
CHANGED
|
@@ -367,41 +367,32 @@ class Roster {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/**
|
|
370
|
-
* Get the
|
|
371
|
-
* @param {string} domain - The domain name (e.g., 'example.com')
|
|
372
|
-
* @param {Object} options - Optional configuration
|
|
373
|
-
* @param {number} options.minLocalPort - Minimum port range (default: 4000)
|
|
374
|
-
* @param {number} options.maxLocalPort - Maximum port range (default: 9999)
|
|
375
|
-
* @returns {string} The local URL (e.g., 'http://localhost:4321')
|
|
376
|
-
*/
|
|
377
|
-
static getLocalUrl(domain, options = {}) {
|
|
378
|
-
const minPort = options.minLocalPort || 4000;
|
|
379
|
-
const maxPort = options.maxLocalPort || 9999;
|
|
380
|
-
|
|
381
|
-
// Remove www prefix if present
|
|
382
|
-
const cleanDomain = domain.startsWith('www.') ? domain.slice(4) : domain;
|
|
383
|
-
|
|
384
|
-
// Calculate deterministic port
|
|
385
|
-
const port = domainToPort(cleanDomain, minPort, maxPort);
|
|
386
|
-
|
|
387
|
-
return `http://localhost:${port}`;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Get the local URL for a domain that was registered on this instance
|
|
370
|
+
* Get the URL for a domain based on the current environment
|
|
392
371
|
* @param {string} domain - The domain name
|
|
393
|
-
* @returns {string|null} The
|
|
372
|
+
* @returns {string|null} The URL if domain is registered, null otherwise
|
|
394
373
|
*/
|
|
395
|
-
|
|
374
|
+
getUrl(domain) {
|
|
396
375
|
// Remove www prefix if present
|
|
397
376
|
const cleanDomain = domain.startsWith('www.') ? domain.slice(4) : domain;
|
|
398
377
|
|
|
399
|
-
// Check if domain
|
|
400
|
-
|
|
401
|
-
|
|
378
|
+
// Check if domain is registered
|
|
379
|
+
const isRegistered = this.sites[cleanDomain] || this.sites[`www.${cleanDomain}`];
|
|
380
|
+
if (!isRegistered) {
|
|
381
|
+
return null;
|
|
402
382
|
}
|
|
403
383
|
|
|
404
|
-
|
|
384
|
+
// Return URL based on environment
|
|
385
|
+
if (this.local) {
|
|
386
|
+
// Local mode: return localhost URL with assigned port
|
|
387
|
+
if (this.domainPorts && this.domainPorts[cleanDomain]) {
|
|
388
|
+
return `http://localhost:${this.domainPorts[cleanDomain]}`;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
} else {
|
|
392
|
+
// Production mode: return HTTPS URL
|
|
393
|
+
const port = this.defaultPort === 443 ? '' : `:${this.defaultPort}`;
|
|
394
|
+
return `https://${cleanDomain}${port}`;
|
|
395
|
+
}
|
|
405
396
|
}
|
|
406
397
|
|
|
407
398
|
createVirtualServer(domain) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# RosterServer Skill
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
RosterServer is a virtual hosting platform for multiple HTTPS sites with automatic SSL via Let's Encrypt. Each domain gets an isolated `VirtualServer` instance to prevent configuration conflicts between different application types (Express, Socket.IO, etc).
|
|
6
|
+
|
|
7
|
+
## Quick Setup
|
|
8
|
+
|
|
9
|
+
### Production
|
|
10
|
+
```javascript
|
|
11
|
+
const Roster = require('roster-server');
|
|
12
|
+
|
|
13
|
+
const roster = new Roster({
|
|
14
|
+
email: 'admin@example.com',
|
|
15
|
+
wwwPath: '/srv/www',
|
|
16
|
+
greenlockStorePath: '/srv/greenlock.d',
|
|
17
|
+
staging: false // Use true for testing
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
roster.start();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Local Development
|
|
24
|
+
```javascript
|
|
25
|
+
const roster = new Roster({
|
|
26
|
+
local: true, // HTTP mode, no SSL
|
|
27
|
+
wwwPath: './www',
|
|
28
|
+
minLocalPort: 4000, // Optional
|
|
29
|
+
maxLocalPort: 9999 // Optional
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
roster.start().then(() => {
|
|
33
|
+
console.log('example.com:', roster.getUrl('example.com'));
|
|
34
|
+
// → http://localhost:9465 (deterministic CRC32-based port)
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Directory Structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
project/
|
|
42
|
+
├── greenlock.d/ # SSL certificates (auto-generated)
|
|
43
|
+
├── www/
|
|
44
|
+
│ ├── example.com/
|
|
45
|
+
│ │ └── index.js # Handler for example.com
|
|
46
|
+
│ └── api.example.com/
|
|
47
|
+
│ └── index.js # Handler for subdomain
|
|
48
|
+
└── server.js # Your setup
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Handler Patterns
|
|
52
|
+
|
|
53
|
+
Each `www/{domain}/index.js` must export a function that receives `httpsServer` and returns a request handler.
|
|
54
|
+
|
|
55
|
+
### Pattern 1: Basic HTTP Handler
|
|
56
|
+
```javascript
|
|
57
|
+
module.exports = (httpsServer) => {
|
|
58
|
+
return (req, res) => {
|
|
59
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
60
|
+
res.end('Hello World');
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Pattern 2: Express App
|
|
66
|
+
```javascript
|
|
67
|
+
const express = require('express');
|
|
68
|
+
|
|
69
|
+
module.exports = (httpsServer) => {
|
|
70
|
+
const app = express();
|
|
71
|
+
|
|
72
|
+
app.get('/', (req, res) => res.send('Hello'));
|
|
73
|
+
app.post('/api/data', (req, res) => res.json({ ok: true }));
|
|
74
|
+
|
|
75
|
+
return app;
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Pattern 3: Socket.IO
|
|
80
|
+
```javascript
|
|
81
|
+
const { Server } = require('socket.io');
|
|
82
|
+
|
|
83
|
+
module.exports = (httpsServer) => {
|
|
84
|
+
const io = new Server(httpsServer);
|
|
85
|
+
|
|
86
|
+
io.on('connection', (socket) => {
|
|
87
|
+
socket.on('message', (data) => io.emit('message', data));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return (req, res) => {
|
|
91
|
+
if (req.url && req.url.startsWith(io.opts.path)) return;
|
|
92
|
+
res.writeHead(200);
|
|
93
|
+
res.end('Socket.IO running');
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Pattern 4: Manual Registration
|
|
99
|
+
```javascript
|
|
100
|
+
// In server.js, before roster.start()
|
|
101
|
+
roster.register('example.com', (httpsServer) => {
|
|
102
|
+
return (req, res) => {
|
|
103
|
+
res.writeHead(200);
|
|
104
|
+
res.end('Manual handler');
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// With custom port
|
|
109
|
+
roster.register('api.example.com:8443', handler);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Key Configuration Options
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
new Roster({
|
|
116
|
+
email: 'admin@example.com', // Required for SSL
|
|
117
|
+
wwwPath: '/srv/www', // Site handlers directory
|
|
118
|
+
greenlockStorePath: '/srv/greenlock.d', // SSL storage
|
|
119
|
+
|
|
120
|
+
// Environment
|
|
121
|
+
local: false, // true = HTTP, false = HTTPS
|
|
122
|
+
staging: false, // true = Let's Encrypt staging
|
|
123
|
+
|
|
124
|
+
// Server
|
|
125
|
+
hostname: '0.0.0.0',
|
|
126
|
+
port: 443, // Default HTTPS port (NOT 80!)
|
|
127
|
+
|
|
128
|
+
// Local mode
|
|
129
|
+
minLocalPort: 4000,
|
|
130
|
+
maxLocalPort: 9999,
|
|
131
|
+
|
|
132
|
+
// Advanced
|
|
133
|
+
filename: 'index', // Handler filename (no extension)
|
|
134
|
+
basePath: '/srv' // Base for relative paths
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Core API
|
|
139
|
+
|
|
140
|
+
### `roster.start()`
|
|
141
|
+
Loads sites, generates SSL config, starts servers. Returns `Promise<void>`.
|
|
142
|
+
|
|
143
|
+
### `roster.register(domain, handler)`
|
|
144
|
+
Manually register a domain handler. Domain can include port: `'api.com:8443'`.
|
|
145
|
+
|
|
146
|
+
### `roster.getUrl(domain)`
|
|
147
|
+
Get environment-aware URL:
|
|
148
|
+
- Local mode: `http://localhost:{port}`
|
|
149
|
+
- Production: `https://{domain}` or `https://{domain}:{port}`
|
|
150
|
+
- Returns `null` if domain not registered
|
|
151
|
+
|
|
152
|
+
## How It Works
|
|
153
|
+
|
|
154
|
+
### Request Flow
|
|
155
|
+
1. Request arrives → Dispatcher extracts `Host` header
|
|
156
|
+
2. Strips `www.` prefix (301 redirect if present)
|
|
157
|
+
3. Looks up domain → Gets `VirtualServer` instance
|
|
158
|
+
4. Routes to handler via `virtualServer.processRequest(req, res)`
|
|
159
|
+
|
|
160
|
+
### VirtualServer Architecture
|
|
161
|
+
Each domain gets isolated server instance that simulates `http.Server`:
|
|
162
|
+
- Captures `request` and `upgrade` event listeners
|
|
163
|
+
- Complete separation between domains
|
|
164
|
+
- No configuration conflicts between apps
|
|
165
|
+
|
|
166
|
+
### Port Assignment
|
|
167
|
+
**Production**: Default 443, custom via `domain:port` syntax
|
|
168
|
+
**Local**: CRC32 hash of domain → deterministic port in range 4000-9999
|
|
169
|
+
**Reserved**: Port 80 for ACME challenges only
|
|
170
|
+
|
|
171
|
+
### SSL Management
|
|
172
|
+
- Automatic Let's Encrypt certificate generation
|
|
173
|
+
- Auto-renewal 45 days before expiration
|
|
174
|
+
- SNI support for multiple domains
|
|
175
|
+
- Custom ports reuse certificates via SNI callback
|
|
176
|
+
|
|
177
|
+
## Common Issues & Solutions
|
|
178
|
+
|
|
179
|
+
**Port 443 in use**: Use different port `{ port: 8443 }`
|
|
180
|
+
**Certificate failed**: Check firewall (ports 80, 443), verify DNS, try `staging: true`
|
|
181
|
+
**Site not found**: Verify directory name matches domain, check `index.js` exports function
|
|
182
|
+
**Local port conflict**: Adjust `minLocalPort`/`maxLocalPort` range
|
|
183
|
+
**Socket.IO not working**: Ensure handler checks `io.opts.path` and returns properly
|
|
184
|
+
|
|
185
|
+
## Best Practices
|
|
186
|
+
|
|
187
|
+
1. **Test with staging first**: `staging: true` to avoid Let's Encrypt rate limits
|
|
188
|
+
2. **Use local mode for dev**: `local: true` for faster iteration
|
|
189
|
+
3. **Environment variables**: Configure via `process.env` for portability
|
|
190
|
+
4. **Error handling**: Wrap handlers with try/catch, don't expose internals
|
|
191
|
+
5. **Socket.IO paths**: Always check `req.url.startsWith(io.opts.path)` in returned handler
|
|
192
|
+
6. **Port 80**: Never use as HTTPS port (reserved for ACME)
|
|
193
|
+
|
|
194
|
+
## Quick Examples
|
|
195
|
+
|
|
196
|
+
### Full Production Setup
|
|
197
|
+
```javascript
|
|
198
|
+
const Roster = require('roster-server');
|
|
199
|
+
|
|
200
|
+
const roster = new Roster({
|
|
201
|
+
email: process.env.ADMIN_EMAIL,
|
|
202
|
+
wwwPath: '/srv/www',
|
|
203
|
+
greenlockStorePath: '/srv/greenlock.d',
|
|
204
|
+
staging: process.env.NODE_ENV !== 'production'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
roster.start().then(() => {
|
|
208
|
+
console.log('RosterServer running');
|
|
209
|
+
}).catch(err => {
|
|
210
|
+
console.error('Startup failed:', err);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Local Dev with Manual Registration
|
|
216
|
+
```javascript
|
|
217
|
+
const roster = new Roster({ local: true, wwwPath: './www' });
|
|
218
|
+
|
|
219
|
+
roster.register('test.local', (server) => {
|
|
220
|
+
return (req, res) => {
|
|
221
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
222
|
+
res.end(JSON.stringify({ status: 'ok', url: roster.getUrl('test.local') }));
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
roster.start();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Environment-Aware Configuration
|
|
230
|
+
```javascript
|
|
231
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
232
|
+
|
|
233
|
+
const roster = new Roster({
|
|
234
|
+
email: process.env.ADMIN_EMAIL || 'admin@example.com',
|
|
235
|
+
wwwPath: process.env.WWW_PATH || './www',
|
|
236
|
+
greenlockStorePath: process.env.SSL_PATH || './greenlock.d',
|
|
237
|
+
local: !isProduction,
|
|
238
|
+
staging: !isProduction,
|
|
239
|
+
minLocalPort: parseInt(process.env.MIN_PORT) || 4000,
|
|
240
|
+
maxLocalPort: parseInt(process.env.MAX_PORT) || 9999
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
roster.start();
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Implementation Checklist
|
|
247
|
+
|
|
248
|
+
When implementing RosterServer:
|
|
249
|
+
|
|
250
|
+
- [ ] Create `www/` directory structure with domain folders
|
|
251
|
+
- [ ] Each domain has `index.js` exporting `(httpsServer) => handler`
|
|
252
|
+
- [ ] Configure email for Let's Encrypt notifications
|
|
253
|
+
- [ ] Test with `local: true` first
|
|
254
|
+
- [ ] Test with `staging: true` before production
|
|
255
|
+
- [ ] Ensure ports 80 and 443 are open (production)
|
|
256
|
+
- [ ] Verify DNS points to server
|
|
257
|
+
- [ ] Never use port 80 as HTTPS port
|
|
258
|
+
- [ ] Use `roster.getUrl(domain)` for environment-aware URLs
|
|
259
|
+
- [ ] Handle Socket.IO paths correctly in returned handler
|
|
260
|
+
- [ ] Implement error handling in handlers
|