Skip to content

Commit 256a139

Browse files
committed
Avoid rejecting unknown ALPN in Node v20.4+
Unfortunately there's no possible fix for Node 20.0 - 20.3, which will instead reject all unknown ALPN connections. This can cause issues, e.g. for some Android apps that I've seen use GRPC ALPN as the only option (HTTP compatible, but not officially named as such) and other cases such as ALPN bugs (e.g. the incorrect https-proxy-agent ALPN string for H1).
1 parent b50532a commit 256a139

File tree

1 file changed

+30
-8
lines changed

1 file changed

+30
-8
lines changed

src/server/http-combo-server.ts

+30-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import _ = require('lodash');
2-
import now = require("performance-now");
2+
import now = require('performance-now');
33
import net = require('net');
44
import tls = require('tls');
55
import http = require('http');
66
import http2 = require('http2');
77
import * as streams from 'stream';
8+
9+
import * as semver from 'semver';
810
import { makeDestroyable, DestroyableServer } from 'destroyable-server';
911
import httpolyglot = require('@httptoolkit/httpolyglot');
1012
import {
@@ -148,17 +150,37 @@ export async function createComboServer(
148150
const ca = await getCA(options.https);
149151
const defaultCert = ca.generateCertificate(options.https.defaultDomain ?? 'localhost');
150152

153+
const serverProtocolPreferences = options.http2 === true
154+
? ['h2', 'http/1.1', 'http 1.1'] // 'http 1.1' is non-standard, but used by https-proxy-agent
155+
: options.http2 === 'fallback'
156+
? ['http/1.1', 'http 1.1', 'h2']
157+
// options.http2 === false:
158+
: ['http/1.1', 'http 1.1'];
159+
160+
const ALPNOption: tls.TlsOptions = semver.satisfies(process.version, '>=20.4.0')
161+
? {
162+
// In modern Node (20+), ALPNProtocols will reject unknown protocols. To allow those (so we can
163+
// at least read the request, and hopefully handle HTTP-like cases - not uncommon) we use the new
164+
// ALPNCallback feature instead, which lets us dynamically accept unrecognized protocols:
165+
ALPNCallback: ({ protocols: clientProtocols }) => {
166+
const preferredProtocol = serverProtocolPreferences.find(p => clientProtocols.includes(p));
167+
168+
// Wherever possible, we tell the client to use our preferred protocol
169+
if (preferredProtocol) return preferredProtocol;
170+
171+
// If the client only offers protocols that we don't understand, shrug and accept:
172+
else return clientProtocols[1];
173+
}
174+
} : {
175+
// In Node versions without ALPNCallback, we just set preferences directly:
176+
ALPNProtocols: serverProtocolPreferences
177+
}
178+
151179
const tlsServer = tls.createServer({
152180
key: defaultCert.key,
153181
cert: defaultCert.cert,
154182
ca: [defaultCert.ca],
155-
ALPNProtocols: options.http2 === true
156-
? ['h2', 'http/1.1', 'http 1.1'] // 'http 1.1' is non-standard, but used by https-proxy-agent
157-
: options.http2 === 'fallback'
158-
? ['http/1.1', 'http 1.1', 'h2']
159-
// false
160-
: ['http/1.1', 'http 1.1'],
161-
183+
...ALPNOption,
162184
SNICallback: (domain: string, cb: Function) => {
163185
if (options.debug) console.log(`Generating certificate for ${domain}`);
164186

0 commit comments

Comments
 (0)