roster-server 2.4.2 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.greenlockrc ADDED
@@ -0,0 +1 @@
1
+ {"configDir":"/Users/martinclasen/Stuff/greenlock.d"}
package/README.md CHANGED
@@ -247,6 +247,8 @@ When creating a new `RosterServer` instance, you can pass the following options:
247
247
  - `greenlockStorePath` (string): Directory for Greenlock configuration.
248
248
  - `dnsChallenge` (object|false): Optional override for wildcard DNS-01 challenge config. Default is `acme-dns-01-cli` wrapper with `propagationDelay: 120000`, `autoContinue: false`, and `dryRunDelay: 120000`. Manual mode still works, but you can enable automatic Linode DNS API mode by setting `ROSTER_DNS_PROVIDER=linode` and `LINODE_API_KEY`. In automatic mode, Roster creates/removes TXT records itself and still polls public resolvers every 15s before continuing. Set `false` to disable DNS challenge. You can pass `{ module: '...', propagationDelay: 180000 }` to tune DNS wait time (ms). For Greenlock dry-runs (`_greenlock-dryrun-*`), delay defaults to `dryRunDelay` (same as `propagationDelay` unless overridden with `dnsChallenge.dryRunDelay` or env `ROSTER_DNS_DRYRUN_DELAY_MS`). When wildcard sites are present, Roster creates a separate wildcard certificate (`*.example.com`) that uses `dns-01`, while apex/www stay on the regular certificate flow (typically `http-01`), reducing manual TXT records.
249
249
  - `staging` (boolean): Set to `true` to use Let's Encrypt's staging environment (for testing).
250
+ - `autoCertificates` (boolean): Enables automatic certificate issuance and renewal in production lifecycle. **Default: `true`**. Set to `false` only if certificates are managed externally.
251
+ - `certificateRenewIntervalMs` (number): Renewal check interval when `autoCertificates` is enabled (minimum 60s, default 12h).
250
252
  - `local` (boolean): Set to `true` to run in local development mode.
251
253
  - `minLocalPort` (number): Minimum port for local mode (default: 4000).
252
254
  - `maxLocalPort` (number): Maximum port for local mode (default: 9999).
@@ -400,7 +402,7 @@ Returns the WebSocket upgrade dispatcher for a given port. Routes upgrades to th
400
402
 
401
403
  #### `roster.sniCallback()` → `(servername, callback) => void`
402
404
 
403
- Returns a TLS SNI callback that resolves certificates from `greenlockStorePath` on disk. Not available in local mode.
405
+ Returns a TLS SNI callback. It resolves certificates from `greenlockStorePath` and, when `autoCertificates` is enabled (default), can issue missing certificates automatically. Not available in local mode.
404
406
 
405
407
  #### `roster.attach(server, { port }?)` → `Roster`
406
408
 
