roster-server 3.4.2 → 3.5.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 +6 -4
- package/index.js +17 -180
- package/lib/https-server.js +101 -0
- package/lib/site-worker.js +3 -3
- package/package.json +4 -5
- package/skills/roster-server/SKILL.md +1 -1
- package/test/https-server.test.js +189 -0
- package/test/isolation.test.js +6 -4
- package/test/lifecycle.test.js +5 -1
- package/vendor/greenlock-express/servers.js +2 -1
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ module.exports = { createMyPlugin };
|
|
|
180
180
|
|
|
181
181
|
On `close()`, or a handled signal, each isolated process drains its requests, runs its `onClose` hooks, and exits. Hook errors from those processes appear in the same `AggregateError`. When `closeTimeoutMs` expires, Roster kills the remaining isolated processes. Isolated processes ignore `SIGINT` and `SIGTERM` and wait for the main process, so a signal sent to the whole process group by a terminal or process manager does not cut their drain short. If the main process exits without closing, each isolated process closes itself.
|
|
182
182
|
|
|
183
|
-
Isolation needs a Roster-owned listener, so it applies only when `start()` initializes the instance. Calling `init()` yourself keeps every site in the current process for `attach()` and the other external-server helpers. Isolation
|
|
183
|
+
Isolation needs a Roster-owned listener, so it applies only when `start()` initializes the instance. Calling `init()` yourself keeps every site in the current process for `attach()` and the other external-server helpers. Isolation cannot be combined with the legacy `cluster: true` launcher, which disables it by default.
|
|
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
|
|
|
@@ -204,7 +204,7 @@ export default {
|
|
|
204
204
|
};
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
-
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.
|
|
207
|
+
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. To terminate TLS with an HTTPS server, create it with `Roster.createHttpsServer()` and hand it the socket with `Roster.acceptTlsConnection(server, socket)`: on Node these are `https.createServer()` and `server.emit('connection', socket)`, and on Bun they also select certificates per host and adopt the socket, which Bun's own `https.Server` does not. 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.
|
|
208
208
|
|
|
209
209
|
`certificate(servername)` returns the certificate Roster would serve for that host, with the same wildcard and zone matching. A missing certificate is issued by the main process first, as for any isolated site. Roster also renews it; call `certificate()` again to pick up a renewal, for example on an interval or when a cached copy gets old. It is not available in local mode. 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.
|
|
210
210
|
|
|
@@ -348,6 +348,8 @@ Pass operational settings through the constructor, using the consuming applicati
|
|
|
348
348
|
| `createManagedHttpsServer({ servername, port?, ensureCertificate?, tlsOptions? })` | `Promise<https.Server>` with certificates and dispatchers; does not listen. Certificate assurance defaults to `true`. Requires production-mode `init()`. |
|
|
349
349
|
| `createServingHttpsServer({ servername, port?, tlsOptions? })` | Same helper with `ensureCertificate: false`; set `autoCertificates: false` for a serving-only instance. |
|
|
350
350
|
| `virtualServer.onClose(fn)` | Register a cleanup hook; returns the virtual server. See [shutdown](#startup-and-shutdown). |
|
|
351
|
+
| `Roster.createHttpsServer(options, handler?)` | `https.createServer()` with per-host `SNICallback`, `setSecureContext()`, and adopted sockets on both Node and Bun. Roster creates its HTTPS servers with it. |
|
|
352
|
+
| `Roster.acceptTlsConnection(server, socket)` | Hand a raw client socket, with any bytes already read put back, to a server from `createHttpsServer()` to terminate TLS. |
|
|
351
353
|
| `Roster.pluginSource` | Symbol for a request plugin's `{ module, export, options }` descriptor. See [site isolation](#site-isolation). |
|
|
352
354
|
|
|
353
355
|
## Request plugins
|
|
@@ -378,7 +380,7 @@ Enable `trustProxy` only behind a trusted proxy that overwrites `X-Forwarded-For
|
|
|
378
380
|
| Certificates are missing in a worker | Ensure the manager has issued them into the same store before worker startup. `init()` alone does not bind ACME HTTP. |
|
|
379
381
|
| Shutdown times out | Inspect the aggregated errors and site hooks, active requests, and pending certificate operations. |
|
|
380
382
|
|
|
381
|
-
Local integration checks cover Node HTTP/TLS and Socket.IO polling/WebSocket shutdown on Node and Bun
|
|
383
|
+
Local integration checks cover Node HTTP/TLS and Socket.IO polling/WebSocket shutdown on Node and Bun, and the test suite runs on both. Bun's `node:https` server never invokes `SNICallback`, lacks `addContext()` and `setSecureContext()`, and cannot adopt a socket passed to `emit('connection')` (checked on Bun 1.3.4 and 1.4.4). On Bun, Roster therefore terminates TLS with a `tls.Server` in front of an `http.Server`, so per-host certificates, certificate reload, custom HTTPS ports and site isolation behave as on Node. The visible difference is the server's class: on Bun, `Roster.createHttpsServer()`, `createManagedHttpsServer()` and `createServingHttpsServer()` return an `http.Server` whose `listen()`, `close()`, `address()` and certificate methods act on the inner `tls.Server`, so `instanceof https.Server` is `false` there.
|
|
382
384
|
|
|
383
385
|
## Agent skill and development
|
|
384
386
|
|
|
@@ -388,7 +390,7 @@ The [RosterServer skill](skills/roster-server/SKILL.md) guides application integ
|
|
|
388
390
|
npx skills add https://github.com/clasen/RosterServer --skill roster-server
|
|
389
391
|
```
|
|
390
392
|
|
|
391
|
-
Contributors can run `pnpm test` for the Node test suite. Examples live in [demo](demo); they are separate from the published API reference above.
|
|
393
|
+
Contributors can run `pnpm test` for the Node test suite and `pnpm test:bun` to run the same suite under Bun. Examples live in [demo](demo); they are separate from the published API reference above.
|
|
392
394
|
|
|
393
395
|
## License
|
|
394
396
|
|
package/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const http = require('http');
|
|
4
|
-
const https = require('https');
|
|
5
4
|
const net = require('net');
|
|
6
5
|
const tls = require('tls');
|
|
7
6
|
const crypto = require('crypto');
|
|
8
7
|
const { EventEmitter } = require('events');
|
|
9
8
|
const Greenlock = require('./vendor/greenlock-express/greenlock-express.js');
|
|
10
9
|
const GreenlockShim = require('./vendor/greenlock-express/greenlock-shim.js');
|
|
10
|
+
const { createHttpsServer, acceptTlsConnection, isBunRuntime } = require('./lib/https-server.js');
|
|
11
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');
|
|
@@ -58,8 +58,6 @@ function invokeRequest(handler, req, res) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const isBunRuntime = typeof Bun !== 'undefined' || (typeof process !== 'undefined' && process.release?.name === 'bun');
|
|
62
|
-
|
|
63
61
|
// CRC32 implementation for deterministic port assignment
|
|
64
62
|
function crc32(str) {
|
|
65
63
|
const crcTable = [];
|
|
@@ -372,9 +370,6 @@ class Roster {
|
|
|
372
370
|
}
|
|
373
371
|
this.disableWildcard = parseBooleanFlag(options.disableWildcard, false);
|
|
374
372
|
this.combineWildcardCerts = parseBooleanFlag(options.combineWildcardCerts, false);
|
|
375
|
-
if (isBunRuntime && this.combineWildcardCerts) {
|
|
376
|
-
log.info('Bun runtime detected: combined wildcard certificates enabled (SNI bypass)');
|
|
377
|
-
}
|
|
378
373
|
|
|
379
374
|
this.skipLocalCheck = parseBooleanFlag(options.skipLocalCheck, true);
|
|
380
375
|
this.autoCertificates = parseBooleanFlag(options.autoCertificates, true);
|
|
@@ -1015,7 +1010,7 @@ class Roster {
|
|
|
1015
1010
|
return;
|
|
1016
1011
|
}
|
|
1017
1012
|
socket.unshift(head);
|
|
1018
|
-
server
|
|
1013
|
+
acceptTlsConnection(server, socket);
|
|
1019
1014
|
};
|
|
1020
1015
|
socket.on('data', read);
|
|
1021
1016
|
}
|
|
@@ -1488,7 +1483,8 @@ class Roster {
|
|
|
1488
1483
|
Promise.allSettled([...this._certificateChecks.values(), ...this._contextLoads.values()])
|
|
1489
1484
|
]));
|
|
1490
1485
|
if (isBunRuntime) {
|
|
1491
|
-
|
|
1486
|
+
// Isolation routers are plain TCP servers; their sockets are in _sockets.
|
|
1487
|
+
for (const server of this._ownedServers) server.closeAllConnections?.();
|
|
1492
1488
|
await wait(Promise.all([...this._ownedServers].map(closeServer)));
|
|
1493
1489
|
}
|
|
1494
1490
|
|
|
@@ -1582,7 +1578,7 @@ class Roster {
|
|
|
1582
1578
|
? await this.ensureCertificate(normalizedServername)
|
|
1583
1579
|
: this.loadCertificate(normalizedServername);
|
|
1584
1580
|
|
|
1585
|
-
const server = this._configureSiteServer(
|
|
1581
|
+
const server = this._configureSiteServer(createHttpsServer({
|
|
1586
1582
|
minVersion: this.tlsMinVersion,
|
|
1587
1583
|
maxVersion: this.tlsMaxVersion,
|
|
1588
1584
|
...tlsOptions,
|
|
@@ -1661,13 +1657,7 @@ class Roster {
|
|
|
1661
1657
|
if (this._closing) return Promise.reject(new Error('Roster is closing or closed'));
|
|
1662
1658
|
if (!this._startPromise) {
|
|
1663
1659
|
// Isolation requires Roster to own the listeners, so only start() initializes for it.
|
|
1664
|
-
if (this.isolation && !this._initPromise)
|
|
1665
|
-
if (isBunRuntime) {
|
|
1666
|
-
log.warn('⚠️ Bun runtime detected: site isolation is not supported; all sites run in the main process');
|
|
1667
|
-
} else {
|
|
1668
|
-
this._isolate = true;
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1660
|
+
if (this.isolation && !this._initPromise) this._isolate = true;
|
|
1671
1661
|
this._startPromise = this._start().catch(async error => {
|
|
1672
1662
|
if (!this._closing) {
|
|
1673
1663
|
try { await this.close(); } catch (cleanupError) {
|
|
@@ -1705,7 +1695,6 @@ class Roster {
|
|
|
1705
1695
|
this._assertOpen();
|
|
1706
1696
|
{
|
|
1707
1697
|
const httpServer = glx.httpServer();
|
|
1708
|
-
const bunTlsHotReloadHandlers = [];
|
|
1709
1698
|
|
|
1710
1699
|
await this._listen(httpServer, 80, this.hostname);
|
|
1711
1700
|
log.info('HTTP server listening on port 80');
|
|
@@ -1717,182 +1706,28 @@ class Roster {
|
|
|
1717
1706
|
const portData = this._sitesByPort[port] || { virtualServers: {}, appHandlers: {} };
|
|
1718
1707
|
const dispatcher = this._createDispatcher(portData);
|
|
1719
1708
|
const upgradeHandler = this._createUpgradeHandler(portData);
|
|
1709
|
+
const tlsOpts = { minVersion: this.tlsMinVersion, maxVersion: this.tlsMaxVersion };
|
|
1720
1710
|
|
|
1721
|
-
|
|
1722
|
-
const host = this._normalizeHostInput(servername).trim().toLowerCase();
|
|
1723
|
-
if (!host) return null;
|
|
1724
|
-
|
|
1725
|
-
let pems = this._resolvePemsForServername(host);
|
|
1726
|
-
if (pems) return pems;
|
|
1727
|
-
|
|
1728
|
-
try {
|
|
1729
|
-
await this._checkCertificate(host);
|
|
1730
|
-
} catch (error) {
|
|
1731
|
-
log.warn(`⚠️ Greenlock issuance failed for ${host}: ${error?.message || error}`);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
pems = this._resolvePemsForServername(host);
|
|
1735
|
-
if (pems) return pems;
|
|
1736
|
-
|
|
1737
|
-
const wildcardSubject = wildcardSubjectForHost(host);
|
|
1738
|
-
const zone = wildcardSubject ? wildcardRoot(wildcardSubject) : null;
|
|
1739
|
-
if (zone) {
|
|
1740
|
-
const bootstrapHost = `bun-bootstrap.${zone}`;
|
|
1741
|
-
try {
|
|
1742
|
-
await this._checkCertificate(bootstrapHost);
|
|
1743
|
-
} catch (error) {
|
|
1744
|
-
log.warn(`⚠️ Greenlock wildcard bootstrap failed for ${bootstrapHost}: ${error?.message || error}`);
|
|
1745
|
-
}
|
|
1746
|
-
pems = this._resolvePemsForServername(host);
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
return pems;
|
|
1750
|
-
};
|
|
1751
|
-
|
|
1752
|
-
const ensureBunDefaultPems = async (primaryDomain) => {
|
|
1753
|
-
let pems = await issueAndReloadPemsForServername(primaryDomain);
|
|
1754
|
-
|
|
1755
|
-
const needsWildcard = this.combineWildcardCerts
|
|
1756
|
-
&& this.wildcardZones.has(primaryDomain)
|
|
1757
|
-
&& this.dnsChallenge;
|
|
1758
|
-
|
|
1759
|
-
if (pems && needsWildcard && !certCoversName(pems.cert, `*.${primaryDomain}`)) {
|
|
1760
|
-
log.warn(`⚠️ Existing cert for ${primaryDomain} lacks *.${primaryDomain} SAN — clearing stale cert for combined re-issuance`);
|
|
1761
|
-
const certDir = path.join(this.greenlockStorePath, 'live', primaryDomain);
|
|
1762
|
-
try { fs.rmSync(certDir, { recursive: true, force: true }); } catch {}
|
|
1763
|
-
pems = null;
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
if (pems) return pems;
|
|
1767
|
-
|
|
1768
|
-
const certSubject = primaryDomain.startsWith('*.') ? wildcardRoot(primaryDomain) : primaryDomain;
|
|
1769
|
-
log.warn(`⚠️ Bun: requesting ${needsWildcard ? 'combined wildcard' : ''} certificate for ${certSubject} via Greenlock before HTTPS bind`);
|
|
1770
|
-
try {
|
|
1771
|
-
await this._checkCertificate(certSubject);
|
|
1772
|
-
} catch (error) {
|
|
1773
|
-
log.error(`❌ Failed to obtain certificate for ${certSubject} under Bun:`, error?.message || error);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
pems = this._resolvePemsForServername(primaryDomain);
|
|
1777
|
-
if (pems) return pems;
|
|
1778
|
-
|
|
1779
|
-
throw new Error(
|
|
1780
|
-
`Bun runtime could not load TLS certificate files for ${primaryDomain}. ` +
|
|
1781
|
-
`Refusing to start HTTPS on port ${portNum} to avoid serving invalid TLS.`
|
|
1782
|
-
);
|
|
1783
|
-
};
|
|
1784
|
-
|
|
1711
|
+
let httpsServer;
|
|
1785
1712
|
if (portNum === this.defaultPort) {
|
|
1786
|
-
|
|
1787
|
-
let httpsServer;
|
|
1788
|
-
|
|
1789
|
-
if (isBunRuntime) {
|
|
1790
|
-
const primaryDomain = Object.keys(portData.virtualServers)[0];
|
|
1791
|
-
let defaultPems = await ensureBunDefaultPems(primaryDomain);
|
|
1792
|
-
let defaultContext = tls.createSecureContext(defaultPems);
|
|
1793
|
-
httpsServer = https.createServer({
|
|
1794
|
-
...tlsOpts,
|
|
1795
|
-
key: defaultPems.key,
|
|
1796
|
-
cert: defaultPems.cert,
|
|
1797
|
-
SNICallback: (servername, callback) => {
|
|
1798
|
-
const resolveContext = async () => {
|
|
1799
|
-
const cached = await this._resolveSecureContext(servername);
|
|
1800
|
-
if (cached) return cached;
|
|
1801
|
-
const pems = await issueAndReloadPemsForServername(servername);
|
|
1802
|
-
this._assertOpen();
|
|
1803
|
-
return pems ? this._getSecureContext(servername, false) : defaultContext;
|
|
1804
|
-
};
|
|
1805
|
-
resolveContext().then(context => callback(null, context), callback);
|
|
1806
|
-
}
|
|
1807
|
-
}, dispatcher);
|
|
1808
|
-
const reloadBunDefaultTls = async (servername, reason) => {
|
|
1809
|
-
const nextPems = await issueAndReloadPemsForServername(servername);
|
|
1810
|
-
if (!nextPems) return false;
|
|
1811
|
-
this._assertOpen();
|
|
1812
|
-
defaultPems = nextPems;
|
|
1813
|
-
defaultContext = tls.createSecureContext(defaultPems);
|
|
1814
|
-
if (typeof httpsServer.setSecureContext === 'function') {
|
|
1815
|
-
try {
|
|
1816
|
-
httpsServer.setSecureContext({ key: defaultPems.key, cert: defaultPems.cert });
|
|
1817
|
-
log.info(`🔄 Bun TLS default certificate reloaded on port ${portNum} (${reason})`);
|
|
1818
|
-
} catch (error) {
|
|
1819
|
-
log.warn(`⚠️ Failed to hot-reload Bun TLS context on port ${portNum}: ${error?.message || error}`);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
return true;
|
|
1823
|
-
};
|
|
1824
|
-
bunTlsHotReloadHandlers.push(reloadBunDefaultTls);
|
|
1825
|
-
log.warn(`⚠️ Bun runtime detected: using file-based TLS with SNI for ${primaryDomain} on port ${portNum}`);
|
|
1826
|
-
} else {
|
|
1827
|
-
httpsServer = glx.httpsServer(tlsOpts, dispatcher);
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
httpsServer.on('upgrade', upgradeHandler);
|
|
1831
|
-
|
|
1832
|
-
await this._listenSite(httpsServer, portNum);
|
|
1833
|
-
log.info(`HTTPS server listening on port ${portNum}`);
|
|
1713
|
+
httpsServer = glx.httpsServer(tlsOpts, dispatcher);
|
|
1834
1714
|
} else {
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
maxVersion: this.tlsMaxVersion,
|
|
1715
|
+
httpsServer = createHttpsServer({
|
|
1716
|
+
...tlsOpts,
|
|
1838
1717
|
SNICallback: (servername, callback) => {
|
|
1839
1718
|
this._getSecureContext(servername, false).then(context => callback(null, context), callback);
|
|
1840
1719
|
}
|
|
1841
|
-
};
|
|
1842
|
-
|
|
1843
|
-
const httpsServer = https.createServer(httpsOptions, dispatcher);
|
|
1844
|
-
httpsServer.on('upgrade', upgradeHandler);
|
|
1845
|
-
|
|
1720
|
+
}, dispatcher);
|
|
1846
1721
|
httpsServer.on('tlsClientError', (error) => {
|
|
1847
1722
|
if (!error.message.includes('http request')) {
|
|
1848
1723
|
log.error(`TLS error on port ${portNum}:`, error.message);
|
|
1849
1724
|
}
|
|
1850
1725
|
});
|
|
1851
|
-
|
|
1852
|
-
await this._listenSite(httpsServer, portNum);
|
|
1853
|
-
log.info(`HTTPS server listening on port ${portNum}`);
|
|
1854
1726
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
if (isBunRuntime && !this.combineWildcardCerts && this.wildcardZones.size > 0 && bunTlsHotReloadHandlers.length > 0) {
|
|
1858
|
-
const retryDelayMs = Number.isFinite(Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_RETRY_MS))
|
|
1859
|
-
? Math.max(1000, Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_RETRY_MS))
|
|
1860
|
-
: 30000;
|
|
1861
|
-
const maxAttempts = Number.isFinite(Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
|
|
1862
|
-
? Math.max(0, Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
|
|
1863
|
-
: 0;
|
|
1864
|
-
|
|
1865
|
-
for (const zone of this.wildcardZones) {
|
|
1866
|
-
const bootstrapHost = `bun-bootstrap.${zone}`;
|
|
1867
|
-
const attemptPrewarm = async (attempt = 1) => {
|
|
1868
|
-
if (this._closing) return;
|
|
1869
|
-
try {
|
|
1870
|
-
log.warn(`⚠️ Bun runtime detected: prewarming wildcard certificate via ${bootstrapHost} (attempt ${attempt})`);
|
|
1871
|
-
let reloaded = false;
|
|
1872
|
-
for (const reloadTls of bunTlsHotReloadHandlers) {
|
|
1873
|
-
reloaded = (await reloadTls(bootstrapHost, `prewarm ${bootstrapHost} attempt ${attempt}`)) || reloaded;
|
|
1874
|
-
}
|
|
1875
|
-
if (!reloaded) {
|
|
1876
|
-
throw new Error(`No certificate could be loaded for ${bootstrapHost}`);
|
|
1877
|
-
}
|
|
1878
|
-
log.info(`✅ Bun wildcard prewarm succeeded for ${zone} on attempt ${attempt}`);
|
|
1879
|
-
} catch (error) {
|
|
1880
|
-
log.warn(`⚠️ Bun wildcard prewarm failed for ${zone} (attempt ${attempt}): ${error?.message || error}`);
|
|
1881
|
-
if (maxAttempts > 0 && attempt >= maxAttempts) {
|
|
1882
|
-
log.warn(`⚠️ Bun wildcard prewarm stopped for ${zone} after ${attempt} attempts`);
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
if (this._closing) return;
|
|
1886
|
-
const timer = setTimeout(() => {
|
|
1887
|
-
this._retryTimers.delete(timer);
|
|
1888
|
-
attemptPrewarm(attempt + 1).catch(() => {});
|
|
1889
|
-
}, retryDelayMs);
|
|
1890
|
-
this._retryTimers.add(timer);
|
|
1891
|
-
}
|
|
1892
|
-
};
|
|
1727
|
+
httpsServer.on('upgrade', upgradeHandler);
|
|
1893
1728
|
|
|
1894
|
-
|
|
1895
|
-
}
|
|
1729
|
+
await this._listenSite(httpsServer, portNum);
|
|
1730
|
+
log.info(`HTTPS server listening on port ${portNum}`);
|
|
1896
1731
|
}
|
|
1897
1732
|
}
|
|
1898
1733
|
return this;
|
|
@@ -1901,6 +1736,8 @@ class Roster {
|
|
|
1901
1736
|
|
|
1902
1737
|
module.exports = Roster;
|
|
1903
1738
|
module.exports.isBunRuntime = isBunRuntime;
|
|
1739
|
+
module.exports.createHttpsServer = createHttpsServer;
|
|
1740
|
+
module.exports.acceptTlsConnection = acceptTlsConnection;
|
|
1904
1741
|
module.exports.pluginSource = PLUGIN_SOURCE;
|
|
1905
1742
|
module.exports.wildcardRoot = wildcardRoot;
|
|
1906
1743
|
module.exports.hostMatchesWildcard = hostMatchesWildcard;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// HTTPS servers that select certificates per host and adopt handed-off sockets on Node and Bun.
|
|
4
|
+
// Bun's https.Server never calls SNICallback and treats a socket passed to emit('connection') as plain
|
|
5
|
+
// text, but its tls.Server does both. On Bun, a tls.Server therefore terminates TLS in front of an
|
|
6
|
+
// http.Server, which is returned so request, upgrade and connection handling stay those of Node.
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const tls = require('tls');
|
|
10
|
+
|
|
11
|
+
const isBunRuntime = typeof Bun !== 'undefined' || process.release?.name === 'bun';
|
|
12
|
+
const kTlsServer = Symbol('roster.tlsServer');
|
|
13
|
+
|
|
14
|
+
function createHttpsServer(options = {}, handler) {
|
|
15
|
+
if (!isBunRuntime) return https.createServer(options, handler);
|
|
16
|
+
|
|
17
|
+
const server = http.createServer(handler);
|
|
18
|
+
const tlsServer = tls.createServer({ ALPNProtocols: ['http/1.1'], ...options });
|
|
19
|
+
server[kTlsServer] = tlsServer;
|
|
20
|
+
|
|
21
|
+
trackConnections(server);
|
|
22
|
+
tlsServer.on('secureConnection', socket => server.emit('connection', socket));
|
|
23
|
+
tlsServer.on('tlsClientError', (error, socket) => {
|
|
24
|
+
if (server.listenerCount('tlsClientError')) server.emit('tlsClientError', error, socket);
|
|
25
|
+
else socket.destroy();
|
|
26
|
+
});
|
|
27
|
+
tlsServer.on('listening', () => server.emit('listening'));
|
|
28
|
+
tlsServer.on('error', error => server.emit('error', error));
|
|
29
|
+
tlsServer.on('close', () => server.emit('close'));
|
|
30
|
+
|
|
31
|
+
server.listen = (...args) => {
|
|
32
|
+
tlsServer.listen(...args);
|
|
33
|
+
return server;
|
|
34
|
+
};
|
|
35
|
+
server.close = callback => {
|
|
36
|
+
tlsServer.close(callback);
|
|
37
|
+
server.closeIdleConnections();
|
|
38
|
+
return server;
|
|
39
|
+
};
|
|
40
|
+
server.address = () => tlsServer.address();
|
|
41
|
+
server.ref = () => { tlsServer.ref(); return server; };
|
|
42
|
+
server.unref = () => { tlsServer.unref(); return server; };
|
|
43
|
+
Object.defineProperty(server, 'listening', { get: () => tlsServer.listening });
|
|
44
|
+
server.setSecureContext = options => tlsServer.setSecureContext(options);
|
|
45
|
+
server.addContext = (hostname, context) => tlsServer.addContext(hostname, context);
|
|
46
|
+
return server;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Bun's http.Server neither applies keepAliveTimeout to connections passed to emit('connection') nor
|
|
50
|
+
// closes them in closeIdleConnections(), so the connection lifecycle is kept here.
|
|
51
|
+
function trackConnections(server) {
|
|
52
|
+
const active = new Set();
|
|
53
|
+
const idleTimers = new Map();
|
|
54
|
+
const setIdle = socket => {
|
|
55
|
+
active.delete(socket);
|
|
56
|
+
clearTimeout(idleTimers.get(socket));
|
|
57
|
+
const timeout = server.keepAliveTimeout;
|
|
58
|
+
idleTimers.set(socket, timeout > 0 ? setTimeout(() => socket.destroy(), timeout).unref() : null);
|
|
59
|
+
};
|
|
60
|
+
const forget = socket => {
|
|
61
|
+
active.delete(socket);
|
|
62
|
+
clearTimeout(idleTimers.get(socket));
|
|
63
|
+
idleTimers.delete(socket);
|
|
64
|
+
};
|
|
65
|
+
const onRequest = (req, res) => {
|
|
66
|
+
const socket = req.socket;
|
|
67
|
+
if (!idleTimers.has(socket)) return;
|
|
68
|
+
clearTimeout(idleTimers.get(socket));
|
|
69
|
+
idleTimers.set(socket, null);
|
|
70
|
+
active.add(socket);
|
|
71
|
+
res.once('finish', () => { if (idleTimers.has(socket) && !socket.destroyed) setIdle(socket); });
|
|
72
|
+
};
|
|
73
|
+
// Observed from emit() so the server's listeners stay exactly those its users added.
|
|
74
|
+
const emit = server.emit;
|
|
75
|
+
server.emit = function (event, ...args) {
|
|
76
|
+
if (event === 'connection') {
|
|
77
|
+
const socket = args[0];
|
|
78
|
+
idleTimers.set(socket, null);
|
|
79
|
+
socket.once('close', () => forget(socket));
|
|
80
|
+
} else if (event === 'request') {
|
|
81
|
+
onRequest(...args);
|
|
82
|
+
} else if (event === 'upgrade') {
|
|
83
|
+
// An upgraded socket belongs to its upgrade handler.
|
|
84
|
+
forget(args[0].socket);
|
|
85
|
+
}
|
|
86
|
+
return emit.call(this, event, ...args);
|
|
87
|
+
};
|
|
88
|
+
server.closeIdleConnections = () => {
|
|
89
|
+
for (const socket of idleTimers.keys()) if (!active.has(socket)) socket.destroy();
|
|
90
|
+
};
|
|
91
|
+
server.closeAllConnections = () => {
|
|
92
|
+
for (const socket of idleTimers.keys()) socket.destroy();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Hands a raw client socket, with any bytes already read pushed back, to an HTTPS server to terminate TLS.
|
|
97
|
+
function acceptTlsConnection(server, socket) {
|
|
98
|
+
(server[kTlsServer] || server).emit('connection', socket);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { createHttpsServer, acceptTlsConnection, isBunRuntime };
|
package/lib/site-worker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// Entry point of an isolated site process, forked by IsolatedSite.
|
|
4
|
-
const
|
|
4
|
+
const { createHttpsServer, acceptTlsConnection } = require('./https-server.js');
|
|
5
5
|
const net = require('net');
|
|
6
6
|
const Roster = require('../index.js');
|
|
7
7
|
const { resolveSiteApp, isSocketSite } = require('./resolve-site-app.js');
|
|
@@ -131,7 +131,7 @@ class SiteWorkerRoster extends Roster {
|
|
|
131
131
|
for (const port of ports) {
|
|
132
132
|
// Only the default port issues missing certificates, as in the main process.
|
|
133
133
|
const issue = port === this.defaultPort;
|
|
134
|
-
const server = this._configureSiteServer(
|
|
134
|
+
const server = this._configureSiteServer(createHttpsServer({
|
|
135
135
|
minVersion: this.tlsMinVersion,
|
|
136
136
|
maxVersion: this.tlsMaxVersion,
|
|
137
137
|
SNICallback: (servername, callback) => {
|
|
@@ -157,7 +157,7 @@ class SiteWorkerRoster extends Roster {
|
|
|
157
157
|
this._sockets.add(socket);
|
|
158
158
|
socket.once('close', () => this._sockets.delete(socket));
|
|
159
159
|
socket.unshift(head);
|
|
160
|
-
server
|
|
160
|
+
acceptTlsConnection(server, socket);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roster-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --test 'test/**/*.test.js'"
|
|
7
|
+
"test": "node --test 'test/**/*.test.js'",
|
|
8
|
+
"test:bun": "bun test ./test"
|
|
8
9
|
},
|
|
9
10
|
"repository": {
|
|
10
11
|
"type": "git",
|
|
@@ -41,14 +42,12 @@
|
|
|
41
42
|
"@root/csr": "^0.8.1",
|
|
42
43
|
"@root/encoding": "^1.0.1",
|
|
43
44
|
"@root/keypairs": "^0.10.0",
|
|
44
|
-
"@root/mkdirp": "^1.0.0",
|
|
45
45
|
"@root/request": "^1.6.1",
|
|
46
46
|
"acme-dns-01-cli": "^3.0.7",
|
|
47
47
|
"acme-http-01-standalone": "^3.0.5",
|
|
48
48
|
"cert-info": "^1.5.1",
|
|
49
49
|
"greenlock-store-fs": "^3.2.2",
|
|
50
50
|
"lemonlog": "^1.2.0",
|
|
51
|
-
"redirect-https": "^1.3.1"
|
|
52
|
-
"safe-replace": "^1.1.0"
|
|
51
|
+
"redirect-https": "^1.3.1"
|
|
53
52
|
}
|
|
54
53
|
}
|
|
@@ -72,7 +72,7 @@ A virtual request listener owns its request even before it ends the response. Ro
|
|
|
72
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
73
|
- `combineWildcardCerts` combines apex/www/wildcard issuance using DNS-01; `disableWildcard` ignores wildcard sites. Do not enable either as a generic troubleshooting step.
|
|
74
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
|
-
-
|
|
75
|
+
- Bun's own `https.Server` ignores `SNICallback` and cannot adopt passed sockets; Roster works around it with `Roster.createHttpsServer()` (a `tls.Server` in front of an `http.Server` on Bun), so per-host certificates, reload, custom ports and isolation match Node. Socket sites that terminate TLS should use `Roster.createHttpsServer()` and `Roster.acceptTlsConnection(server, socket)` rather than `https.createServer()` to work on Bun. Verify both runtimes with `pnpm test` and `pnpm test:bun`.
|
|
76
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
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.
|
|
78
78
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The HTTPS server factory must behave the same on Node and Bun: run with `pnpm test` and `pnpm test:bun`.
|
|
4
|
+
const { it, before } = require('node:test');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const net = require('node:net');
|
|
10
|
+
const tls = require('node:tls');
|
|
11
|
+
const https = require('node:https');
|
|
12
|
+
const { execFileSync } = require('node:child_process');
|
|
13
|
+
const Roster = require('../index.js');
|
|
14
|
+
const { createHttpsServer, acceptTlsConnection } = Roster;
|
|
15
|
+
const { parseClientHello } = require('../lib/client-hello.js');
|
|
16
|
+
|
|
17
|
+
const pems = {};
|
|
18
|
+
before(() => {
|
|
19
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-https-server-'));
|
|
20
|
+
for (const [name, serial] of [['one.test', 1], ['two.test', 2]]) {
|
|
21
|
+
const key = path.join(directory, `${name}.key`);
|
|
22
|
+
const cert = path.join(directory, `${name}.crt`);
|
|
23
|
+
execFileSync('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', key, '-out', cert,
|
|
24
|
+
'-subj', `/CN=${name}`, '-days', '1', '-set_serial', String(serial)], { stdio: 'ignore' });
|
|
25
|
+
pems[name] = { key: fs.readFileSync(key), cert: fs.readFileSync(cert) };
|
|
26
|
+
}
|
|
27
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const sni = (servername, callback) => callback(null, tls.createSecureContext(pems[servername] || pems['one.test']));
|
|
31
|
+
|
|
32
|
+
async function listen(t, server) {
|
|
33
|
+
t.after(() => new Promise(resolve => {
|
|
34
|
+
server.closeAllConnections?.();
|
|
35
|
+
server.close(() => resolve());
|
|
36
|
+
}));
|
|
37
|
+
await new Promise((resolve, reject) => {
|
|
38
|
+
server.once('error', reject);
|
|
39
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
40
|
+
});
|
|
41
|
+
return server.address().port;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function get(port, servername, agent = false) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
https.get({ hostname: '127.0.0.1', port, servername, rejectUnauthorized: false, agent }, res => {
|
|
47
|
+
const serial = res.socket.getPeerCertificate().serialNumber;
|
|
48
|
+
let body = '';
|
|
49
|
+
res.on('data', chunk => { body += chunk; });
|
|
50
|
+
res.on('end', () => resolve({ serial, body }));
|
|
51
|
+
}).on('error', reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it('reports the runtime it is loaded in', () => {
|
|
56
|
+
assert.equal(Roster.isBunRuntime, typeof process.versions.bun === 'string');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('selects the certificate of each host through SNICallback', async t => {
|
|
60
|
+
const server = createHttpsServer({ ...pems['one.test'], SNICallback: sni },
|
|
61
|
+
(req, res) => res.end(`${req.socket.encrypted} ${req.socket.servername}`));
|
|
62
|
+
const port = await listen(t, server);
|
|
63
|
+
assert.equal(server.listening, true);
|
|
64
|
+
assert.deepEqual(await get(port, 'one.test'), { serial: '01', body: 'true one.test' });
|
|
65
|
+
assert.deepEqual(await get(port, 'two.test'), { serial: '02', body: 'true two.test' });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('replaces the default certificate with setSecureContext', async t => {
|
|
69
|
+
const server = createHttpsServer(pems['one.test'], (req, res) => res.end('ok'));
|
|
70
|
+
const port = await listen(t, server);
|
|
71
|
+
assert.equal((await get(port, '')).serial, '01');
|
|
72
|
+
server.setSecureContext(pems['two.test']);
|
|
73
|
+
assert.equal((await get(port, '')).serial, '02');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('terminates TLS on a raw socket handed over after its ClientHello was read', async t => {
|
|
77
|
+
const server = createHttpsServer({ SNICallback: sni }, (req, res) => res.end('adopted'));
|
|
78
|
+
const front = net.createServer(socket => {
|
|
79
|
+
let head = Buffer.alloc(0);
|
|
80
|
+
const read = chunk => {
|
|
81
|
+
head = Buffer.concat([head, chunk]);
|
|
82
|
+
if (parseClientHello(head) === undefined) return;
|
|
83
|
+
socket.removeListener('data', read);
|
|
84
|
+
socket.pause();
|
|
85
|
+
socket.unshift(head);
|
|
86
|
+
acceptTlsConnection(server, socket);
|
|
87
|
+
};
|
|
88
|
+
socket.on('data', read);
|
|
89
|
+
});
|
|
90
|
+
const port = await listen(t, front);
|
|
91
|
+
assert.deepEqual(await get(port, 'two.test'), { serial: '02', body: 'adopted' });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('keeps connections alive between requests and closes them once idle', async t => {
|
|
95
|
+
const server = createHttpsServer({ SNICallback: sni }, (req, res) => res.end(req.url));
|
|
96
|
+
server.keepAliveTimeout = 300;
|
|
97
|
+
let connections = 0;
|
|
98
|
+
server.on('connection', () => connections++);
|
|
99
|
+
const port = await listen(t, server);
|
|
100
|
+
const { bodies, idleMs } = await new Promise((resolve, reject) => {
|
|
101
|
+
const socket = tls.connect({ host: '127.0.0.1', port, servername: 'one.test', rejectUnauthorized: false });
|
|
102
|
+
const send = pathname => socket.write(`GET ${pathname} HTTP/1.1\r\nHost: one.test\r\nConnection: keep-alive\r\n\r\n`);
|
|
103
|
+
const bodies = [];
|
|
104
|
+
let buffer = '';
|
|
105
|
+
let answeredAt = null;
|
|
106
|
+
socket.once('secureConnect', () => send('/first'));
|
|
107
|
+
socket.on('data', data => {
|
|
108
|
+
buffer += data;
|
|
109
|
+
const responses = buffer.split('HTTP/1.1 ').slice(1).map(response => response.split('\r\n\r\n')[1]).filter(Boolean);
|
|
110
|
+
if (responses.length === 1 && bodies.length === 0) {
|
|
111
|
+
bodies.push(responses[0]);
|
|
112
|
+
send('/second');
|
|
113
|
+
} else if (responses.length === 2 && bodies.length === 1) {
|
|
114
|
+
bodies.push(responses[1]);
|
|
115
|
+
answeredAt = Date.now();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
socket.on('close', () => resolve({ bodies, idleMs: Date.now() - answeredAt }));
|
|
119
|
+
socket.on('error', reject);
|
|
120
|
+
});
|
|
121
|
+
assert.deepEqual(bodies, ['/first', '/second']);
|
|
122
|
+
assert.equal(connections, 1);
|
|
123
|
+
assert.ok(idleMs >= 250 && idleMs < 2500, `connection stayed open ${idleMs} ms`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('closes idle connections but lets an active request finish', async t => {
|
|
127
|
+
let release;
|
|
128
|
+
const server = createHttpsServer({ SNICallback: sni }, (req, res) => {
|
|
129
|
+
if (req.url === '/slow') release = () => res.end('slow');
|
|
130
|
+
else res.end('fast');
|
|
131
|
+
});
|
|
132
|
+
const port = await listen(t, server);
|
|
133
|
+
const agent = new https.Agent({ keepAlive: true, rejectUnauthorized: false });
|
|
134
|
+
t.after(() => agent.destroy());
|
|
135
|
+
await get(port, 'one.test', agent);
|
|
136
|
+
const idleSocket = Object.values(agent.freeSockets).flat()[0];
|
|
137
|
+
const idleClosed = new Promise(resolve => idleSocket.once('close', resolve));
|
|
138
|
+
const slow = new Promise((resolve, reject) => {
|
|
139
|
+
https.get({ hostname: '127.0.0.1', port, path: '/slow', servername: 'one.test', rejectUnauthorized: false, agent: false },
|
|
140
|
+
res => { let body = ''; res.on('data', chunk => { body += chunk; }); res.on('end', () => resolve(body)); })
|
|
141
|
+
.on('error', reject);
|
|
142
|
+
});
|
|
143
|
+
while (!release) await new Promise(resolve => setTimeout(resolve, 10));
|
|
144
|
+
server.closeIdleConnections();
|
|
145
|
+
await idleClosed;
|
|
146
|
+
release();
|
|
147
|
+
assert.equal(await slow, 'slow');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('delivers upgrade requests with their socket', async t => {
|
|
151
|
+
const server = createHttpsServer({ SNICallback: sni }, (req, res) => res.end('not upgraded'));
|
|
152
|
+
server.on('upgrade', (req, socket) => {
|
|
153
|
+
socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: echo\r\nConnection: Upgrade\r\n\r\n');
|
|
154
|
+
socket.on('data', data => socket.write(`echo:${data}`));
|
|
155
|
+
});
|
|
156
|
+
server.keepAliveTimeout = 50;
|
|
157
|
+
const port = await listen(t, server);
|
|
158
|
+
const echoed = await new Promise((resolve, reject) => {
|
|
159
|
+
const socket = tls.connect({ host: '127.0.0.1', port, servername: 'one.test', rejectUnauthorized: false }, () => {
|
|
160
|
+
socket.write('GET / HTTP/1.1\r\nHost: one.test\r\nConnection: Upgrade\r\nUpgrade: echo\r\n\r\n');
|
|
161
|
+
});
|
|
162
|
+
let buffer = '';
|
|
163
|
+
socket.on('data', data => {
|
|
164
|
+
buffer += data;
|
|
165
|
+
if (buffer.includes('\r\n\r\n') && !buffer.includes('echo:')) setTimeout(() => socket.write('ping'), 200);
|
|
166
|
+
if (buffer.includes('echo:ping')) {
|
|
167
|
+
socket.destroy();
|
|
168
|
+
resolve(buffer.split('\r\n\r\n')[1]);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
socket.on('error', reject);
|
|
172
|
+
});
|
|
173
|
+
assert.equal(echoed, 'echo:ping');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('reports listening, address and close like a Node server', async t => {
|
|
177
|
+
const server = createHttpsServer({ SNICallback: sni }, (req, res) => res.end('ok'));
|
|
178
|
+
const events = [];
|
|
179
|
+
server.on('listening', () => events.push('listening'));
|
|
180
|
+
server.on('close', () => events.push('close'));
|
|
181
|
+
assert.equal(server.listening, false);
|
|
182
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
183
|
+
assert.equal(server.address().address, '127.0.0.1');
|
|
184
|
+
await new Promise(resolve => server.close(resolve));
|
|
185
|
+
assert.equal(server.listening, false);
|
|
186
|
+
assert.deepEqual(events, ['listening', 'close']);
|
|
187
|
+
const error = await new Promise(resolve => server.close(resolve));
|
|
188
|
+
assert.equal(error?.code, 'ERR_SERVER_NOT_RUNNING');
|
|
189
|
+
});
|
package/test/isolation.test.js
CHANGED
|
@@ -13,6 +13,7 @@ const { once } = require('node:events');
|
|
|
13
13
|
const { execFileSync, spawn } = require('node:child_process');
|
|
14
14
|
const Roster = require('../index.js');
|
|
15
15
|
const { parseClientHello } = require('../lib/client-hello.js');
|
|
16
|
+
const { createHttpsServer } = require('../lib/https-server.js');
|
|
16
17
|
const { createScannerBlocker } = require('../plugins/scanner-blocker.js');
|
|
17
18
|
const shim = require('../vendor/greenlock-express/greenlock-shim.js');
|
|
18
19
|
const greenlock = require('../vendor/greenlock-express/greenlock-express.js');
|
|
@@ -64,7 +65,7 @@ async function startProduction(t, sites, { certificates = [], issue = async () =
|
|
|
64
65
|
ready(callback) {
|
|
65
66
|
setImmediate(() => callback({
|
|
66
67
|
httpServer: () => http.createServer(),
|
|
67
|
-
httpsServer: (options, handler) =>
|
|
68
|
+
httpsServer: (options, handler) => createHttpsServer({ ...options, SNICallback: roster.sniCallback() }, handler)
|
|
68
69
|
}));
|
|
69
70
|
return this;
|
|
70
71
|
}
|
|
@@ -410,6 +411,7 @@ it('closes isolated sites when the main process dies without closing', { timeout
|
|
|
410
411
|
|
|
411
412
|
function socketSite(server, marker, start = 'async start() {}') {
|
|
412
413
|
return `const fs = require('fs');
|
|
414
|
+
const { createHttpsServer, acceptTlsConnection } = require(${JSON.stringify(path.join(__dirname, '..', 'index.js'))});
|
|
413
415
|
let server = ${server};
|
|
414
416
|
module.exports = {
|
|
415
417
|
${start},
|
|
@@ -417,7 +419,7 @@ module.exports = {
|
|
|
417
419
|
fs.writeFileSync(${JSON.stringify(marker)} + '.head', String(head.length));
|
|
418
420
|
// A ClientHello read by Roster goes back on the stream; a local connection arrives paused.
|
|
419
421
|
if (head.length > 0) socket.unshift(head);
|
|
420
|
-
server
|
|
422
|
+
acceptTlsConnection(server, socket);
|
|
421
423
|
if (head.length === 0) socket.resume();
|
|
422
424
|
},
|
|
423
425
|
reply(req, res) {
|
|
@@ -435,7 +437,7 @@ it('hands raw client sockets with their ClientHello to a socket site that termin
|
|
|
435
437
|
// The site gets its certificate from Roster's store through start(), not from files it reads.
|
|
436
438
|
const start = `async start({ certificate }) {
|
|
437
439
|
const pems = await certificate('raw.example.com');
|
|
438
|
-
server =
|
|
440
|
+
server = createHttpsServer(pems, (req, res) => module.exports.reply(req, res));
|
|
439
441
|
}`;
|
|
440
442
|
const { roster, port } = await startProduction(t, {
|
|
441
443
|
'raw.example.com': socketSite('null', marker, start),
|
|
@@ -458,7 +460,7 @@ it('issues a missing certificate through the main process when a socket site ask
|
|
|
458
460
|
const store = path.join(path.dirname(marker), 'greenlock.d');
|
|
459
461
|
const issued = [];
|
|
460
462
|
const start = `async start({ certificate }) {
|
|
461
|
-
server =
|
|
463
|
+
server = createHttpsServer(await certificate('fresh.example.com'), (req, res) => module.exports.reply(req, res));
|
|
462
464
|
}`;
|
|
463
465
|
const { port } = await startProduction(t, { 'fresh.example.com': socketSite('null', marker, start) }, {
|
|
464
466
|
options: { greenlockStorePath: store },
|
package/test/lifecycle.test.js
CHANGED
|
@@ -337,7 +337,11 @@ it('preserves initialization and cleanup errors when both fail', async t => {
|
|
|
337
337
|
});
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
-
it('cancels a pending listen without opening a port when DNS resolves after shutdown', {
|
|
340
|
+
it('cancels a pending listen without opening a port when DNS resolves after shutdown', {
|
|
341
|
+
timeout: 5000,
|
|
342
|
+
// Bun resolves listen() hostnames natively, so mocking node:dns cannot hold the lookup open.
|
|
343
|
+
skip: Roster.isBunRuntime && 'Bun listen() does not use node:dns.lookup'
|
|
344
|
+
}, async t => {
|
|
341
345
|
const dns = require('node:dns');
|
|
342
346
|
let resolveLookup;
|
|
343
347
|
t.mock.method(dns, 'lookup', (hostname, options, callback) => { resolveLookup = callback; });
|
|
@@ -52,7 +52,8 @@ Servers.create = function(greenlock, onError) {
|
|
|
52
52
|
};
|
|
53
53
|
servers.httpsServer = function(secureOpts, defaultApp) {
|
|
54
54
|
return servers._httpsServer(secureOpts, defaultApp, function(secureOpts, fn) {
|
|
55
|
-
|
|
55
|
+
// Roster's factory keeps per-host SNI working on Bun, whose https.Server ignores SNICallback.
|
|
56
|
+
return require("../../lib/https-server.js").createHttpsServer(secureOpts, fn);
|
|
56
57
|
});
|
|
57
58
|
};
|
|
58
59
|
servers._httpsServer = function(secureOpts, defaultApp, createSecureServer) {
|