Skip to content
This repository has been archived by the owner on Jul 6, 2018. It is now read-only.

http2: ALPN fallback to HTTP/1.1 #125

Closed
wants to merge 8 commits into from
Closed
3 changes: 3 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@ server.listen(80);
* `noRecvClientMagic` {boolean} (TODO: Add detail)
* `paddingStrategy` {number} (TODO: Add detail)
* `peerMaxConcurrentStreams` {number} (TODO: Add detail)
* `allowHTTP1` {boolean} Incoming client connections that do not support
HTTP/2 will be downgraded to HTTP/1.x when set to `true`. The default value
is `false`, which rejects non-HTTP/2 client connections.
* `settings` {[Settings Object][]} The initial settings to send to the
remote peer upon connection.
* ...: Any [`tls.createServer()`][] options can be provided. For
Expand Down
11 changes: 9 additions & 2 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { Duplex } = require('stream');
const { URL } = require('url');
const { onServerStream } = require('internal/http2/compat');
const { utcDate } = require('internal/http');
const { _connectionListener: httpConnectionListener } = require('http');

const {
assertIsObject,
Expand Down Expand Up @@ -1255,6 +1256,13 @@ function connectionListener(socket) {
socket.on('timeout', socketOnTimeout);
}

// TLS ALPN fallback to HTTP/1.1
if (options.allowHTTP1 === true &&
(socket.alpnProtocol === false ||
socket.alpnProtocol === 'http/1.1')) {
return httpConnectionListener.call(this, socket);
}

socket.on('error', socketOnError);
socket.on('resume', socketOnResume);
socket.on('pause', socketOnPause);
Expand Down Expand Up @@ -1287,8 +1295,7 @@ function initializeOptions(options) {

function initializeTLSOptions(options, servername) {
options = initializeOptions(options);
options.ALPNProtocols = ['hc', 'h2'];
options.NPNProtocols = ['hc', 'h2'];
options.ALPNProtocols = ['h2', 'http/1.1'];
if (servername !== undefined && options.servername === undefined) {
options.servername = servername;
}
Expand Down
4 changes: 1 addition & 3 deletions test/parallel/test-http2-create-client-secure-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ function verifySecureSession(key, cert, ca, opts) {
req.on('end', common.mustCall(() => {
const jsonData = JSON.parse(data);
assert.strictEqual(jsonData.servername, opts.servername || 'localhost');
assert(
jsonData.alpnProtocol === 'h2' || jsonData.alpnProtocol === 'hc');
assert.strictEqual(jsonData.alpnProtocol, 'h2');
server.close();
client.socket.destroy();
}));
Expand All @@ -66,7 +65,6 @@ verifySecureSession(
loadKey('agent8-cert.pem'),
loadKey('fake-startcom-root-cert.pem'));


// Custom servername is specified.
verifySecureSession(
loadKey('agent1-key.pem'),
Expand Down
132 changes: 132 additions & 0 deletions test/parallel/test-http2-https-fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use strict';

const { fixturesDir, mustCall, mustNotCall } = require('../common');
const { strictEqual } = require('assert');
const { join } = require('path');
const { readFileSync } = require('fs');
const { createSecureContext } = require('tls');
const { createSecureServer, connect } = require('http2');
const { get } = require('https');
const { parse } = require('url');

const countdown = (count, done) => () => --count === 0 && done();

function loadKey(keyname) {
return readFileSync(join(fixturesDir, 'keys', keyname));
}

const key = loadKey('agent8-key.pem');
const cert = loadKey('agent8-cert.pem');
const ca = loadKey('fake-startcom-root-cert.pem');

const clientOptions = { secureContext: createSecureContext({ ca }) };

function onRequest(request, response) {
const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ?
request.stream.session : request;
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({
alpnProtocol,
httpVersion: request.httpVersion
}));
}

function onSession(session) {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'https',
':authority': `localhost:${this.server.address().port}`
};

const request = session.request(headers);
request.on('response', mustCall((headers) => {
strictEqual(headers[':status'], '200');
strictEqual(headers['content-type'], 'application/json');
}));
request.setEncoding('utf8');
let raw = '';
request.on('data', (chunk) => { raw += chunk; });
request.on('end', mustCall(() => {
const { alpnProtocol, httpVersion } = JSON.parse(raw);
strictEqual(alpnProtocol, 'h2');
strictEqual(httpVersion, '2.0');

session.destroy();
this.cleanup();
}));
request.end();
}

// HTTP/2 & HTTP/1.1 server
{
const server = createSecureServer(
{ cert, key, allowHTTP1: true },
mustCall(onRequest, 2)
);

server.listen(0);

server.on('listening', mustCall(() => {
const port = server.address().port;
const origin = `https://localhost:${port}`;

const cleanup = countdown(2, () => server.close());

// HTTP/2 client
connect(
origin,
clientOptions,
mustCall(onSession.bind({ cleanup, server }))
);

// HTTP/1.1 client
get(
Object.assign(parse(origin), clientOptions),
mustCall((response) => {
strictEqual(response.statusCode, 200);
strictEqual(response.statusMessage, 'OK');
strictEqual(response.headers['content-type'], 'application/json');

response.setEncoding('utf8');
let raw = '';
response.on('data', (chunk) => { raw += chunk; });
response.on('end', mustCall(() => {
const { alpnProtocol, httpVersion } = JSON.parse(raw);
strictEqual(alpnProtocol, false);
strictEqual(httpVersion, '1.1');

cleanup();
}));
})
);
}));
}

// HTTP/2-only server
{
const server = createSecureServer(
{ cert, key },
mustCall(onRequest)
);

server.listen(0);

server.on('listening', mustCall(() => {
const port = server.address().port;
const origin = `https://localhost:${port}`;

const cleanup = countdown(2, () => server.close());

// HTTP/2 client
connect(
origin,
clientOptions,
mustCall(onSession.bind({ cleanup, server }))
);

// HTTP/1.1 client
get(Object.assign(parse(origin), clientOptions), mustNotCall())
.on('error', mustCall(cleanup));
}));
}