Skip to content

Commit

Permalink
Avoid rejecting unknown ALPN in Node v20.4+
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
pimterry committed Oct 4, 2023
1 parent b50532a commit 256a139
Showing 1 changed file with 30 additions and 8 deletions.
38 changes: 30 additions & 8 deletions src/server/http-combo-server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import _ = require('lodash');
import now = require("performance-now");
import now = require('performance-now');
import net = require('net');
import tls = require('tls');
import http = require('http');
import http2 = require('http2');
import * as streams from 'stream';

import * as semver from 'semver';
import { makeDestroyable, DestroyableServer } from 'destroyable-server';
import httpolyglot = require('@httptoolkit/httpolyglot');
import {
Expand Down Expand Up @@ -148,17 +150,37 @@ export async function createComboServer(
const ca = await getCA(options.https);
const defaultCert = ca.generateCertificate(options.https.defaultDomain ?? 'localhost');

const serverProtocolPreferences = options.http2 === true
? ['h2', 'http/1.1', 'http 1.1'] // 'http 1.1' is non-standard, but used by https-proxy-agent
: options.http2 === 'fallback'
? ['http/1.1', 'http 1.1', 'h2']
// options.http2 === false:
: ['http/1.1', 'http 1.1'];

const ALPNOption: tls.TlsOptions = semver.satisfies(process.version, '>=20.4.0')
? {
// In modern Node (20+), ALPNProtocols will reject unknown protocols. To allow those (so we can
// at least read the request, and hopefully handle HTTP-like cases - not uncommon) we use the new
// ALPNCallback feature instead, which lets us dynamically accept unrecognized protocols:
ALPNCallback: ({ protocols: clientProtocols }) => {
const preferredProtocol = serverProtocolPreferences.find(p => clientProtocols.includes(p));

// Wherever possible, we tell the client to use our preferred protocol
if (preferredProtocol) return preferredProtocol;

// If the client only offers protocols that we don't understand, shrug and accept:
else return clientProtocols[1];
}
} : {
// In Node versions without ALPNCallback, we just set preferences directly:
ALPNProtocols: serverProtocolPreferences
}

const tlsServer = tls.createServer({
key: defaultCert.key,
cert: defaultCert.cert,
ca: [defaultCert.ca],
ALPNProtocols: options.http2 === true
? ['h2', 'http/1.1', 'http 1.1'] // 'http 1.1' is non-standard, but used by https-proxy-agent
: options.http2 === 'fallback'
? ['http/1.1', 'http 1.1', 'h2']
// false
: ['http/1.1', 'http 1.1'],

...ALPNOption,
SNICallback: (domain: string, cb: Function) => {
if (options.debug) console.log(`Generating certificate for ${domain}`);

Expand Down

0 comments on commit 256a139

Please sign in to comment.