@@ -0,0 +1,84 @@
1
+ const cluster = require('cluster');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const https = require('https');
5
+ const Roster = require('../index.js');
6
+
7
+ // Change these values to your target domain and HTTPS port.
8
+ const CONFIG = {
9
+ domain: 'chinchon.blyts.com',
10
+ httpsPort: 4336,
11
+ workers: Math.max(1, Math.min(2, os.cpus().length)),
12
+ wwwPath: path.join(__dirname, 'www'),
13
+ greenlockStorePath: path.join(__dirname, '..', 'greenlock.d'),
14
+ email: 'mclasen@blyts.com',
15
+ staging: false,
16
+ autoCertificates: true
17
+ };
18
+
19
+ async function startWorker() {
20
+ const roster = new Roster({
21
+ local: false,
22
+ email: CONFIG.email,
23
+ staging: CONFIG.staging,
24
+ wwwPath: CONFIG.wwwPath,
25
+ greenlockStorePath: CONFIG.greenlockStorePath,
26
+ autoCertificates: CONFIG.autoCertificates
27
+ });
28
+
29
+ // Domain is configured from CONFIG (single source of truth).
30
+ roster.register(CONFIG.domain, () => {
31
+ return (req, res) => {
32
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
33
+ res.end(`HTTPS cluster response from worker ${process.pid} for ${CONFIG.domain}`);
34
+ };
35
+ });
36
+
37
+ await roster.init();
38
+ // Automatic cert issuance by roster-server (and renewal loop) in cluster-friendly mode.
39
+ const defaultPems = await roster.ensureCertificate(CONFIG.domain);
40
+
41
+ const server = https.createServer({
42
+ key: defaultPems.key,
43
+ cert: defaultPems.cert,
44
+ SNICallback: roster.sniCallback(),
45
+ minVersion: 'TLSv1.2',
46
+ maxVersion: 'TLSv1.3'
47
+ });
48
+
49
+ roster.attach(server);
50
+
51
+ server.listen(CONFIG.httpsPort, () => {
52
+ console.log(`[worker ${process.pid}] listening on https://0.0.0.0:${CONFIG.httpsPort} for domain ${CONFIG.domain}`);
53
+ });
54
+ }
55
+
56
+ async function startPrimary() {
57
+ console.log(`\n[primary ${process.pid}] starting ${CONFIG.workers} workers`);
58
+ console.log(`domain=${CONFIG.domain} port=${CONFIG.httpsPort}`);
59
+ console.log(`wwwPath=${CONFIG.wwwPath}`);
60
+ console.log(`greenlockStorePath=${CONFIG.greenlockStorePath}\n`);
61
+ console.log(`[primary] cert lifecycle managed by roster-server (autoCertificates=${CONFIG.autoCertificates})\n`);
62
+
63
+ for (let i = 0; i < CONFIG.workers; i++) {
64
+ cluster.fork();
65
+ }
66
+
67
+ cluster.on('exit', (worker) => {
68
+ console.log(`[primary] worker ${worker.process.pid} exited, restarting...`);
69
+ cluster.fork();
70
+ });
71
+ }
72
+
73
+ if (cluster.isPrimary) {
74
+ startPrimary().catch((err) => {
75
+ console.error(`[primary ${process.pid}] startup failed`, err);
76
+ process.exit(1);
77
+ });
78
+ } else {
79
+ startWorker().catch((err) => {
80
+ console.error(`[worker ${process.pid}] startup failed`, err);
81
+ process.exit(1);
82
+ });
83
+ }
84
+
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: eyap8usul0
3
+ type: decision
4
+ title: 'Decision update: autoCertificates default true'
5
+ created: '2026-03-20 23:53:16'
6
+ ---
7
+ # Decision update: autoCertificates default true
8
+
9
+ **What changed**: `autoCertificates` now defaults to `true` instead of `false`.
10
+
11
+ **Why**: User expectation and product value are that SSL lifecycle should be automatic and robust by default in RosterServer.
12
+
13
+ **Implementation**:
14
+ - Constructor default changed in `index.js` (`parseBooleanFlag(options.autoCertificates, true)`).
15
+ - Added constructor tests for default true and explicit opt-out (`autoCertificates: false`).
16
+ - Updated docs (`README.md`, `skills/roster-server/SKILL.md`) to reflect default-on behavior.
17
+
18
+ **Guardrails**:
19
+ - Users can still opt out with `autoCertificates: false` when certs are externally managed.
20
+ - `ensureCertificate()` keeps explicit error message when autoCertificates is disabled.
package/index.js CHANGED
@@ -268,6 +268,12 @@ class Roster {
268
268
  }
269
269
 
270
270
  this.skipLocalCheck = parseBooleanFlag(options.skipLocalCheck, true);
271
+ this.autoCertificates = parseBooleanFlag(options.autoCertificates, true);
272
+ this.certificateRenewIntervalMs = Number.isFinite(Number(options.certificateRenewIntervalMs))
273
+ ? Math.max(60000, Number(options.certificateRenewIntervalMs))
274
+ : 12 * 60 * 60 * 1000;
275
+ this._greenlockRuntime = null;
276
+ this._certificateRenewTimer = null;
271
277
 
272
278
  const port = options.port === undefined ? 443 : options.port;
