Skip to content

Commit 2b1dc67

Browse files
committed
Add no-tls, http1 & http2 SNI-controlled TLS endpoints
You can now visit no-tls.testserver.host to get a server that will reject all TLS connections, http1.testserver.host to get an HTTP/1-only server, or http2.testserver.host to get an HTTP/2-only server. This can also be combined with all HTTP endpoints.
1 parent ca6f776 commit 2b1dc67

3 files changed

Lines changed: 111 additions & 2 deletions

File tree

src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ async function generateTlsConfig(options: ServerOptions) {
4141
if (!options.acmeProvider) {
4242
console.log('Using self signed certificates');
4343
return {
44+
rootDomain,
4445
key: defaultCert.key,
4546
cert: defaultCert.cert,
4647
ca: caCert.cert,
@@ -62,6 +63,7 @@ async function generateTlsConfig(options: ServerOptions) {
6263
acmeCA.tryGetCertificateSync(rootDomain); // Preload the root domain every time
6364

6465
return {
66+
rootDomain,
6567
key: defaultCert.key,
6668
cert: defaultCert.cert,
6769
ca: caCert.cert,

src/tls-handler.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as tls from 'tls';
33
import { ConnectionProcessor } from './process-connection.js';
44

55
interface TlsHandlerConfig {
6+
rootDomain: string;
67
key: string;
78
cert: string;
89
ca: string;
@@ -13,6 +14,18 @@ interface TlsHandlerConfig {
1314
};
1415
}
1516

17+
const supportedProtocolFilters: { [key: string]: string } = {
18+
'http2': 'h2',
19+
'http1': 'http/1.1'
20+
};
21+
22+
const getSNIPrefixParts = (servername: string, rootDomain: string) => {
23+
const serverNamePrefix = servername.endsWith(rootDomain)
24+
? servername.slice(0, -rootDomain.length - 1)
25+
: servername;
26+
return serverNamePrefix.split('.');
27+
};
28+
1629
export async function createTlsHandler(
1730
tlsConfig: TlsHandlerConfig,
1831
connProcessor: ConnectionProcessor
@@ -21,8 +34,29 @@ export async function createTlsHandler(
2134
key: tlsConfig.key,
2235
cert: tlsConfig.cert,
2336
ca: [tlsConfig.ca],
24-
ALPNProtocols: ['h2', 'http/1.1'],
37+
38+
ALPNCallback: ({ servername, protocols: clientProtocols }) => {
39+
// If specific protocol(s) are provided as part of the server name,
40+
// only negotiate those via ALPN.
41+
const serverNameParts = getSNIPrefixParts(servername, tlsConfig.rootDomain);
42+
43+
let protocolFilterNames = serverNameParts.filter(protocol =>
44+
supportedProtocolFilters[protocol]
45+
);
46+
const serverProtocols = protocolFilterNames.length > 0
47+
? protocolFilterNames.map(protocol => supportedProtocolFilters[protocol])
48+
: Object.values(supportedProtocolFilters);
49+
50+
// Follow the clients preferences, within the protocols we support:
51+
return clientProtocols.find(protocol => serverProtocols.includes(protocol));
52+
},
2553
SNICallback: (domain: string, cb: Function) => {
54+
const serverNameParts = getSNIPrefixParts(domain, tlsConfig.rootDomain);
55+
if (serverNameParts.includes('no-tls')) {
56+
// This closes the unwanted TLS connection without response
57+
return cb(new Error('Intentionally rejecting TLS connection'), null);
58+
}
59+
2660
try {
2761
const generatedCert = tlsConfig.generateCertificate(domain);
2862
cb(null, tls.createSecureContext({

test/https.spec.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as net from 'net';
22
import * as http from 'http';
33
import * as https from 'https';
4+
import * as tls from 'tls';
45
import * as streamConsumers from 'stream/consumers';
56

67
import { expect } from 'chai';
@@ -21,7 +22,7 @@ describe("HTTPS requests", () => {
2122

2223
afterEach(async () => {
2324
await server.destroy();
24-
})
25+
});
2526

2627
it("can connect successfully", async () => {
2728
const address = `https://localhost:${serverPort}/echo`;
@@ -49,4 +50,76 @@ Connection: keep-alive
4950
);
5051
});
5152

53+
it("cannot connect to no-tls.* connect successfully", async () => {
54+
const conn = tls.connect({
55+
port: serverPort,
56+
servername: 'no-tls.localhost',
57+
rejectUnauthorized: false // Needed as it's untrusted
58+
});
59+
60+
const result = await new Promise<string>((resolve, reject) => {
61+
conn.on('secureConnect', () => resolve('Connected'));
62+
conn.on('error', (err) => resolve(`Failed: ${err.message}`));
63+
});
64+
conn.destroy();
65+
66+
expect(result).to.equal('Failed: Client network socket disconnected before secure TLS connection was established');
67+
});
68+
69+
it("negotiates http2 for http2.*", async () => {
70+
const conn = tls.connect({
71+
port: serverPort,
72+
servername: 'http2.localhost',
73+
ALPNProtocols: ['http/1.1', 'h2'],
74+
rejectUnauthorized: false // Needed as it's untrusted
75+
});
76+
77+
const selectedProtocol = await new Promise<any>((resolve, reject) => {
78+
conn.on('secureConnect', () => resolve(conn.alpnProtocol));
79+
conn.on('error', reject);
80+
});
81+
conn.destroy();
82+
83+
expect(selectedProtocol).to.equal('h2');
84+
});
85+
86+
it("negotiates http1.1 for http1.*", async () => {
87+
const conn = tls.connect({
88+
port: serverPort,
89+
servername: 'http1.localhost',
90+
ALPNProtocols: ['h2', 'http/1.1'],
91+
rejectUnauthorized: false // Needed as it's untrusted
92+
});
93+
94+
const selectedProtocol = await new Promise<any>((resolve, reject) => {
95+
conn.on('secureConnect', () => resolve(conn.alpnProtocol));
96+
conn.on('error', reject);
97+
});
98+
conn.destroy();
99+
100+
expect(selectedProtocol).to.equal('http/1.1');
101+
});
102+
103+
it("follows client ALPN preference if all are supported", async () => {
104+
await Promise.all([
105+
['h2', 'http/1.1'],
106+
['http/1.1', 'h2']
107+
].map(async (protocols) => {
108+
const conn = tls.connect({
109+
port: serverPort,
110+
servername: 'do-anything.localhost',
111+
ALPNProtocols: protocols,
112+
rejectUnauthorized: false // Needed as it's untrusted
113+
});
114+
115+
const selectedProtocol = await new Promise<any>((resolve, reject) => {
116+
conn.on('secureConnect', () => resolve(conn.alpnProtocol));
117+
conn.on('error', reject);
118+
});
119+
conn.destroy();
120+
expect(selectedProtocol).to.equal(protocols[0]);
121+
}));
122+
});
123+
124+
52125
});

0 commit comments

Comments
 (0)