roster-server 3.2.0 → 3.3.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/README.md +20 -0
- package/index.js +21 -8
- package/lib/resolve-site-app.js +9 -1
- package/lib/site-worker.js +67 -3
- package/package.json +53 -53
- package/test/https-integration.test.js +3 -2
- package/test/isolation.test.js +60 -0
- package/test/static-and-tls.test.js +38 -0
- package/vendor/greenlock/bin/certonly.js +0 -0
- package/vendor/greenlock/bin/greenlock.js +0 -0
- package/vendor/greenlock/logo/beaker-browser-301x112.png +0 -0
- package/vendor/greenlock/logo/greenlock-1063x250.png +0 -0
- package/vendor/greenlock/logo/ibm-301x112.png +0 -0
- package/vendor/greenlock/logo/telebit-301x112.png +0 -0
- package/vendor/greenlock-express/scripts/postinstall +0 -0
package/README.md
CHANGED
|
@@ -184,6 +184,26 @@ Isolation needs a Roster-owned listener, so it applies only when `start()` initi
|
|
|
184
184
|
|
|
185
185
|
Each isolated process loads Node.js and its site, so memory use grows with the number of site directories. Code that runs when a site's module loads runs in that site's process, not in the main process.
|
|
186
186
|
|
|
187
|
+
### Socket sites
|
|
188
|
+
|
|
189
|
+
A site directory whose entry exports an object with a `connection(socket, head)` method receives raw client connections instead of requests. Use it for a site that terminates TLS itself, for example to spread connections across its own worker processes:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
// www/game.example.com/index.js
|
|
193
|
+
export default {
|
|
194
|
+
connection(socket, head) {
|
|
195
|
+
// Terminate TLS here, or pass socket and head to another process.
|
|
196
|
+
},
|
|
197
|
+
async close() {
|
|
198
|
+
// Release the site's resources; runs after Roster stops accepting connections.
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
In production, `socket` is the client's TCP connection and `head` holds the TLS ClientHello that Roster already read; put it back with `socket.unshift(head)` before terminating TLS, and do not `resume()` the socket after wrapping it. In local mode, the site's assigned port accepts plain TCP connections, `socket` arrives paused, and `head` is empty. `connection()` runs synchronously; if it throws, Roster destroys the socket.
|
|
204
|
+
|
|
205
|
+
Roster keeps issuing and renewing the site's certificates under `greenlockStorePath/live/<domain>`, but the site loads them and must pick up renewed files itself. Request plugins, `www.` redirects, and `onClose` hooks do not apply: the site owns every connection it receives. A socket site must run isolated; Roster rejects it when it would be served by the main process.
|
|
206
|
+
|
|
187
207
|
## Startup and shutdown
|
|
188
208
|
|
|
189
209
|
Concurrent `init()` calls share initialization, so each factory runs once per registered domain/port. Concurrent `start()` calls share startup. In standalone and local modes, `await roster.start()` waits for listeners to bind. Initialization or bind failures reject and clean up resources already initialized by the instance.
|
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const crypto = require('crypto');
|
|
|
8
8
|
const { EventEmitter } = require('events');
|
|
9
9
|
const Greenlock = require('./vendor/greenlock-express/greenlock-express.js');
|
|
10
10
|
const GreenlockShim = require('./vendor/greenlock-express/greenlock-shim.js');
|
|
11
|
-
const { locateSiteEntry, resolveSiteApp } = require('./lib/resolve-site-app.js');
|
|
11
|
+
const { locateSiteEntry, resolveSiteApp, isSocketSite } = require('./lib/resolve-site-app.js');
|
|
12
12
|
const { IsolatedSite } = require('./lib/isolated-site.js');
|
|
13
13
|
const { parseClientHello } = require('./lib/client-hello.js');
|
|
14
14
|
const log = require('lemonlog')('roster');
|
|
@@ -139,6 +139,12 @@ function buildCertLookupCandidates(servername) {
|
|
|
139
139
|
return candidates;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// The certificate stored for a host's own subject serves it; a wildcard or zone
|
|
143
|
+
// certificate found as a fallback serves only the names it covers.
|
|
144
|
+
function certDirServesHost(certDir, host, certificate) {
|
|
145
|
+
return certDirCandidatesForSubject(host).includes(certDir) || certificate.checkHost(host) !== undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
function certCoversName(certPem, name) {
|
|
143
149
|
try {
|
|
144
150
|
const x509 = new crypto.X509Certificate(certPem);
|
|
@@ -887,7 +893,7 @@ class Roster {
|
|
|
887
893
|
const candidates = buildCertLookupCandidates(host);
|
|
888
894
|
for (const candidate of candidates) {
|
|
889
895
|
const pems = this._loadCert(candidate);
|
|
890
|
-
if (pems) return pems;
|
|
896
|
+
if (pems && certDirServesHost(candidate, host, new crypto.X509Certificate(pems.cert))) return pems;
|
|
891
897
|
}
|
|
892
898
|
return null;
|
|
893
899
|
}
|
|
@@ -896,6 +902,9 @@ class Roster {
|
|
|
896
902
|
this._sitesByPort = {};
|
|
897
903
|
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
898
904
|
this._assertOpen();
|
|
905
|
+
if (isSocketSite(siteApp)) {
|
|
906
|
+
throw new Error(`${hostKey} serves raw connections and must run as an isolated site directory`);
|
|
907
|
+
}
|
|
899
908
|
if (hostKey.startsWith('www.') || typeof siteApp !== 'function') continue;
|
|
900
909
|
const { domain, port } = this.parseDomainWithPort(hostKey);
|
|
901
910
|
if (!this._sitesByPort[port]) {
|
|
@@ -1096,8 +1105,8 @@ class Roster {
|
|
|
1096
1105
|
pending = this._loadSecureContext(subject).finally(() => this._contextLoads.delete(subject));
|
|
1097
1106
|
this._contextLoads.set(subject, pending);
|
|
1098
1107
|
}
|
|
1099
|
-
const
|
|
1100
|
-
if (
|
|
1108
|
+
const loaded = await pending;
|
|
1109
|
+
if (loaded && certDirServesHost(subject, host, loaded.certificate)) return loaded.context;
|
|
1101
1110
|
}
|
|
1102
1111
|
return null;
|
|
1103
1112
|
}
|
|
@@ -1116,12 +1125,16 @@ class Roster {
|
|
|
1116
1125
|
this._assertOpen();
|
|
1117
1126
|
const version = stats.map(stat => `${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`).join('|');
|
|
1118
1127
|
const cached = this._secureContexts.get(subject);
|
|
1119
|
-
if (cached && cached.version === version) return cached
|
|
1128
|
+
if (cached && cached.version === version) return cached;
|
|
1120
1129
|
const [key, cert, chain] = await Promise.all(files.map(file => fs.promises.readFile(file, 'utf8')));
|
|
1121
1130
|
this._assertOpen();
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1131
|
+
const loaded = {
|
|
1132
|
+
version,
|
|
1133
|
+
context: tls.createSecureContext({ key, cert: cert + chain }),
|
|
1134
|
+
certificate: new crypto.X509Certificate(cert)
|
|
1135
|
+
};
|
|
1136
|
+
this._secureContexts.set(subject, loaded);
|
|
1137
|
+
return loaded;
|
|
1125
1138
|
}
|
|
1126
1139
|
|
|
1127
1140
|
_checkCertificate(servername) {
|
package/lib/resolve-site-app.js
CHANGED
|
@@ -41,4 +41,12 @@ async function resolveSiteApp(domainPath, options = {}) {
|
|
|
41
41
|
return { siteApp, type: 'js' };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* A socket site exports `{ connection(socket, head), close?() }` and serves raw client
|
|
46
|
+
* connections itself, including TLS. Only an isolated site process can hand it sockets.
|
|
47
|
+
*/
|
|
48
|
+
function isSocketSite(siteApp) {
|
|
49
|
+
return typeof siteApp === 'object' && siteApp !== null && typeof siteApp.connection === 'function';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { locateSiteEntry, resolveSiteApp, isSocketSite };
|
package/lib/site-worker.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// Entry point of an isolated site process, forked by IsolatedSite.
|
|
4
4
|
const https = require('https');
|
|
5
|
+
const net = require('net');
|
|
5
6
|
const Roster = require('../index.js');
|
|
6
|
-
const { resolveSiteApp } = require('./resolve-site-app.js');
|
|
7
|
+
const { resolveSiteApp, isSocketSite } = require('./resolve-site-app.js');
|
|
7
8
|
const { serializeError } = require('./isolated-site.js');
|
|
8
9
|
const log = require('lemonlog')('roster');
|
|
9
10
|
|
|
@@ -11,6 +12,8 @@ class SiteWorkerRoster extends Roster {
|
|
|
11
12
|
constructor(site) {
|
|
12
13
|
super({ ...site.options, isolation: false, autoCertificates: false, handleSignals: false });
|
|
13
14
|
this._site = site;
|
|
15
|
+
this._socketSite = null;
|
|
16
|
+
this._socketSiteClose = null;
|
|
14
17
|
this._httpsServers = new Map();
|
|
15
18
|
this._certificateRequests = new Map();
|
|
16
19
|
this._nextCertificateRequest = 0;
|
|
@@ -31,9 +34,64 @@ class SiteWorkerRoster extends Roster {
|
|
|
31
34
|
log.warn(`⚠️ No index file (js/mjs/cjs or index.html) found in ${sitePath}`);
|
|
32
35
|
return;
|
|
33
36
|
}
|
|
37
|
+
if (isSocketSite(resolved.siteApp)) {
|
|
38
|
+
this._socketSite = resolved.siteApp;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
34
41
|
for (const key of keys) this.sites[key] = resolved.siteApp;
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
get socketSite() {
|
|
45
|
+
return this._socketSite !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// In local mode a socket site gets plain connections on its assigned ports, with no ClientHello.
|
|
49
|
+
async serveLocalSockets() {
|
|
50
|
+
const head = Buffer.alloc(0);
|
|
51
|
+
for (const key of this._site.keys) {
|
|
52
|
+
if (key.startsWith('www.')) continue;
|
|
53
|
+
const { domain, port } = this.parseDomainWithPort(key);
|
|
54
|
+
const server = net.createServer({ pauseOnConnect: true });
|
|
55
|
+
const listening = this._listen(server, this._site.localPorts[`${domain}:${port}`], 'localhost');
|
|
56
|
+
server.on('connection', socket => {
|
|
57
|
+
// The site owns the connection from here; it may pass it to another process.
|
|
58
|
+
this._sockets.delete(socket);
|
|
59
|
+
this._handOff(head, socket);
|
|
60
|
+
});
|
|
61
|
+
await listening;
|
|
62
|
+
log.info(`🌐 ${domain} → tcp://localhost:${this._site.localPorts[`${domain}:${port}`]} (socket site)`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_handOff(head, socket) {
|
|
67
|
+
if (this._closing || socket.destroyed) {
|
|
68
|
+
socket.destroy();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
this._socketSite.connection(socket, head);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
socket.destroy();
|
|
75
|
+
log.error(`❌ Socket site ${this._site.name} rejected a connection:`, error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
close() {
|
|
80
|
+
if (!this._socketSite) return super.close();
|
|
81
|
+
// Stop accepting first, then let the site drain the connections it was handed.
|
|
82
|
+
this._socketSiteClose ??= Promise.allSettled([super.close()]).then(async ([listeners]) => {
|
|
83
|
+
const errors = listeners.status === 'rejected' ? [listeners.reason] : [];
|
|
84
|
+
try {
|
|
85
|
+
await this._socketSite.close?.();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
errors.push(error);
|
|
88
|
+
}
|
|
89
|
+
const flat = errors.flatMap(error => error instanceof AggregateError ? error.errors : [error]);
|
|
90
|
+
if (flat.length) throw new AggregateError(flat, 'Roster close failed');
|
|
91
|
+
});
|
|
92
|
+
return this._socketSiteClose;
|
|
93
|
+
}
|
|
94
|
+
|
|
37
95
|
generateConfigJson() {}
|
|
38
96
|
|
|
39
97
|
assignPortToDomain(domain, port) {
|
|
@@ -81,6 +139,10 @@ class SiteWorkerRoster extends Roster {
|
|
|
81
139
|
}
|
|
82
140
|
|
|
83
141
|
accept(port, head, socket) {
|
|
142
|
+
if (this._socketSite) {
|
|
143
|
+
this._handOff(head, socket);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
84
146
|
const server = this._httpsServers.get(port);
|
|
85
147
|
if (this._closing || !server) {
|
|
86
148
|
socket.destroy();
|
|
@@ -103,10 +165,12 @@ async function start(site) {
|
|
|
103
165
|
for (const plugin of site.plugins) {
|
|
104
166
|
roster.use(require(plugin.module)[plugin.export](plugin.options));
|
|
105
167
|
}
|
|
106
|
-
|
|
168
|
+
await roster.init();
|
|
169
|
+
if (roster.socketSite) {
|
|
170
|
+
if (roster.local) await roster.serveLocalSockets();
|
|
171
|
+
} else if (roster.local) {
|
|
107
172
|
await roster.start();
|
|
108
173
|
} else {
|
|
109
|
-
await roster.init();
|
|
110
174
|
roster.serveHttps();
|
|
111
175
|
}
|
|
112
176
|
process.send({ type: 'ready' });
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
2
|
+
"name": "roster-server",
|
|
3
|
+
"version": "3.3.0",
|
|
4
|
+
"description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test 'test/**/*.test.js'"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/clasen/RosterServer.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"greenlock",
|
|
15
|
+
"https",
|
|
16
|
+
"domain",
|
|
17
|
+
"multi-domain",
|
|
18
|
+
"letsencrypt",
|
|
19
|
+
"hosting",
|
|
20
|
+
"ssl",
|
|
21
|
+
"host",
|
|
22
|
+
"vhost",
|
|
23
|
+
"virtual-host",
|
|
24
|
+
"socket.io",
|
|
25
|
+
"websocket",
|
|
26
|
+
"express",
|
|
27
|
+
"greenlock-express",
|
|
28
|
+
"shotx",
|
|
29
|
+
"bun",
|
|
30
|
+
"clasen"
|
|
31
|
+
],
|
|
32
|
+
"author": "Martin Clasen",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/clasen/RosterServer/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/clasen/RosterServer#readme",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@greenlock/manager": "^3.1.0",
|
|
40
|
+
"@root/acme": "^3.1.0",
|
|
41
|
+
"@root/csr": "^0.8.1",
|
|
42
|
+
"@root/encoding": "^1.0.1",
|
|
43
|
+
"@root/keypairs": "^0.10.0",
|
|
44
|
+
"@root/mkdirp": "^1.0.0",
|
|
45
|
+
"@root/request": "^1.6.1",
|
|
46
|
+
"acme-dns-01-cli": "^3.0.7",
|
|
47
|
+
"acme-http-01-standalone": "^3.0.5",
|
|
48
|
+
"cert-info": "^1.5.1",
|
|
49
|
+
"greenlock-store-fs": "^3.2.2",
|
|
50
|
+
"lemonlog": "^1.2.0",
|
|
51
|
+
"redirect-https": "^1.3.1",
|
|
52
|
+
"safe-replace": "^1.1.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -32,8 +32,9 @@ function certificate(roster, subject, serial) {
|
|
|
32
32
|
const key = path.join(directory, 'privkey.pem');
|
|
33
33
|
const cert = path.join(directory, 'cert.pem');
|
|
34
34
|
const keyArgs = fs.existsSync(key) ? ['-key', key] : ['-newkey', 'rsa:2048', '-nodes', '-keyout', key];
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const name = subject.replace(/^_wildcard_\./, '*.');
|
|
36
|
+
execFileSync('openssl', ['req', '-x509', ...keyArgs, '-out', cert, '-subj', '/CN=example.com',
|
|
37
|
+
'-addext', `subjectAltName=DNS:${name}`, '-days', '1', '-set_serial', String(serial)], { stdio: 'ignore' });
|
|
37
38
|
fs.writeFileSync(path.join(directory, 'chain.pem'), '');
|
|
38
39
|
return directory;
|
|
39
40
|
}
|
package/test/isolation.test.js
CHANGED
|
@@ -405,6 +405,66 @@ it('closes isolated sites when the main process dies without closing', { timeout
|
|
|
405
405
|
assert.equal(fs.readFileSync(marker, 'utf8'), 'closed');
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
+
function socketSite(server, marker) {
|
|
409
|
+
return `const fs = require('fs');
|
|
410
|
+
const server = ${server};
|
|
411
|
+
server.on('request', (req, res) => res.end(JSON.stringify({ pid: process.pid, address: req.socket.remoteAddress })));
|
|
412
|
+
module.exports = {
|
|
413
|
+
connection(socket, head) {
|
|
414
|
+
fs.writeFileSync(${JSON.stringify(marker)} + '.head', String(head.length));
|
|
415
|
+
// A ClientHello read by Roster goes back on the stream; a local connection arrives paused.
|
|
416
|
+
if (head.length > 0) socket.unshift(head);
|
|
417
|
+
server.emit('connection', socket);
|
|
418
|
+
if (head.length === 0) socket.resume();
|
|
419
|
+
},
|
|
420
|
+
close() {
|
|
421
|
+
fs.writeFileSync(${JSON.stringify(marker)}, 'closed');
|
|
422
|
+
}
|
|
423
|
+
};`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
it('hands raw client sockets with their ClientHello to a socket site that terminates TLS', { timeout: 20000 }, async t => {
|
|
427
|
+
const marker = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'roster-socket-site-')), 'closed');
|
|
428
|
+
t.after(() => fs.rmSync(path.dirname(marker), { recursive: true, force: true }));
|
|
429
|
+
const tlsServer = `require('https').createServer({
|
|
430
|
+
key: fs.readFileSync(${JSON.stringify(path.join(pki, 'key.pem'))}),
|
|
431
|
+
cert: fs.readFileSync(${JSON.stringify(path.join(pki, 'cert.pem'))})
|
|
432
|
+
})`;
|
|
433
|
+
const { roster, port } = await startProduction(t, {
|
|
434
|
+
'raw.example.com': socketSite(tlsServer, marker),
|
|
435
|
+
'plain.example.com': REPORT_SITE
|
|
436
|
+
}, { certificates: ['plain.example.com'] });
|
|
437
|
+
const raw = JSON.parse((await get(port, 'raw.example.com')).body);
|
|
438
|
+
const plain = JSON.parse((await get(port, 'plain.example.com')).body);
|
|
439
|
+
assert.notEqual(raw.pid, process.pid);
|
|
440
|
+
assert.notEqual(raw.pid, plain.pid);
|
|
441
|
+
assert.match(raw.address, /127\.0\.0\.1$/);
|
|
442
|
+
assert.ok(Number(fs.readFileSync(`${marker}.head`, 'utf8')) > 0);
|
|
443
|
+
await roster.close();
|
|
444
|
+
assert.equal(fs.readFileSync(marker, 'utf8'), 'closed');
|
|
445
|
+
assert.equal(isRunning(raw.pid), false);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('gives a socket site plain connections on its local port', { timeout: 20000 }, async t => {
|
|
449
|
+
const marker = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'roster-socket-site-')), 'closed');
|
|
450
|
+
t.after(() => fs.rmSync(path.dirname(marker), { recursive: true, force: true }));
|
|
451
|
+
const { wwwPath } = createWww(t, { 'raw.example': socketSite(`require('http').createServer()`, marker) });
|
|
452
|
+
const roster = new Roster({ local: true, wwwPath, minLocalPort: 19370, maxLocalPort: 19379 });
|
|
453
|
+
t.after(() => roster.close().catch(() => {}));
|
|
454
|
+
await roster.start();
|
|
455
|
+
const { port } = new URL(roster.getUrl('raw.example'));
|
|
456
|
+
assert.notEqual(JSON.parse((await localGet(port)).body).pid, process.pid);
|
|
457
|
+
assert.equal(fs.readFileSync(`${marker}.head`, 'utf8'), '0');
|
|
458
|
+
await roster.close();
|
|
459
|
+
assert.equal(fs.readFileSync(marker, 'utf8'), 'closed');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('refuses a socket site served by the main process', { timeout: 20000 }, async t => {
|
|
463
|
+
const { wwwPath } = createWww(t, { 'raw.example': `module.exports = { connection() {} };` });
|
|
464
|
+
const roster = new Roster({ local: true, wwwPath, isolation: false, minLocalPort: 19380, maxLocalPort: 19389 });
|
|
465
|
+
await assert.rejects(roster.start(), /must run as an isolated site directory/);
|
|
466
|
+
});
|
|
467
|
+
|
|
408
468
|
it('rejects combining isolation with the legacy cluster launcher', () => {
|
|
409
469
|
assert.throws(() => new Roster({ cluster: true, isolation: true }), /cannot be combined/);
|
|
410
470
|
assert.equal(new Roster({ cluster: true }).isolation, null);
|
|
@@ -218,6 +218,44 @@ it('reuses TLS contexts, coalesces concurrent loads, and reloads changed certifi
|
|
|
218
218
|
assert.equal(roster._secureContexts.size, 0);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
function installCertificate(root, directoryName, names) {
|
|
222
|
+
const directory = path.join(root, 'live', directoryName);
|
|
223
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
224
|
+
execFileSync('openssl', ['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes',
|
|
225
|
+
'-keyout', path.join(directory, 'privkey.pem'), '-out', path.join(directory, 'cert.pem'),
|
|
226
|
+
'-subj', `/CN=${names[0]}`, '-addext', `subjectAltName=${names.map(name => `DNS:${name}`).join(',')}`,
|
|
227
|
+
'-days', '1'], { stdio: 'ignore' });
|
|
228
|
+
fs.writeFileSync(path.join(directory, 'chain.pem'), '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
it('falls back to zone and wildcard certificates only when they cover the host', async t => {
|
|
232
|
+
const root = temporaryDirectory(t);
|
|
233
|
+
installCertificate(root, 'example.com', ['example.com', 'www.example.com']);
|
|
234
|
+
installCertificate(root, '_wildcard_.wild.example.com', ['*.wild.example.com']);
|
|
235
|
+
const roster = new Roster({ local: true, wwwPath: root, greenlockStorePath: root });
|
|
236
|
+
t.after(() => roster.close());
|
|
237
|
+
const issued = [];
|
|
238
|
+
roster._greenlockRuntime = {
|
|
239
|
+
get: async ({ servername }) => {
|
|
240
|
+
issued.push(servername);
|
|
241
|
+
installCertificate(root, servername, [servername]);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const www = await roster._getSecureContext('www.example.com');
|
|
246
|
+
assert.equal(www, roster._secureContexts.get('example.com').context);
|
|
247
|
+
const wild = await roster._getSecureContext('shop.wild.example.com');
|
|
248
|
+
assert.equal(wild, roster._secureContexts.get('_wildcard_.wild.example.com').context);
|
|
249
|
+
assert.deepEqual(issued, []);
|
|
250
|
+
|
|
251
|
+
const uncovered = await roster._getSecureContext('api.example.com');
|
|
252
|
+
assert.deepEqual(issued, ['api.example.com']);
|
|
253
|
+
assert.equal(uncovered, roster._secureContexts.get('api.example.com').context);
|
|
254
|
+
|
|
255
|
+
assert.match(roster._resolvePemsForServername('www.example.com').cert, /BEGIN CERTIFICATE/);
|
|
256
|
+
assert.equal(roster._resolvePemsForServername('mail.example.com'), null);
|
|
257
|
+
});
|
|
258
|
+
|
|
221
259
|
it('coalesces certificate checks and prevents new issuance after close', async t => {
|
|
222
260
|
const root = temporaryDirectory(t);
|
|
223
261
|
const roster = new Roster({ local: true, wwwPath: root });
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|