273
279
  if (port === 80 && !this.local) {
@@ -775,28 +781,175 @@ class Roster {
775
781
 
776
782
  _initSniResolver() {
777
783
  this._sniCallback = (servername, callback) => {
784
+ const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase();
778
785
  try {
779
- const pems = this._resolvePemsForServername(servername);
786
+ const pems = this._resolvePemsForServername(normalizedServername);
780
787
  if (pems) {
781
788
  callback(null, tls.createSecureContext({ key: pems.key, cert: pems.cert }));
782
- } else {
783
- callback(new Error(`No certificate files available for ${servername}`));
789
+ return;
784
790
  }
785
791
  } catch (error) {
786
792
  callback(error);
793
+ return;
794
+ }
795
+
796
+ // Cluster-friendly automatic issuance path (no internal listen lifecycle).
797
+ if (!this._greenlockRuntime || !normalizedServername) {
798
+ callback(new Error(`No certificate files available for ${servername}`));
799
+ return;
787
800
  }
801
+
802
+ this._greenlockRuntime.get({ servername: normalizedServername })
803
+ .then(() => {
804
+ const issued = this._resolvePemsForServername(normalizedServername);
805
+ if (issued) {
806
+ callback(null, tls.createSecureContext({ key: issued.key, cert: issued.cert }));
807
+ } else {
808
+ callback(new Error(`No certificate files available for ${servername}`));
809
+ }
810
+ })
811
+ .catch((error) => {
812
+ callback(error);
813
+ });
788
814
  };
789
815
  }
790
816
 
817
+ _buildGreenlockOptions() {
818
+ return {
819
+ packageRoot: __dirname,
820
+ configDir: this.greenlockStorePath,
821
+ maintainerEmail: this.email,
822
+ cluster: this.cluster,
823
+ staging: this.staging,
824
+ skipDryRun: this.skipLocalCheck,
825
+ skipChallengeTest: this.skipLocalCheck,
826
+ notify: (event, details) => {
827
+ const eventDomain = (() => {
828
+ if (!details || typeof details !== 'object') return null;
829
+
830
+ const directKeys = ['subject', 'servername', 'domain', 'hostname', 'host'];
831
+ for (const key of directKeys) {
832
+ if (typeof details[key] === 'string' && details[key].trim()) {
833
+ return details[key].trim().toLowerCase();
834
+ }
835
+ }
836
+
837
+ if (Array.isArray(details.altnames) && details.altnames.length > 0) {
838
+ const alt = details.altnames.find(name => typeof name === 'string' && name.trim());
839
+ if (alt) return alt.trim().toLowerCase();
840
+ }
841
+
842
+ if (Array.isArray(details.domains) && details.domains.length > 0) {
843
+ const domain = details.domains.find(name => typeof name === 'string' && name.trim());
844
+ if (domain) return domain.trim().toLowerCase();
845
+ }
846
+
847
+ if (details.identifier && typeof details.identifier.value === 'string' && details.identifier.value.trim()) {
848
+ return details.identifier.value.trim().toLowerCase();
849
+ }
850
+
851
+ return null;
852
+ })();
853
+
854
+ let msg;
855
+ if (typeof details === 'string') {
856
+ msg = details;
857
+ } else if (details instanceof Error) {
858
+ msg = details.stack || details.message;
859
+ } else if (details && typeof details === 'object' && typeof details.message === 'string') {
860
+ msg = details.message;
861
+ } else {
862
+ try {
863
+ msg = JSON.stringify(details);
864
+ } catch {
865
+ msg = String(details);
866
+ }
867
+ }
868
+ if (!msg || msg === 'undefined') msg = `[${event}] (no details)`;
869
+ if (eventDomain && !msg.includes(`[${eventDomain}]`)) {
870
+ msg = `[${eventDomain}] ${msg}`;
871
+ }
872
+ if (event === 'warning' && typeof msg === 'string') {
873
+ if (/acme-dns-01-cli.*(incorrect function signatures|deprecated use of callbacks)/i.test(msg)) return;
874
+ if (/dns-01 challenge plugin should have zones/i.test(msg)) return;
875
+ }
876
+ if (event === 'error') log.error(msg);
877
+ else if (event === 'warning') log.warn(msg);
878
+ else log.info(msg);
879
+ }
880
+ };
881
+ }
882
+
883
+ _getManagedCertificateSubjects() {
884
+ const uniqueDomains = new Set();
885
+ this.domains.forEach((domain) => {
886
+ const root = domain.startsWith('*.') ? wildcardRoot(domain) : domain.replace(/^www\./, '');
887
+ if (root) uniqueDomains.add(root);
888
+ });
889
+ const subjects = [];
890
+ uniqueDomains.forEach((domain) => {
891
+ subjects.push(domain);
892
+ const includeWildcard = this.wildcardZones.has(domain) && this.dnsChallenge && !this.combineWildcardCerts;
893
+ if (includeWildcard) subjects.push(`*.${domain}`);
894
+ });
895
+ return [...new Set(subjects)];
896
+ }
897
+
898
+ _startCertificateRenewLoop() {
899
+ if (!this._greenlockRuntime || this._certificateRenewTimer) return;
900
+ const subjects = this._getManagedCertificateSubjects();
901
+ if (subjects.length === 0) return;
902
+ this._certificateRenewTimer = setInterval(() => {
903
+ subjects.forEach((subject) => {
904
+ this._greenlockRuntime.get({ servername: subject }).catch((error) => {
905
+ log.warn(`⚠️ Certificate renew check failed for ${subject}: ${error?.message || error}`);
906
+ });
907
+ });
908
+ }, this.certificateRenewIntervalMs);
909
+ if (typeof this._certificateRenewTimer.unref === 'function') {
910
+ this._certificateRenewTimer.unref();
911
+ }
912
+ }
913
+
914
+ async ensureCertificate(servername) {
915
+ if (this.local) {
916
+ throw new Error('ensureCertificate() is not available in local mode');
917
+ }
918
+ if (!this._initialized) {
919
+ throw new Error('Call init() before ensureCertificate()');
920
+ }
921
+ const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase();
922
+ if (!normalizedServername) {
923
+ throw new Error('servername is required');
924
+ }
925
+ let pems = this._resolvePemsForServername(normalizedServername);
926
+ if (pems) return pems;
927
+ if (!this._greenlockRuntime) {
928
+ throw new Error('autoCertificates is disabled; enable { autoCertificates: true } to issue certificates automatically');
929
+ }
930
+ await this._greenlockRuntime.get({ servername: normalizedServername });
931
+ pems = this._resolvePemsForServername(normalizedServername);
932
+ if (!pems) {
933
+ throw new Error(`Certificate issuance completed but no PEM files were found for ${normalizedServername}`);
934
+ }
935
+ return pems;
936
+ }
937
+
791
938
  async init() {
792
939
  if (this._initialized) return this;
793
940
  await this.loadSites();
794
941
  if (!this.local) {
795
942
  this.generateConfigJson();
943
+ if (this.autoCertificates) {
944
+ this._greenlockRuntime = GreenlockShim.create(this._buildGreenlockOptions());
945
+ }
796
946
  }
797
947
  this._initSiteHandlers();
798
948
  if (!this.local) {
799
949
  this._initSniResolver();
950
+ if (this.autoCertificates) {
951
+ this._startCertificateRenewLoop();
952
+ }
800
953
  }
801
954
  this._initialized = true;
802
955
  return this;
@@ -891,69 +1044,7 @@ class Roster {
891
1044
  return this.startLocalMode();
892
1045
  }
893
1046
 
894
- const greenlockOptions = {
895
- packageRoot: __dirname,
896
- configDir: this.greenlockStorePath,
897
- maintainerEmail: this.email,
898
- cluster: this.cluster,
899
- staging: this.staging,
900
- skipDryRun: this.skipLocalCheck,
901
- skipChallengeTest: this.skipLocalCheck,
902
- notify: (event, details) => {
903
- const eventDomain = (() => {
904
- if (!details || typeof details !== 'object') return null;
905
-
906
- const directKeys = ['subject', 'servername', 'domain', 'hostname', 'host'];
907
- for (const key of directKeys) {
908
- if (typeof details[key] === 'string' && details[key].trim()) {
909
- return details[key].trim().toLowerCase();
910
- }
911
- }
912
-
913
- if (Array.isArray(details.altnames) && details.altnames.length > 0) {
914
- const alt = details.altnames.find(name => typeof name === 'string' && name.trim());
915
- if (alt) return alt.trim().toLowerCase();
916
- }
917
-
918
- if (Array.isArray(details.domains) && details.domains.length > 0) {
919
- const domain = details.domains.find(name => typeof name === 'string' && name.trim());
920
- if (domain) return domain.trim().toLowerCase();
921
- }
922
-
923
- if (details.identifier && typeof details.identifier.value === 'string' && details.identifier.value.trim()) {
924
- return details.identifier.value.trim().toLowerCase();
925
- }
926
-
927
- return null;
928
- })();
929
-
930
- let msg;
931
- if (typeof details === 'string') {
932
- msg = details;
933
- } else if (details instanceof Error) {
934
- msg = details.stack || details.message;
935
- } else if (details && typeof details === 'object' && typeof details.message === 'string') {
936
- msg = details.message;
937
- } else {
938
- try {
939
- msg = JSON.stringify(details);
940
- } catch {
941
- msg = String(details);
942
- }
943
- }
944
- if (!msg || msg === 'undefined') msg = `[${event}] (no details)`;
945
- if (eventDomain && !msg.includes(`[${eventDomain}]`)) {
946
- msg = `[${eventDomain}] ${msg}`;
947
- }
948
- if (event === 'warning' && typeof msg === 'string') {
949
- if (/acme-dns-01-cli.*(incorrect function signatures|deprecated use of callbacks)/i.test(msg)) return;
950
- if (/dns-01 challenge plugin should have zones/i.test(msg)) return;
951
- }
952
- if (event === 'error') log.error(msg);
953
- else if (event === 'warning') log.warn(msg);
954
- else log.info(msg);
955
- }
956
- };
1047
+ const greenlockOptions = this._buildGreenlockOptions();
957
1048
  const greenlockRuntime = GreenlockShim.create(greenlockOptions);
958
1049
  const greenlock = Greenlock.init({
959
1050
  ...greenlockOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -189,7 +189,10 @@ Returns `(req, res) => void` dispatcher for a port (defaults to `defaultPort`).
189
189
  Returns `(req, socket, head) => void` for WebSocket upgrade routing. Requires `init()` first.
190
190
 
191
191
  ### `roster.sniCallback()`
192
- Returns `(servername, callback) => void` TLS SNI callback that resolves certs from `greenlockStorePath`. Production mode only. Requires `init()` first.
192
+ 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.
193
+
194
+ ### `roster.ensureCertificate(servername)`
195
+ Forces certificate availability for a domain and returns `{ key, cert }`. With `autoCertificates` enabled (default), it issues certs automatically when missing.
193
196
 
194
197
  ### `roster.attach(server, { port }?)`
195
198
  Convenience: wires `requestHandler` + `upgradeHandler` onto an external `http.Server` or `https.Server`. Returns `this`. Requires `init()` first.
@@ -318,6 +318,14 @@ describe('Roster', () => {
318
318
  const roster = new Roster({ local: false, combineWildcardCerts: true });
319
319
  assert.strictEqual(roster.combineWildcardCerts, true);
320
320
  });
321
+ it('defaults autoCertificates to true', () => {
322
+ const roster = new Roster({ local: false });
323
+ assert.strictEqual(roster.autoCertificates, true);
324
+ });
325
+ it('allows disabling autoCertificates explicitly', () => {
326
+ const roster = new Roster({ local: false, autoCertificates: false });
327
+ assert.strictEqual(roster.autoCertificates, false);
328
+ });
321
329
  });
322
330
 
323
331
  describe('register (normal domain)', () => {
@@ -901,6 +909,30 @@ describe('Roster sniCallback()', () => {
901
909
  });
902
910
  });
903
911
 
912
+ describe('Roster ensureCertificate()', () => {
913
+ it('throws if called before init()', async () => {
914
+ const roster = new Roster({ local: false, autoCertificates: true });
915
+ await assert.rejects(() => roster.ensureCertificate('example.com'), /Call init\(\) before ensureCertificate/);
916
+ });
917
+
918
+ it('throws in local mode', async () => {
919
+ const roster = new Roster({ local: true, autoCertificates: true });
920
+ roster.register('local-cert.example', () => () => {});
921
+ await roster.init();
922
+ await assert.rejects(() => roster.ensureCertificate('local-cert.example'), /not available in local mode/);
923
+ });
924
+
925
+ it('throws when autoCertificates is disabled and cert is missing', async () => {
926
+ const roster = new Roster({ local: false, autoCertificates: false });
927
+ roster.register('missing-cert.example', () => () => {});
928
+ await roster.init();
929
+ await assert.rejects(
930
+ () => roster.ensureCertificate('missing-cert.example'),
931
+ /autoCertificates is disabled/
932
+ );
933
+ });
934
+ });
935
+
904
936
  describe('Roster attach()', () => {
905
937
  it('throws if called before init()', () => {
906
938
  const roster = new Roster({ local: true });
@@ -1,90 +0,0 @@
1
- const https = require('https');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const Roster = require('../index.js');
5
-
6
- function hasCertificateFor(storePath, subject) {
7
- const base = path.join(storePath, 'live', subject);
8
- return (
9
- fs.existsSync(path.join(base, 'privkey.pem')) &&
10
- fs.existsSync(path.join(base, 'cert.pem')) &&
11
- fs.existsSync(path.join(base, 'chain.pem'))
12
- );
13
- }
14
-
15
- async function main() {
16
- // In this demo, certs are expected to already exist in greenlock.d/live/<subject>.
17
- // init()+attach() do routing only; no ACME issuance/listen lifecycle is started.
18
- const greenlockStorePath = process.env.GREENLOCK_STORE_PATH || path.join(__dirname, '..', 'greenlock.d');
19
- const listenPort = Number(process.env.DEMO_HTTPS_PORT || 19643);
20
-
21
- const roster = new Roster({
22
- local: false,
23
- email: process.env.ADMIN_EMAIL || 'admin@example.com',
24
- greenlockStorePath
25
- });
26
-
27
- roster.register('example.com', () => {
28
- return (req, res) => {
29
- res.writeHead(200, { 'Content-Type': 'text/plain' });
30
- res.end('hello from external HTTPS worker (default port routes)');
31
- };
32
- });
33
-
34
- roster.register('api.example.com:9443', () => {
35
- return (req, res) => {
36
- res.writeHead(200, { 'Content-Type': 'application/json' });
37
- res.end(JSON.stringify({ ok: true, source: 'api.example.com:9443 route table' }));
38
- };
39
- });
40
-
41
- await roster.init();
42
-
43
- const missing = ['example.com', '*.example.com'].filter((subject) => !hasCertificateFor(greenlockStorePath, subject));
44
- if (missing.length > 0) {
45
- console.log('\n⚠️ Missing certificate files for:');
46
- missing.forEach((subject) => console.log(` - ${subject}`));
47
- console.log('\nExpected files:');
48
- console.log(' greenlock.d/live/<subject>/privkey.pem');
49
- console.log(' greenlock.d/live/<subject>/cert.pem');
50
- console.log(' greenlock.d/live/<subject>/chain.pem');
51
- console.log('\nTip: run regular roster.start() once (or your cert manager) to issue certs first.\n');
52
- }
53
-
54
- const server = https.createServer({
55
- SNICallback: roster.sniCallback(),
56
- minVersion: 'TLSv1.2',
57
- maxVersion: 'TLSv1.3'
58
- });
59
-
60
- // Attach default routing table (defaultPort = 443 routes)
61
- roster.attach(server);
62
-
63
- await new Promise((resolve, reject) => {
64
- server.listen(listenPort, '0.0.0.0', resolve);
65
- server.on('error', reject);
66
- });
67
-
68
- console.log('\n✅ HTTPS cluster-friendly demo running');
69
- console.log(`Listening on :${listenPort} with external server ownership`);
70
- console.log('Roster only provides SNI + vhost routing (no internal listen/bootstrap).\n');
71
-
72
- console.log('Try:');
73
- console.log(`curl -k --resolve example.com:${listenPort}:127.0.0.1 https://example.com:${listenPort}/`);
74
- console.log(`curl -k --resolve www.example.com:${listenPort}:127.0.0.1 https://www.example.com:${listenPort}/foo`);
75
- console.log('\nFor custom route table port=9443, attach another server:');
76
- console.log(' const srv9443 = https.createServer({ SNICallback: roster.sniCallback() });');
77
- console.log(' roster.attach(srv9443, { port: 9443 });');
78
- console.log(' srv9443.listen(19644);');
79
- console.log(` curl -k --resolve api.example.com:19644:127.0.0.1 https://api.example.com:19644/`);
80
-
81
- // Sticky-session runtime pattern:
82
- // process.on('message', (msg, socket) => {
83
- // if (msg === 'sticky-session:connection') server.emit('connection', socket);
84
- // });
85
- }
86
-
87
- main().catch((err) => {
88
- console.error('Demo failed:', err);
89
- process.exit(1);
90
- });