roster-server 2.4.11 β 2.4.13
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 +192 -404
- package/index.js +420 -128
- package/lib/static-site-handler.js +36 -27
- package/package.json +1 -1
- package/skills/roster-server/SKILL.md +50 -338
- package/test/https-integration.test.js +149 -0
- package/test/lifecycle.test.js +469 -0
- package/test/roster-server.test.js +59 -14
- package/test/scanner-blocker.test.js +3 -6
- package/test/static-and-tls.test.js +273 -0
- package/vendor/greenlock-express/greenlock-express.js +1 -1
- package/vendor/greenlock-express/servers.js +2 -1
- package/vendor/greenlock-express/single.js +1 -1
- package/vendor/greenlock-express/worker.js +2 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { pipeline } = require('stream/promises');
|
|
5
6
|
|
|
6
7
|
const MIME_BY_EXT = {
|
|
7
8
|
html: 'text/html',
|
|
@@ -71,7 +72,7 @@ function createStaticHandler(rootPath) {
|
|
|
71
72
|
const root = path.resolve(rootPath);
|
|
72
73
|
|
|
73
74
|
return function staticSiteFactory(virtualServer) {
|
|
74
|
-
return function staticHandler(req, res) {
|
|
75
|
+
return async function staticHandler(req, res) {
|
|
75
76
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
76
77
|
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
|
77
78
|
res.end('Method Not Allowed');
|
|
@@ -90,40 +91,48 @@ function createStaticHandler(rootPath) {
|
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!stat.isDirectory()) {
|
|
94
|
+
let file;
|
|
95
|
+
try {
|
|
96
|
+
let servePath = filePath;
|
|
97
|
+
let stat = await fs.promises.stat(servePath);
|
|
98
|
+
if (stat.isDirectory()) {
|
|
99
|
+
servePath = path.join(servePath, 'index.html');
|
|
100
|
+
stat = await fs.promises.stat(servePath);
|
|
101
|
+
}
|
|
102
|
+
if (!stat.isFile()) {
|
|
103
103
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
104
104
|
res.end('Not Found');
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
file = await fs.promises.open(servePath, 'r');
|
|
108
|
+
stat = await file.stat();
|
|
109
|
+
if (res.destroyed) return;
|
|
110
|
+
if (!stat.isFile()) {
|
|
109
111
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
110
112
|
res.end('Not Found');
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
res.
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
res.writeHead(200, {
|
|
116
|
+
'Content-Type': getContentType(servePath),
|
|
117
|
+
'Content-Length': stat.size
|
|
118
|
+
});
|
|
119
|
+
if (req.method === 'HEAD') {
|
|
120
|
+
res.end();
|
|
121
|
+
} else {
|
|
122
|
+
await pipeline(file.createReadStream(), res);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (res.destroyed || res.writableEnded) return;
|
|
126
|
+
if (res.headersSent) {
|
|
127
|
+
res.destroy(error);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const missing = error.code === 'ENOENT' || error.code === 'ENOTDIR';
|
|
131
|
+
const forbidden = error.code === 'EACCES' || error.code === 'EPERM';
|
|
132
|
+
res.writeHead(missing ? 404 : forbidden ? 403 : 500, { 'Content-Type': 'text/plain' });
|
|
133
|
+
res.end(missing ? 'Not Found' : forbidden ? 'Forbidden' : 'Internal Server Error');
|
|
134
|
+
} finally {
|
|
135
|
+
if (file) await file.close();
|
|
127
136
|
}
|
|
128
137
|
};
|
|
129
138
|
};
|
package/package.json
CHANGED
|
@@ -1,367 +1,79 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: roster-server
|
|
3
|
-
description:
|
|
3
|
+
description: Integrate and troubleshoot roster-server in applications using domain routing, HTTPS certificates, local HTTP, Express, Socket.IO, external servers, and graceful shutdown. Use when adding, configuring, or debugging RosterServer in a service.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# RosterServer integration
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
```javascript
|
|
10
|
-
const Roster = require('roster-server');
|
|
11
|
-
|
|
12
|
-
const roster = new Roster({
|
|
13
|
-
email: 'admin@example.com',
|
|
14
|
-
wwwPath: '/srv/www',
|
|
15
|
-
greenlockStorePath: '/srv/greenlock.d',
|
|
16
|
-
local: true
|
|
17
|
-
});
|
|
8
|
+
Use this skill to configure a consuming application. Keep its existing configuration source, module format, and process lifecycle. Check the installed package's API when version differences matter; the upstream README may describe changes not yet installed.
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### Local Development
|
|
23
|
-
```javascript
|
|
24
|
-
const roster = new Roster({
|
|
25
|
-
local: true, // HTTP mode, no SSL
|
|
26
|
-
wwwPath: './www'
|
|
27
|
-
});
|
|
10
|
+
## Choose the lifecycle owner
|
|
28
11
|
|
|
29
|
-
roster.start().
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Directory Structure
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
project/
|
|
39
|
-
βββ greenlock.d/ # SSL certificates (auto-generated)
|
|
40
|
-
βββ www/
|
|
41
|
-
β βββ example.com/
|
|
42
|
-
β β βββ index.js # Handler for example.com
|
|
43
|
-
β βββ api.example.com/
|
|
44
|
-
β β βββ index.js # Handler for subdomain
|
|
45
|
-
β βββ static-site.com/ # Static site (no index.js)
|
|
46
|
-
β β βββ index.html
|
|
47
|
-
β β βββ css/
|
|
48
|
-
β β βββ images/
|
|
49
|
-
β βββ *.example.com/
|
|
50
|
-
β βββ index.js # Wildcard: one handler for all subdomains
|
|
51
|
-
βββ server.js # Your setup
|
|
52
|
-
```
|
|
12
|
+
- **Local development:** `new Roster({ local: true, wwwPath: absolutePath })`, register sites, then `await roster.start()`. Local mode binds to `localhost` and assigns ports; read URLs with `roster.getUrl(domain)` after startup.
|
|
13
|
+
- **Standalone HTTPS:** set the real `email`, absolute `wwwPath` and `greenlockStorePath`; leave `local` false. `await roster.start()` owns ACME HTTP on port 80 and the configured HTTPS listeners. A different HTTPS port does not eliminate the need for port 80.
|
|
14
|
+
- **External server/worker:** `await roster.init()`, then `roster.attach(server, { port })` or an HTTPS helper. The caller owns listening and closing that server. `port` on `attach()` selects the routing table, not the TCP port.
|
|
15
|
+
- **Serving-only worker:** use `autoCertificates: false` with `init()` and `createServingHttpsServer({ servername })`. A separate manager must provide certificates first. Calling `start()` still enables the standalone ACME lifecycle.
|
|
53
16
|
|
|
54
|
-
|
|
17
|
+
`init()` invokes site factories and can write certificate configuration/start renewal work; it is not a side-effect-free inspection method. Keep domains and certificate configuration consistent across workers sharing a store. The legacy `cluster: true` launcher delegates process management to Greenlock; it is separate from using Roster with an external cluster manager.
|
|
55
18
|
|
|
56
|
-
##
|
|
19
|
+
## Sites and routing
|
|
57
20
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
**Static site**: If the domain folder has no index script but has `index.html`, RosterServer serves the folder as static files (`GET /` β `index.html`, other paths β file or 404, path-traversal protected). No code required.
|
|
61
|
-
|
|
62
|
-
### Pattern 1: Basic HTTP Handler
|
|
63
|
-
```javascript
|
|
64
|
-
module.exports = (httpsServer) => {
|
|
65
|
-
return (req, res) => {
|
|
66
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
67
|
-
res.end('Hello World');
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Pattern 2: Express App
|
|
73
|
-
```javascript
|
|
74
|
-
const express = require('express');
|
|
75
|
-
|
|
76
|
-
module.exports = (httpsServer) => {
|
|
77
|
-
const app = express();
|
|
78
|
-
|
|
79
|
-
app.get('/', (req, res) => res.send('Hello'));
|
|
80
|
-
app.post('/api/data', (req, res) => res.json({ ok: true }));
|
|
81
|
-
|
|
82
|
-
return app;
|
|
83
|
-
};
|
|
84
|
-
```
|
|
21
|
+
Register sites before `init()`/`start()`. Export a synchronous factory `(virtualServer) => requestHandler`; the returned `(req, res)` handler may be async. An Express app is a valid return value. Never call `app.listen()` inside the factory.
|
|
85
22
|
|
|
86
|
-
### Pattern 3: Socket.IO
|
|
87
23
|
```javascript
|
|
88
|
-
const { Server } = require('socket.io');
|
|
89
|
-
|
|
90
|
-
module.exports = (httpsServer) => {
|
|
91
|
-
const io = new Server(httpsServer);
|
|
92
|
-
|
|
93
|
-
io.on('connection', (socket) => {
|
|
94
|
-
socket.on('message', (data) => io.emit('message', data));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return (req, res) => {
|
|
98
|
-
if (req.url && req.url.startsWith(io.opts.path)) return;
|
|
99
|
-
res.writeHead(200);
|
|
100
|
-
res.end('Socket.IO running');
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Pattern 4: Manual Registration
|
|
106
|
-
```javascript
|
|
107
|
-
// In server.js, before roster.start()
|
|
108
|
-
roster.register('example.com', (httpsServer) => {
|
|
109
|
-
return (req, res) => {
|
|
110
|
-
res.writeHead(200);
|
|
111
|
-
res.end('Manual handler');
|
|
112
|
-
};
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// With custom port
|
|
116
|
-
roster.register('api.example.com:8443', handler);
|
|
117
|
-
|
|
118
|
-
// Wildcard: one handler for all subdomains (default port or custom)
|
|
119
|
-
roster.register('*.example.com', handler);
|
|
120
|
-
roster.register('*.example.com:8080', handler);
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Pattern 5: Static Site (no code)
|
|
124
|
-
Place only `index.html` (and assets) in `www/example.com/`. No `index.js` needed. RosterServer serves files with path-traversal protection; `/` β `index.html`, other paths β file or 404. Implemented in `lib/static-site-handler.js` and `lib/resolve-site-app.js`.
|
|
125
|
-
|
|
126
|
-
### Pattern 6: Cluster-Friendly (external server)
|
|
127
|
-
```javascript
|
|
128
|
-
const https = require('https');
|
|
129
24
|
const Roster = require('roster-server');
|
|
25
|
+
const path = require('node:path');
|
|
130
26
|
|
|
131
|
-
const roster = new Roster({
|
|
132
|
-
|
|
133
|
-
wwwPath: '/srv/www',
|
|
134
|
-
greenlockStorePath: '/srv/greenlock.d'
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
await roster.init();
|
|
138
|
-
|
|
139
|
-
const server = https.createServer({ SNICallback: roster.sniCallback() });
|
|
140
|
-
roster.attach(server);
|
|
141
|
-
|
|
142
|
-
// Master passes connections β worker never calls listen()
|
|
143
|
-
process.on('message', (msg, connection) => {
|
|
144
|
-
if (msg === 'sticky-session:connection') {
|
|
145
|
-
server.emit('connection', connection);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
27
|
+
const roster = new Roster({ local: true, wwwPath: path.resolve('www') });
|
|
28
|
+
roster.register('example.com', () => (req, res) => res.end('Hello'));
|
|
148
29
|
```
|
|
149
30
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
manager.register('example.com', () => (req, res) => res.end('manager'));
|
|
159
|
-
await manager.start();
|
|
160
|
-
await manager.ensureCertificate('example.com');
|
|
31
|
+
- Discovery tries `index.js`, `index.mjs`, then `index.cjs`; use the module format appropriate to the application's package. Without an entry script, `index.html` enables static serving.
|
|
32
|
+
- `filename` changes the script basename only. `basePath` supplies default directories; it does not rebase explicitly supplied relative paths. Prefer absolute paths.
|
|
33
|
+
- Discovered sites stay on HTTPS **443**, even with another default `port`. Discovery replaces a manual registration for the same domain/port; distinct ports coexist.
|
|
34
|
+
- Manual `register('api.example.com:8443', factory)` selects that port. Exact hosts precede wildcard matches; `*.example.com` does not match the apex.
|
|
35
|
+
- Static serving streams files and supports HEAD and directory indexes, with strict missing-file 404s and no SPA fallback.
|
|
36
|
+
- Local ports are hash-based with in-instance collision handling, not OS availability detection. Do not hardcode example port numbers.
|
|
37
|
+
- Production `getUrl()` uses the instance's default port; it is not a per-registration custom-port lookup. A site registered only on another port can return `null`.
|
|
161
38
|
|
|
162
|
-
|
|
163
|
-
const worker = new Roster({
|
|
164
|
-
email: 'admin@example.com',
|
|
165
|
-
greenlockStorePath: '/srv/greenlock.d',
|
|
166
|
-
wwwPath: '/srv/www',
|
|
167
|
-
autoCertificates: false
|
|
168
|
-
});
|
|
169
|
-
worker.register('example.com', () => (req, res) => res.end('worker'));
|
|
170
|
-
await worker.init();
|
|
171
|
-
const httpsServer = await worker.createServingHttpsServer({ servername: 'example.com' });
|
|
172
|
-
httpsServer.listen(4336);
|
|
173
|
-
```
|
|
39
|
+
## Socket.IO and asynchronous requests
|
|
174
40
|
|
|
175
|
-
|
|
176
|
-
```javascript
|
|
177
|
-
const Roster = require('roster-server');
|
|
178
|
-
const { createScannerBlocker } = require('roster-server/plugins/scanner-blocker.js');
|
|
41
|
+
Attach Socket.IO to the supplied virtual server. Return the ordinary HTTP handler and register cleanup in the same factory:
|
|
179
42
|
|
|
180
|
-
const roster = new Roster({ local: true, wwwPath: './www' });
|
|
181
|
-
roster.use(createScannerBlocker());
|
|
182
|
-
roster.start();
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
The plugin blocks common PHP, WordPress, repository, and sensitive-file probes before site handlers. Defaults are a 60-second window, 3 strikes, a 15-minute ban, 10,000 tracked clients, and `trustProxy: false`; pass only the values to override. Set `trustProxy: true` only behind a trusted reverse proxy that overwrites `X-Forwarded-For`. Ban state is per process and in memory; use `onBlock` to integrate a shared firewall or Fail2ban.
|
|
186
|
-
|
|
187
|
-
## Key Configuration Options
|
|
188
|
-
|
|
189
|
-
```javascript
|
|
190
|
-
new Roster({
|
|
191
|
-
email: 'admin@example.com', // Required for SSL
|
|
192
|
-
wwwPath: '/srv/www', // Site handlers directory
|
|
193
|
-
greenlockStorePath: '/srv/greenlock.d', // SSL storage
|
|
194
|
-
dnsChallenge: { ... }, // Optional override. Default is local/manual DNS-01 (acme-dns-01-cli)
|
|
195
|
-
|
|
196
|
-
// Environment
|
|
197
|
-
local: false, // true = HTTP, false = HTTPS
|
|
198
|
-
staging: false, // true = Let's Encrypt staging
|
|
199
|
-
|
|
200
|
-
// Server
|
|
201
|
-
hostname: '::',
|
|
202
|
-
port: 443, // Default HTTPS port (NOT 80!)
|
|
203
|
-
|
|
204
|
-
// Local mode
|
|
205
|
-
minLocalPort: 4000,
|
|
206
|
-
maxLocalPort: 9999,
|
|
207
|
-
|
|
208
|
-
// Advanced
|
|
209
|
-
filename: 'index', // Handler filename (no extension)
|
|
210
|
-
basePath: '/srv' // Base for relative paths
|
|
211
|
-
})
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## Core API
|
|
215
|
-
|
|
216
|
-
### `roster.start()`
|
|
217
|
-
Loads sites, generates SSL config, starts servers. Returns `Promise<void>`. Calls `init()` internally.
|
|
218
|
-
|
|
219
|
-
### `roster.init()`
|
|
220
|
-
Loads sites, creates VirtualServers, prepares dispatchers β but creates **no servers** and calls **no `.listen()`**. Returns `Promise<Roster>`. Idempotent. Use this for cluster-friendly integration where an external manager owns the socket.
|
|
221
|
-
|
|
222
|
-
### `roster.requestHandler(port?)`
|
|
223
|
-
Returns `(req, res) => void` dispatcher for a port (defaults to `defaultPort`). Requires `init()` first. Handles Host-header routing, wwwβnon-www redirects, wildcard matching.
|
|
224
|
-
|
|
225
|
-
### `roster.upgradeHandler(port?)`
|
|
226
|
-
Returns `(req, socket, head) => void` for WebSocket upgrade routing. Requires `init()` first.
|
|
227
|
-
|
|
228
|
-
### `roster.sniCallback()`
|
|
229
|
-
Returns `(servername, callback) => void` TLS SNI callback that resolves certs from `greenlockStorePath`. With `autoCertificates` enabled (default), it can issue missing certs automatically. Production mode only. Requires `init()` first.
|
|
230
|
-
|
|
231
|
-
### `roster.ensureCertificate(servername)`
|
|
232
|
-
Forces certificate availability for a domain and returns `{ key, cert }`. With `autoCertificates` enabled (default), it issues certs automatically when missing.
|
|
233
|
-
|
|
234
|
-
### `roster.loadCertificate(servername)`
|
|
235
|
-
Loads existing `{ key, cert }` from `greenlockStorePath` without issuing new certificates.
|
|
236
|
-
|
|
237
|
-
### `roster.createManagedHttpsServer(options)`
|
|
238
|
-
Creates a pre-wired `https.Server` with default cert, SNI callback, and attached request/upgrade handlers.
|
|
239
|
-
|
|
240
|
-
### `roster.createServingHttpsServer(options)`
|
|
241
|
-
Serving-only helper for worker processes. Same as `createManagedHttpsServer(..., ensureCertificate: false)`.
|
|
242
|
-
|
|
243
|
-
### `roster.attach(server, { port }?)`
|
|
244
|
-
Convenience: wires `requestHandler` + `upgradeHandler` onto an external `http.Server` or `https.Server`. Returns `this`. Requires `init()` first.
|
|
245
|
-
|
|
246
|
-
### `roster.register(domain, handler)`
|
|
247
|
-
Manually register a domain handler. Domain can include port: `'api.com:8443'`. For wildcards use `'*.example.com'` or `'*.example.com:8080'`.
|
|
248
|
-
|
|
249
|
-
### `roster.use(plugin)`
|
|
250
|
-
Registers a synchronous request plugin. It receives `(req, res, { host, domain })` and stops dispatch when it returns `true`.
|
|
251
|
-
|
|
252
|
-
### `roster.getUrl(domain)`
|
|
253
|
-
Get environment-aware URL:
|
|
254
|
-
- Local mode: `http://localhost:{port}`
|
|
255
|
-
- Production: `https://{domain}` or `https://{domain}:{port}`
|
|
256
|
-
- Returns `null` if domain not registered. Supports wildcard-matched hosts (e.g. `getUrl('api.example.com')` when `*.example.com` is registered).
|
|
257
|
-
|
|
258
|
-
## How It Works
|
|
259
|
-
|
|
260
|
-
### Request Flow
|
|
261
|
-
1. Request arrives β Dispatcher extracts `Host` header
|
|
262
|
-
2. Strips `www.` prefix (301 redirect if present)
|
|
263
|
-
3. Looks up domain β Gets `VirtualServer` instance
|
|
264
|
-
4. Routes to handler via `virtualServer.processRequest(req, res)`
|
|
265
|
-
|
|
266
|
-
### VirtualServer Architecture
|
|
267
|
-
Each domain gets isolated server instance that simulates `http.Server`:
|
|
268
|
-
- Captures `request` and `upgrade` event listeners
|
|
269
|
-
- Complete separation between domains
|
|
270
|
-
- No configuration conflicts between apps
|
|
271
|
-
|
|
272
|
-
### Port Assignment
|
|
273
|
-
**Production**: Default 443, custom via `domain:port` syntax
|
|
274
|
-
**Local**: CRC32 hash of domain β deterministic port in range 4000-9999
|
|
275
|
-
**Reserved**: Port 80 for ACME challenges only
|
|
276
|
-
|
|
277
|
-
### SSL Management
|
|
278
|
-
- Automatic Let's Encrypt certificate generation
|
|
279
|
-
- Auto-renewal 45 days before expiration
|
|
280
|
-
- SNI support for multiple domains
|
|
281
|
-
- Custom ports reuse certificates via SNI callback
|
|
282
|
-
- **Wildcard** (`*.example.com`): use folder `www/*.example.com/` or `roster.register('*.example.com', handler)`. Default DNS-01 plugin is local/manual `acme-dns-01-cli`; set `dnsChallenge` only when overriding provider integration.
|
|
283
|
-
|
|
284
|
-
## Common Issues & Solutions
|
|
285
|
-
|
|
286
|
-
**Port 443 in use**: Use different port `{ port: 8443 }`
|
|
287
|
-
**Certificate failed**: Check firewall (ports 80, 443), verify DNS, try `staging: true`
|
|
288
|
-
**Site not found**: Verify directory name matches domain. For Node: check `index.js` exports function. For static: ensure `index.html` exists (no index script).
|
|
289
|
-
**Local port conflict**: Adjust `minLocalPort`/`maxLocalPort` range
|
|
290
|
-
**Socket.IO not working**: Ensure handler checks `io.opts.path` and returns properly
|
|
291
|
-
|
|
292
|
-
## Best Practices
|
|
293
|
-
|
|
294
|
-
1. **Test with staging first**: `staging: true` to avoid Let's Encrypt rate limits
|
|
295
|
-
2. **Use local mode for dev**: `local: true` for faster iteration
|
|
296
|
-
3. **Environment variables**: Configure via `process.env` for portability
|
|
297
|
-
4. **Error handling**: Wrap handlers with try/catch, don't expose internals
|
|
298
|
-
5. **Socket.IO paths**: Always check `req.url.startsWith(io.opts.path)` in returned handler
|
|
299
|
-
6. **Port 80**: Never use as HTTPS port (reserved for ACME)
|
|
300
|
-
|
|
301
|
-
## Quick Examples
|
|
302
|
-
|
|
303
|
-
### Full Production Setup
|
|
304
43
|
```javascript
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
const roster = new Roster({
|
|
308
|
-
email: process.env.ADMIN_EMAIL,
|
|
309
|
-
wwwPath: '/srv/www',
|
|
310
|
-
greenlockStorePath: '/srv/greenlock.d',
|
|
311
|
-
staging: process.env.NODE_ENV !== 'production'
|
|
312
|
-
});
|
|
44
|
+
const { Server } = require('socket.io');
|
|
313
45
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
});
|
|
46
|
+
module.exports = (server) => {
|
|
47
|
+
const io = new Server(server);
|
|
48
|
+
server.onClose(() => new Promise(resolve => io.close(resolve)));
|
|
49
|
+
return (req, res) => res.end('Socket.IO site');
|
|
50
|
+
};
|
|
320
51
|
```
|
|
321
52
|
|
|
322
|
-
|
|
323
|
-
```javascript
|
|
324
|
-
const roster = new Roster({ local: true, wwwPath: './www' });
|
|
325
|
-
|
|
326
|
-
roster.register('test.local', (server) => {
|
|
327
|
-
return (req, res) => {
|
|
328
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
329
|
-
res.end(JSON.stringify({ status: 'ok', url: roster.getUrl('test.local') }));
|
|
330
|
-
};
|
|
331
|
-
});
|
|
53
|
+
Socket.IO wraps the request listeners and captures the returned handler as its fallback. Do not add a manual endpoint exclusion to that handler; use `io.path()` when application code needs the path. Do not use `io.opts.path`.
|
|
332
54
|
|
|
333
|
-
|
|
334
|
-
```
|
|
55
|
+
A virtual request listener owns its request even before it ends the response. Roster does not infer that an asynchronous listener declined the request. Thrown/rejected handler errors produce 500 or destroy an already-started response. Virtual servers share a process; they are not process isolation boundaries.
|
|
335
56
|
|
|
336
|
-
|
|
337
|
-
```javascript
|
|
338
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
57
|
+
## Shutdown
|
|
339
58
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
maxLocalPort: parseInt(process.env.MAX_PORT) || 9999
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
roster.start();
|
|
351
|
-
```
|
|
59
|
+
- Connect `await roster.close()` to the application's existing shutdown flow. Roster does not install signal handlers or terminate processes.
|
|
60
|
+
- Register `virtualServer.onClose(fn)` for each site's timers, databases, and other resources. Return/await its cleanup Promise; wrap callback APIs when needed.
|
|
61
|
+
- Roster signals virtual `close` at shutdown start to release integrations such as long polling, closes routed WebSockets, drains HTTP, then runs cleanup hooks concurrently. Put dependent cleanup steps in one hook.
|
|
62
|
+
- Hooks run once. Failures are aggregated after other hooks are attempted. `closeTimeoutMs` bounds the complete operation; timeout cannot cancel arbitrary user Promises or in-flight ACME work.
|
|
63
|
+
- `close()` is idempotent and terminal. `init()`/`start()` share concurrent calls; startup failures clean up the instance. Create a new instance after closing or failed startup.
|
|
64
|
+
- For `attach()` and both HTTPS helpers, also close the caller-owned server. Roster removes its attached listeners but does not own that server's listener lifecycle. Remove manually wired dispatchers yourself.
|
|
65
|
+
- Coordinate each process separately; closing an instance does not stop the legacy Greenlock cluster's worker processes.
|
|
352
66
|
|
|
353
|
-
##
|
|
67
|
+
## Certificates and plugins
|
|
354
68
|
|
|
355
|
-
|
|
69
|
+
- Use `staging: true` for ACME test issuance when requested. `init()` alone does not bind the HTTP challenge listener.
|
|
70
|
+
- Wildcard certificates require DNS-01. The default CLI wrapper needs manual TXT records unless an API provider is configured; `dnsChallenge: false` disables that integration, not HTTPS.
|
|
71
|
+
- Linode mode uses `ROSTER_DNS_PROVIDER=linode` and `LINODE_API_KEY` from the application's secret configuration; a key also selects Linode when no provider is set. With an explicit `dnsChallenge` object, include `module: 'acme-dns-01-cli'` to select the wrapper.
|
|
72
|
+
- For unattended Linode operation that must fail rather than fall back to manual DNS, set `dnsChallenge.dnsApiFallbackToManual: false`. Keep other provider choices explicit.
|
|
73
|
+
- `combineWildcardCerts` combines apex/www/wildcard issuance using DNS-01; `disableWildcard` ignores wildcard sites. Do not enable either as a generic troubleshooting step.
|
|
74
|
+
- `ensureCertificate(name)` loads existing PEMs or issues missing ones; `loadCertificate(name)` only reads files. Neither means βforce renewal.β File-based SNI caches detect certificate changes on supported runtimes.
|
|
75
|
+
- In local testing, Bun 1.3.4's `node:https` serving helper did not invoke `SNICallback`, including with the previous synchronous resolver. Do not promise SNI certificate reload merely because files changed; Node and Bun require separate runtime verification.
|
|
76
|
+
- Request plugins registered with `roster.use(fn)` are synchronous and run before redirects/dispatch. Return `true` only after handling the response. Promise returns produce 500.
|
|
77
|
+
- The optional `createScannerBlocker` comes from `roster-server/plugins/scanner-blocker.js`. Its ban state is per process and in memory. Enable `trustProxy` only when a trusted proxy overwrites `X-Forwarded-For`; `onBlock` does not itself provide shared bans.
|
|
356
78
|
|
|
357
|
-
|
|
358
|
-
- [ ] Each domain has either `index.js` (or `.mjs`/`.cjs`) exporting `(httpsServer) => handler`, or `index.html` (and assets) for a static site
|
|
359
|
-
- [ ] Configure email for Let's Encrypt notifications
|
|
360
|
-
- [ ] Test with `local: true` first
|
|
361
|
-
- [ ] Test with `staging: true` before production
|
|
362
|
-
- [ ] Ensure ports 80 and 443 are open (production)
|
|
363
|
-
- [ ] Verify DNS points to server
|
|
364
|
-
- [ ] Never use port 80 as HTTPS port
|
|
365
|
-
- [ ] Use `roster.getUrl(domain)` for environment-aware URLs
|
|
366
|
-
- [ ] Handle Socket.IO paths correctly in returned handler
|
|
367
|
-
- [ ] Implement error handling in handlers
|
|
79
|
+
Use the consuming application's installed README for the complete options and method signatures. The [upstream README](https://github.com/clasen/RosterServer#readme) is the project reference; prefer documentation matching the installed version.
|