Skip to content

Commit 8e6bb3c

Browse files
committed
Build a proper framework for TLS endpoints
1 parent 108775e commit 8e6bb3c

File tree

14 files changed

+345
-309
lines changed

14 files changed

+345
-309
lines changed

src/endpoints/endpoint-index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import * as httpIndex from './http-index.js';
2+
import * as tlsIndex from './tls-index.js';
23

34
export const httpEndpoints: Array<httpIndex.HttpEndpoint & { name: string }> = Object.entries(httpIndex)
45
.map(([key, value]) => ({ ...value, name: key }));
6+
7+
export const tlsEndpoints: Array<tlsIndex.TlsEndpoint & { name: string }> = Object.entries(tlsIndex)
8+
.map(([key, value]) => ({ ...value, name: key }));

src/endpoints/tls-index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as tls from 'tls';
2+
import { CertOptions } from '../tls-certificates/cert-definitions.js';
3+
4+
export interface TlsEndpoint {
5+
sniPart: string;
6+
configureCertOptions?(): CertOptions;
7+
configureTlsOptions?(tlsOptions: tls.SecureContextOptions): tls.SecureContextOptions;
8+
configureAlpnPreferences?(preferences: string[]): string[];
9+
}
10+
11+
export * from './tls/alpn-specifiers.js';
12+
export * from './tls/cert-modes.js';
13+
export * from './tls/example.js';
14+
export * from './tls/no-tls.js';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TlsEndpoint } from '../tls-index.js';
2+
3+
export const http2: TlsEndpoint = {
4+
sniPart: 'http2',
5+
configureAlpnPreferences(alpnList) {
6+
alpnList.push('h2');
7+
return alpnList;
8+
}
9+
};
10+
11+
export const http1: TlsEndpoint = {
12+
sniPart: 'http1',
13+
configureAlpnPreferences(alpnList) {
14+
alpnList.push('http/1.1');
15+
return alpnList;
16+
}
17+
};

src/endpoints/tls/cert-modes.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { TlsEndpoint } from '../tls-index.js';
2+
3+
export const expired: TlsEndpoint = {
4+
sniPart: 'expired',
5+
configureCertOptions() {
6+
return {
7+
expired: true
8+
};
9+
}
10+
};
11+
12+
export const revoked: TlsEndpoint = {
13+
sniPart: 'revoked',
14+
configureCertOptions() {
15+
return {
16+
revoked: true
17+
};
18+
}
19+
};
20+
21+
export const selfSigned: TlsEndpoint = {
22+
sniPart: 'self-signed',
23+
configureCertOptions() {
24+
return {
25+
requiredType: 'local',
26+
selfSigned: true
27+
};
28+
}
29+
};
30+
31+
export const untrustedRoot: TlsEndpoint = {
32+
sniPart: 'untrusted-root',
33+
configureCertOptions() {
34+
return {
35+
requiredType: 'local'
36+
};
37+
}
38+
};
39+
40+
export const wrongHost: TlsEndpoint = {
41+
sniPart: 'wrong-host',
42+
configureCertOptions() {
43+
return {
44+
overridePrefix: 'example'
45+
};
46+
}
47+
};

src/endpoints/tls/example.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TlsEndpoint } from '../tls-index.js';
2+
3+
export const example: TlsEndpoint = {
4+
sniPart: 'example'
5+
};

src/endpoints/tls/no-tls.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { TlsEndpoint } from '../tls-index.js';
2+
3+
export const noTls: TlsEndpoint = {
4+
sniPart: 'no-tls',
5+
configureTlsOptions() {
6+
throw new Error('Intentionally rejecting TLS connection');
7+
},
8+
};

src/server.ts

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
} from 'read-tls-client-hello';
99

1010
import { createHttp1Handler, createHttp2Handler } from './http-handler.js';
11-
import { createTlsHandler, CertMode } from './tls-handler.js';
11+
import { createTlsHandler } from './tls-handler.js';
12+
import { CertOptions } from './tls-certificates/cert-definitions.js';
1213
import { ConnectionProcessor } from './process-connection.js';
1314

