Skip to content

Commit 1b8e324

Browse files
authored
Merge pull request #37 from kinetifex/http2
Add HTTP/2 support
2 parents 29c2b82 + 24a2fe4 commit 1b8e324

File tree

3 files changed

+163
-12
lines changed

3 files changed

+163
-12
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ a node-style callback. The config object must have at minimum an `http` or
3030
| `https.sni` | See [SNI Support](#sni-support). |
3131
| `https.handler` | Handler for HTTPS requests. If you want to share a handler with all servers, use a top-level `handler` config property instead. |
3232
| `https.*` | Any other properties supported by [https.createServer](https://nodejs.org/dist/latest-v8.x/docs/api/https.html#https_https_createserver_options_requestlistener) can be added to the https object, except `secureProtocol` and `secureOptions` which are set to recommended values. |
33+
| `http2` | Optional object. If present, an HTTP/2 server is started. You may start multiple HTTP/2 servers by passing an array of objects |
34+
| `http2.allowHTTP1` | Enable [ALPN negotiation] allowing support for both HTTPS and HTTP/2 on the same socket. |
35+
| `http2.*` | The same `https` security options are allowed, as well as any other properties supported by [http2.createSecureServer](https://nodejs.org/dist/latest-v8.x/docs/api/http2.html#http2_http2_createsecureserver_options_onrequesthandler). |
3336

3437
If successful, the `create-servers` callback is passed an object with the
3538
following properties:
@@ -286,3 +289,4 @@ var servers = createServers(
286289
[article]: https://certsimple.com/blog/a-plus-node-js-ssl
287290
[iojs]: https://github.com/iojs/io.js
288291
[ciphers]: https://iojs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener
292+
[ALPN negotiation]: https://nodejs.org/api/http2.html#http2_alpn_negotiation

index.js

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
*/
99

1010
var fs = require('fs'),
11-
http = require('http'),
12-
https = require('https'),
1311
tls = require('tls'),
1412
path = require('path'),
1513
constants = require('constants'),
@@ -51,23 +49,29 @@ module.exports = async function createServers(options, listening) {
5149
return listening(err);
5250
}
5351

54-
const [[httpErr, http], [httpsErr, https]] = await Promise.all([
52+
const [
53+
[httpErr, http],
54+
[httpsErr, https],
55+
[http2Err, http2]] = await Promise.all([
5556
createHttp(options.http, options.log),
56-
createHttps(options.https, options.log)
57+
createHttps(options.https, options.log),
58+
createHttps(options.http2, options.log, true)
5759
]);
5860

5961
const servers = {};
6062
if (http) servers.http = http;
6163
if (https) servers.https = https;
64+
if (http2) servers.http2 = http2;
6265

63-
if (httpErr || httpsErr) {
64-
let errorSource = httpsErr || httpErr;
66+
if (httpErr || httpsErr || http2Err) {
67+
let errorSource = http2Err || httpsErr || httpErr;
6568
if (Array.isArray(errorSource)) {
6669
errorSource = errorSource[0];
6770
}
6871
return listening(
6972
errs.create({
7073
message: errorSource && errorSource.message,
74+
http2: http2Err,
7175
https: httpsErr,
7276
http: httpErr
7377
}),
@@ -81,14 +85,16 @@ module.exports = async function createServers(options, listening) {
8185
function normalizeOptions(options) {
8286
const http = normalizeHttpOptions(options.http, options);
8387
const https = normalizeHttpsOptions(options.https, options);
88+
const http2 = normalizeHttpsOptions(options.http2, options);
8489

85-
if (!http && !https) {
86-
throw new Error('http and/or https are required options');
90+
if (!http && !https && !http2) {
91+
throw new Error('http, https, and/or http2 are required options');
8792
}
8893

8994
return {
9095
http,
9196
https,
97+
http2,
9298
log: options.log || function() {}
9399
};
94100
}
@@ -284,7 +290,7 @@ async function createHttp(httpConfig, log) {
284290
}
285291

286292
return await new Promise(resolve => {
287-
var server = http.createServer(httpConfig.handler),
293+
var server = require('http').createServer(httpConfig.handler),
288294
timeout = httpConfig.timeout,
289295
port = httpConfig.port,
290296
args;
@@ -308,14 +314,14 @@ async function createHttp(httpConfig, log) {
308314
// ### function createHttps ()
309315
// Attempts to create and listen on the HTTPS server.
310316
//
311-
async function createHttps(ssl, log) {
317+
async function createHttps(ssl, log, h2) {
312318
if (typeof ssl === 'undefined') {
313319
log('https | no options.https; no server');
314320
return [null, null];
315321
}
316322

317323
if (Array.isArray(ssl)) {
318-
return await createMultiple(createHttps, ssl, log);
324+
return await createMultiple(createHttps, ssl, log, h2);
319325
}
320326

321327
return await new Promise(resolve => {
@@ -350,7 +356,11 @@ async function createHttps(ssl, log) {
350356
}
351357

352358
log('https | listening on %d', port);
353-
server = https.createServer(finalHttpsOptions, ssl.handler);
359+
if(h2) {
360+
server = require('http2').createSecureServer(finalHttpsOptions, ssl.handler)
361+
} else {
362+
server = require('https').createServer(finalHttpsOptions, ssl.handler);
363+
}
354364

355365
if (typeof timeout === 'number') server.setTimeout(timeout);
356366
args = [server, port];

test/create-servers-test.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ var path = require('path'),
1010
url = require('url'),
1111
http = require('http'),
1212
https = require('https'),
13+
http2 = require('http2'),
1314
{ promisify } = require('util'),
1415
test = require('tape'),
1516
sinon = require('sinon'),
@@ -18,6 +19,8 @@ var path = require('path'),
1819

1920
const createServersAsync = promisify(createServers);
2021

22+
const { HTTP2_HEADER_PATH } = http2.constants;
23+
2124
const ca = fs.readFileSync(path.join(__dirname, './fixtures/example-ca-cert.pem'));
2225

2326
//
@@ -50,6 +53,74 @@ async function download(httpsURL) {
5053
});
5154
}
5255

56+
//
57+
// Request and download response from a URL using HTTP/2
58+
//
59+
async function download2(httpsURL) {
60+
return new Promise((resolve, reject) => {
61+
const clientSession = http2.connect(httpsURL);
62+
const fail = results => {
63+
clientSession.close();
64+
reject(results);
65+
};
66+
67+
const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' });
68+
req.on('response', () => {
69+
const chunks = [];
70+
req
71+
.on('data', chunk => chunks.push(chunk))
72+
.once('end', () => {
73+
resolve(chunks.map(chunk => chunk.toString('utf8')).join(''));
74+
clientSession.close();
75+
});
76+
})
77+
.once('aborted', fail)
78+
.once('close', fail)
79+
.once('error', fail);
80+
});
81+
}
82+
83+
/**
84+
* Helper to start a server with HTTP/2 support.
85+
* Returns stop function to handle server and dns cleanup after tests.
86+
*
87+
* Tests requests can be made to foo.example.org:3456
88+
*
89+
* @param {object} options - Additional http2 server options.
90+
* @returns {Promise<(function(): void)|*>} callback
91+
*/
92+
async function startHttp2Server(options = {}) {
93+
// disabling to avoid UNABLE_TO_VERIFY_LEAF_SIGNATURE for tests
94+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
95+
96+
const servers = await createServersAsync({
97+
http2: {
98+
port: 3456,
99+
root: path.join(__dirname, 'fixtures'),
100+
key: 'example-org-key.pem',
101+
cert: 'example-org-cert.pem',
102+
...options
103+
},
104+
handler: (req, res) => {
105+
const { httpVersion } = req;
106+
const { socket: { alpnProtocol } } = httpVersion === '2.0' ? req.stream.session : req;
107+
res.writeHead(200, { 'content-type': 'application/json' });
108+
res.end(JSON.stringify({
109+
alpnProtocol,
110+
httpVersion
111+
}));
112+
}
113+
});
114+
115+
evilDNS.add('foo.example.org', '0.0.0.0');
116+
117+
return function stop() {
118+
servers && servers.http2 && servers.http2.close();
119+
evilDNS.clear();
120+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
121+
};
122+
}
123+
53124
test('only http', function (t) {
54125
t.plan(5);
55126
createServers({
@@ -145,6 +216,28 @@ test('only https', function (t) {
145216
});
146217
});
147218

219+
test('only http2', function (t) {
220+
t.plan(4);
221+
var time = 4000000;
222+
createServers({
223+
log: console.log,
224+
http2: {
225+
timeout: time,
226+
port: 3456,
227+
root: path.join(__dirname, 'fixtures'),
228+
key: 'example-org-key.pem',
229+
cert: 'example-org-cert.pem'
230+
},
231+
handler: fend
232+
}, function (err, servers) {
233+
t.error(err);
234+
t.equals(typeof servers, 'object');
235+
t.equals(typeof servers.http2, 'object');
236+
t.equals(servers.http2.timeout, time);
237+
servers.http2.close();
238+
});
239+
});
240+
148241
test('absolute cert path resolution', function (t) {
149242
t.plan(3);
150243
createServers({
@@ -445,6 +538,50 @@ test('multiple https servers', async function (t) {
445538
}
446539
});
447540

541+
test('supports http2-only requests', async function (t) {
542+
t.plan(2);
543+
const url = 'https://foo.example.org:3456/';
544+
545+
let stopServer;
546+
try {
547+
stopServer = await startHttp2Server({});
548+
549+
const httpsResponse = await download(url);
550+
t.ok(httpsResponse.includes('Unknown ALPN Protocol'));
551+
552+
const response = JSON.parse(await download2(url));
553+
t.equals(response.httpVersion, '2.0');
554+
555+
} catch (err) {
556+
t.error(err);
557+
} finally {
558+
stopServer && stopServer();
559+
}
560+
});
561+
562+
test('supports http2 and https requests', async function (t) {
563+
t.plan(2);
564+
const url = 'https://foo.example.org:3456/';
565+
566+
let stopServer;
567+
try {
568+
stopServer = await startHttp2Server({
569+
allowHTTP1: true
570+
});
571+
572+
const httpsResponse = JSON.parse(await download(url));
573+
t.equals(httpsResponse.httpVersion, '1.1');
574+
575+
const response = JSON.parse(await download2(url));
576+
t.equals(response.httpVersion, '2.0');
577+
578+
} catch (err) {
579+
t.error(err);
580+
} finally {
581+
stopServer && stopServer();
582+
}
583+
});
584+
448585
async function testSni(t, sniConfig, hostNames) {
449586
t.plan(1);
450587

0 commit comments

Comments
 (0)