Skip to content

Commit

Permalink
Add resolveALPN for HTTP options
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak committed Dec 24, 2018
1 parent 4cdfacb commit 49ae213
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 22 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -158,6 +158,10 @@ Type: `object`

Type: `Function`

### http2.auto.resolveALPN(options)

Resolves ALPN using HTTP options.

### http2.request(url, options, callback)

Same as [https.request](https://nodejs.org/api/https.html#https_https_request_options_callback).
Expand Down
19 changes: 10 additions & 9 deletions source/auto.js
@@ -1,7 +1,7 @@
'use strict';
const http = require('http');
const https = require('https');
const resolveALPN = require('resolve-alpn');
const httpResolveALPN = require('./utils/http-resolve-alpn');
const urlToOptions = require('./utils/url-to-options');
const HTTP2ClientRequest = require('./client-request');

Expand All @@ -12,19 +12,17 @@ module.exports = async (input, options) => {

options = {...input, ...options};
options.protocol = options.protocol || 'https:';
options.port = options.port || (options.protocol === 'http:' ? 80 : 443);

if (options.protocol === 'https:') {
const {alpnProtocol, socket} = await resolveALPN({
ALPNProtocols: ['h2', 'http/1.1'],
const {alpnProtocol, socket} = await httpResolveALPN({
...options,
host: options.hostname || options.host,
path: options.socketPath,
port: options.port || 443,
session: options.socketSession,
resolveSocket: true
resolveSocket: !options.createConnection
});

options.createConnection = () => socket;
if (socket) {
options.createConnection = () => socket;
}

if (alpnProtocol === 'h2') {
return new HTTP2ClientRequest(options);
Expand All @@ -33,5 +31,8 @@ module.exports = async (input, options) => {
options._defaultAgent = https.globalAgent;
}

options.session = options.socketSession;
return new http.ClientRequest(options);
};

module.exports.resolveALPN = httpResolveALPN;
6 changes: 3 additions & 3 deletions source/client-request.js
Expand Up @@ -55,7 +55,7 @@ class HTTP2ClientRequest extends Writable {

this[kErrorHandler] = error => this.emit('error', error);

// Don't confuse with the `net` module
// Don't confuse with the `net` module.
this.path = options.path || '/';
this.session = options.session;
options.path = options.socketPath;
Expand All @@ -71,7 +71,7 @@ class HTTP2ClientRequest extends Writable {
// Forwards errors to this instance.
this.session.once('error', this[kErrorHandler]);

// Emits response `close` event
// Emits response `close` event.
this.session.once('close', () => {
if (this.res) {
if (this.res.readable) {
Expand Down Expand Up @@ -174,7 +174,7 @@ class HTTP2ClientRequest extends Writable {
...this[kHeaders]
};

// Update the `method` property in case user changed it
// Update the `method` property in case user changed it.
this.method = headers[HTTP2_HEADER_METHOD].toUpperCase();
const isConnectMethod = this.method === HTTP2_METHOD_CONNECT;

Expand Down
22 changes: 22 additions & 0 deletions source/utils/calculate-server-name.js
@@ -0,0 +1,22 @@
'use strict';
/* istanbul ignore file: https://github.com/nodejs/node/blob/d4c91f28148af8a6c1a95392e5c88cb93d4b61c6/lib/_http_agent.js */

module.exports = (options, headers) => {
let servername = options.host;
const hostHeader = headers.host;

if (hostHeader) {
if (hostHeader.startsWith('[')) {
const index = hostHeader.indexOf(']');
if (index === -1) {
servername = hostHeader;
} else {
servername = hostHeader.substr(1, index - 1);
}
} else {
servername = hostHeader.split(':', 1)[0];
}
}

return servername;
};
16 changes: 16 additions & 0 deletions source/utils/http-resolve-alpn.js
@@ -0,0 +1,16 @@
'use strict';
const resolveALPN = require('resolve-alpn');
const calculateServerName = require('./calculate-server-name');

// Transforms HTTP options into Socket options and resolves ALPN.
module.exports = (options, headers) => {
return resolveALPN({
ALPNProtocols: ['h2', 'http/1.1'],
...options,
port: options.port || 443,
host: options.hostname || options.host,
path: options.socketPath,
session: options.socketSession,
servername: options.servername || calculateServerName(options, headers || {})
});
};
57 changes: 47 additions & 10 deletions test/auto.js
@@ -1,3 +1,4 @@
import tls from 'tls';
import http from 'http';
import util from 'util';
import test from 'ava';
Expand Down Expand Up @@ -91,16 +92,6 @@ test('accepts string as URL', async t => {
t.is(data, 'h2');
});

test('default port for `https:` protocol is 443', async t => {
const error = await t.throwsAsync(() => http2.auto({
protocol: 'https:',
hostname: 'localhost'
}));

t.is(error.address, '127.0.0.1');
t.is(error.port, 443);
});

test('the default protocol is `https:`', async t => {
const req = await http2.auto({
hostname: 'localhost',
Expand All @@ -113,6 +104,27 @@ test('the default protocol is `https:`', async t => {
t.is(data, 'h2');
});

test('default port for `http:` protocol is 80', async t => {
const req = await http2.auto({
protocol: 'http:',
hostname: 'localhost'
});

const error = await pEvent(req, 'error');
t.is(error.address, '127.0.0.1');
t.is(error.port, 80);
});

test('default port for `https:` protocol is 443', async t => {
const error = await t.throwsAsync(() => http2.auto({
protocol: 'https:',
hostname: 'localhost'
}));

t.is(error.address, '127.0.0.1');
t.is(error.port, 443);
});

test('passes https errors', async t => {
await t.throwsAsync(() => http2.auto(h2s.url, {
headers: {
Expand Down Expand Up @@ -141,3 +153,28 @@ test('`host` option is an alternative to `hostname` option', async t => {
const data = await getStream(res);
t.is(data, 'h2');
});

test('doesn\'t break when using `createConnection` option', async t => {
let called = false;

const req = await http2.auto({
protocol: 'https:',
hostname: 'localhost',
port: h2s.address().port,
createConnection: (authority, options) => {
called = true;

return tls.connect(h2s.address().port, 'localhost', {
...options,
allowHalfOpen: true,
ALPNProtocols: ['h2']
});
}
});
req.end();

const res = await pEvent(req, 'response');
const data = await getStream(res);
t.is(data, 'h2');
t.true(called);
});
14 changes: 14 additions & 0 deletions test/request.js
Expand Up @@ -427,6 +427,20 @@ test('destroy will close the session if not ours', t => {
session.close();
});

test('setNoDelay() doesn\'t throw', t => {
const req = request(s.options);
req.end();

t.notThrows(() => req.setNoDelay());
});

test('setSocketKeepAlive() doesn\'t throw', t => {
const req = request(s.options);
req.end();

t.notThrows(() => req.setSocketKeepAlive());
});

if (process.platform !== 'win32') {
const socketPath = tempy.file({extension: 'socket'});

Expand Down

0 comments on commit 49ae213

Please sign in to comment.