1415
import { AcmeCA, AcmeProvider } from './tls-certificates/acme.js';
@@ -60,8 +61,8 @@ async function generateTlsConfig(options: ServerOptions) {
6061
await certCache.loadCache();
6162
}
6263

63-
const ca = await LocalCA.create(caCert);
64-
const defaultCert = await ca.generateCertificate(rootDomain);
64+
const localCA = await LocalCA.create(caCert);
65+
const defaultCert = await localCA.generateCertificate(rootDomain, {});
6566

6667
if (!options.acmeProvider) {
6768
console.log('Using self signed certificates');
@@ -70,12 +71,13 @@ async function generateTlsConfig(options: ServerOptions) {
7071
key: defaultCert.key,
7172
cert: defaultCert.cert,
7273
ca: caCert.cert,
73-
localCA: ca,
74-
generateCertificate: async (domain: string, mode?: CertMode) => {
75-
if (mode === 'self-signed') return await ca.generateSelfSignedCertificate(domain);
76-
if (mode === 'expired') return await ca.generateExpiredCertificate(domain);
77-
if (mode === 'revoked') return await ca.generateRevokedCertificate(domain);
78-
return await ca.generateCertificate(domain);
74+
localCA,
75+
generateCertificate: async (domain: string, options: CertOptions) => {
76+
if (options.requiredType === 'acme') {
77+
throw new Error(`Can't generate cert for ${domain} without ACME`);
78+
}
79+
80+
return await localCA.generateCertificate(domain, options);
7981
},
8082
acmeChallenge: () => undefined // Not supported
8183
};
@@ -94,40 +96,30 @@ async function generateTlsConfig(options: ServerOptions) {
9496
}
9597

9698
const acmeCA = new AcmeCA(certCache!, options.acmeProvider, options.acmeAccountKey);
97-
acmeCA.tryGetCertificateSync(rootDomain); // Preload the root domain every time
99+
acmeCA.tryGetCertificateSync(rootDomain, {}); // Preload the root domain every time
98100

99101
return {
100102
rootDomain,
101103
proactiveCertDomains: options.proactiveCertDomains,
102104
key: defaultCert.key,
103105
cert: defaultCert.cert,
104106
ca: caCert.cert,
105-
localCA: ca,
106-
generateCertificate: async (domain: string, mode?: CertMode) => {
107-
if (mode === 'self-signed') return await ca.generateSelfSignedCertificate(domain);
108-
109-
if (mode === 'expired') {
110-
// Try to get an actually-expired ACME cert; fall back to LocalCA if not expired yet
111-
const expiredAcmeCert = acmeCA.tryGetExpiredCertificateSync(domain);
112-
if (expiredAcmeCert) return expiredAcmeCert;
113-
return await ca.generateExpiredCertificate(domain);
107+
localCA,
108+
generateCertificate: async (domain: string, options: CertOptions) => {
109+
if (options.requiredType === 'local') {
110+
return await localCA.generateCertificate(domain, options);
114111
}
115112

116-
if (mode === 'revoked') {
117-
// Try to get a revoked ACME cert; fall back to LocalCA revoked cert
118-
const revokedAcmeCert = acmeCA.tryGetRevokedCertificateSync(domain);
119-
if (revokedAcmeCert) return revokedAcmeCert;
120-
return await ca.generateRevokedCertificate(domain);
121-
}
113+
const cert = acmeCA.tryGetCertificateSync(domain, options);
122114

123-
if (domain === rootDomain || domain.endsWith('.' + rootDomain)) {
124-
const cert = acmeCA.tryGetCertificateSync(domain);
125-
if (cert) return cert;
115+
if (cert) {
116+
return cert;
117+
} else {
118+
if (options.requiredType === 'acme') {
119+
return await acmeCA.waitForCertificate(domain, options);
120+
}
121+
return await localCA.generateCertificate(domain, options);
126122
}
127-
128-
// If you use some other domain or the cert isn't immediately available, we fall back
129-
// to self-signed certs for now:
130-
return await ca.generateCertificate(domain);
131123
},
132124
acmeChallenge: (token: string) => acmeCA.getChallengeResponse(token)
133125
}

0 commit comments

Comments
 (0)