Skip to content

Commit

Permalink
feat: http2 option to enable/disable HTTP/2 with HTTPS (#1721)
Browse files Browse the repository at this point in the history
  • Loading branch information
knagaitsev authored and evilebottnawi committed Apr 4, 2019
1 parent 0984d4b commit dcd2434
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 3 deletions.
5 changes: 5 additions & 0 deletions bin/options.js
Expand Up @@ -87,6 +87,11 @@ const options = {
group: SSL_GROUP,
describe: 'HTTPS',
},
http2: {
type: 'boolean',
group: SSL_GROUP,
describe: 'HTTP/2, must be used with HTTPS',
},
key: {
type: 'string',
describe: 'Path to a SSL key.',
Expand Down
33 changes: 30 additions & 3 deletions lib/Server.js
Expand Up @@ -89,13 +89,19 @@ class Server {
if (options.lazy && !options.filename) {
throw new Error("'filename' option must be set in lazy mode.");
}


// if the user enables http2, we can safely enable https
if (options.http2 && !options.https) {
options.https = true;
}

updateCompiler(compiler, options);

this.stats =
options.stats && Object.keys(options.stats).length
? options.stats
: Server.DEFAULT_STATS;

this.hot = options.hot || options.hotOnly;
this.headers = options.headers;
this.progress = options.progress;
Expand Down Expand Up @@ -647,7 +653,21 @@ class Server {
options.https.key = options.https.key || fakeCert;
options.https.cert = options.https.cert || fakeCert;

if (!options.https.spdy) {
// Only prevent HTTP/2 if http2 is explicitly set to false
const isHttp2 = options.http2 !== false;

// note that options.spdy never existed. The user was able
// to set options.https.spdy before, though it was not in the
// docs. Keep options.https.spdy if the user sets it for
// backwards compatability, but log a deprecation warning.
if (options.https.spdy) {
// for backwards compatability: if options.https.spdy was passed in before,
// it was not altered in any way
this.log.warn(
'Providing custom spdy server options is deprecated and will be removed in the next major version.'
);
} else {
// if the normal https server gets this option, it will not affect it.
options.https.spdy = {
protocols: ['h2', 'http/1.1'],
};
Expand All @@ -662,7 +682,14 @@ class Server {
// - https://github.com/nodejs/node/issues/21665
// - https://github.com/webpack/webpack-dev-server/issues/1449
// - https://github.com/expressjs/express/issues/3388
if (semver.gte(process.version, '10.0.0')) {
if (semver.gte(process.version, '10.0.0') || !isHttp2) {
if (options.http2) {
// the user explicitly requested http2 but is not getting it because
// of the node version.
this.log.warn(
'HTTP/2 is currently unsupported for Node 10.0.0 and above, but will be supported once Express supports it'
);
}
this.listeningApp = https.createServer(options.https, app);
} else {
/* eslint-disable global-require */
Expand Down
4 changes: 4 additions & 0 deletions lib/options.json
Expand Up @@ -162,6 +162,9 @@
}
]
},
"http2": {
"type": "boolean"
},
"contentBase": {
"anyOf": [
{
Expand Down Expand Up @@ -321,6 +324,7 @@
"disableHostCheck": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-disablehostcheck)",
"public": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserver-public)",
"https": "should be {Object|Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-https)",
"http2": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-http2)",
"contentBase": "should be {Array} (https://webpack.js.org/configuration/dev-server/#devserver-contentbase)",
"watchContentBase": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-watchcontentbase)",
"open": "should be {String|Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-open)",
Expand Down
4 changes: 4 additions & 0 deletions lib/utils/createConfig.js
Expand Up @@ -136,6 +136,10 @@ function createConfig(config, argv, { port }) {
options.https = true;
}

if (argv.http2) {
options.http2 = true;
}

if (argv.key) {
options.key = argv.key;
}
Expand Down
22 changes: 22 additions & 0 deletions test/CreateConfig.test.js
Expand Up @@ -560,6 +560,28 @@ describe('createConfig', () => {
expect(config).toMatchSnapshot();
});

it('http2 option', () => {
const config = createConfig(
webpackConfig,
Object.assign({}, argv, { https: true, http2: true }),
{ port: 8080 }
);

expect(config).toMatchSnapshot();
});

it('http2 option (in devServer config)', () => {
const config = createConfig(
Object.assign({}, webpackConfig, {
devServer: { https: true, http2: true },
}),
argv,
{ port: 8080 }
);

expect(config).toMatchSnapshot();
});

it('key option', () => {
const config = createConfig(
webpackConfig,
Expand Down
120 changes: 120 additions & 0 deletions test/Http2.test.js
@@ -0,0 +1,120 @@
'use strict';

const path = require('path');
const request = require('supertest');
const semver = require('semver');
const helper = require('./helper');
const config = require('./fixtures/contentbase-config/webpack.config');

const contentBasePublic = path.join(
__dirname,
'fixtures/contentbase-config/public'
);

describe('http2', () => {
let server;
let req;

// HTTP/2 will only work with node versions below 10.0.0
// since spdy is broken past that point, and this test will only
// work above Node 8.8.0, since it is the first version where the
// built-in http2 module is exposed without need for a flag
// (https://nodejs.org/en/blog/release/v8.8.0/)
// if someone is testing below this Node version and breaks this,
// their tests will not catch it, but CI will catch it.
if (
semver.gte(process.version, '8.8.0') &&
semver.lt(process.version, '10.0.0')
) {
/* eslint-disable global-require */
const http2 = require('http2');
/* eslint-enable global-require */
describe('http2 works with https', () => {
beforeAll((done) => {
server = helper.start(
config,
{
contentBase: contentBasePublic,
https: true,
http2: true,
},
done
);
req = request(server.app);
});

it('confirm http2 client can connect', (done) => {
const client = http2.connect('https://localhost:8080', {
rejectUnauthorized: false,
});
client.on('error', (err) => console.error(err));

const http2Req = client.request({ ':path': '/' });

http2Req.on('response', (headers) => {
expect(headers[':status']).toEqual(200);
});

http2Req.setEncoding('utf8');
let data = '';
http2Req.on('data', (chunk) => {
data += chunk;
});
http2Req.on('end', () => {
expect(data).toEqual(expect.stringMatching(/Heyo/));
done();
});
http2Req.end();
});

afterAll(helper.close);
});
}

describe('server works with http2 option, but without https option', () => {
beforeAll((done) => {
server = helper.start(
config,
{
contentBase: contentBasePublic,
http2: true,
},
done
);
req = request(server.app);
});

it('Request to index', (done) => {
req.get('/').expect(200, /Heyo/, done);
});

afterAll(helper.close);
});

describe('https without http2 disables HTTP/2', () => {
beforeAll((done) => {
server = helper.start(
config,
{
contentBase: contentBasePublic,
https: true,
http2: false,
},
done
);
req = request(server.app);
});

it('Request to index', (done) => {
req
.get('/')
.expect(200, /Heyo/)
.then(({ res }) => {
expect(res.httpVersion).not.toEqual('2.0');
done();
});
});

afterAll(helper.close);
});
});
32 changes: 32 additions & 0 deletions test/__snapshots__/CreateConfig.test.js.snap
Expand Up @@ -474,6 +474,38 @@ Object {
}
`;

exports[`createConfig http2 option (in devServer config) 1`] = `
Object {
"hot": true,
"hotOnly": false,
"http2": true,
"https": true,
"noInfo": true,
"port": 8080,
"publicPath": "/",
"stats": Object {
"cached": false,
"cachedAssets": false,
},
}
`;

exports[`createConfig http2 option 1`] = `
Object {
"hot": true,
"hotOnly": false,
"http2": true,
"https": true,
"noInfo": true,
"port": 8080,
"publicPath": "/",
"stats": Object {
"cached": false,
"cachedAssets": false,
},
}
`;

exports[`createConfig https option (in devServer config) 1`] = `
Object {
"hot": true,
Expand Down

0 comments on commit dcd2434

Please sign in to comment.