diff --git a/src/HMRServer.js b/src/HMRServer.js index 0a7e9825207..901c05682cf 100644 --- a/src/HMRServer.js +++ b/src/HMRServer.js @@ -3,14 +3,20 @@ const https = require('https'); const WebSocket = require('ws'); const prettyError = require('./utils/prettyError'); const generateCertificate = require('./utils/generateCertificate'); +const getCertificate = require('./utils/getCertificate'); const logger = require('./Logger'); class HMRServer { async start(options = {}) { - await new Promise(resolve => { - let server = options.https - ? https.createServer(generateCertificate(options)) - : http.createServer(); + await new Promise(async resolve => { + let server; + if (!options.https) { + server = http.createServer(); + } else if (typeof options.https === 'boolean') { + server = https.createServer(generateCertificate(options)); + } else { + server = https.createServer(await getCertificate(options.https)); + } this.wss = new WebSocket.Server({server}); server.listen(options.hmrPort, resolve); diff --git a/src/Server.js b/src/Server.js index 193cad8b88b..b6230a78421 100644 --- a/src/Server.js +++ b/src/Server.js @@ -4,6 +4,7 @@ const serveStatic = require('serve-static'); const getPort = require('get-port'); const serverErrors = require('./utils/customErrors').serverErrors; const generateCertificate = require('./utils/generateCertificate'); +const getCertificate = require('./utils/getCertificate'); const logger = require('./Logger'); serveStatic.mime.define({ @@ -82,9 +83,14 @@ function middleware(bundler) { async function serve(bundler, port, useHTTPS = false) { let handler = middleware(bundler); - let server = useHTTPS - ? https.createServer(generateCertificate(bundler.options), handler) - : http.createServer(handler); + let server; + if (!useHTTPS) { + server = http.createServer(handler); + } else if (typeof useHTTPS === 'boolean') { + server = https.createServer(generateCertificate(bundler.options), handler); + } else { + server = https.createServer(await getCertificate(useHTTPS), handler); + } let freePort = await getPort({port}); server.listen(freePort); diff --git a/src/cli.js b/src/cli.js index a90f57895b2..2dcee79d977 100755 --- a/src/cli.js +++ b/src/cli.js @@ -23,6 +23,8 @@ program 'set the hostname of HMR websockets, defaults to location.hostname of current window' ) .option('--https', 'serves files over HTTPS') + .option('--cert ', 'path to certificate to use with HTTPS') + .option('--key ', 'path to private key to use with HTTPS') .option('--open', 'automatically open in default browser') .option( '-d, --out-dir ', @@ -148,6 +150,13 @@ async function bundle(main, command) { process.env.NODE_ENV = process.env.NODE_ENV || 'development'; } + if (command.cert && command.key) { + command.https = { + cert: command.cert, + key: command.key + }; + } + const bundler = new Bundler(main, command); if (command.name() === 'serve') { diff --git a/src/utils/getCertificate.js b/src/utils/getCertificate.js new file mode 100644 index 00000000000..43d3afec6b2 --- /dev/null +++ b/src/utils/getCertificate.js @@ -0,0 +1,13 @@ +const fs = require('./fs'); + +async function getCertificate(options) { + try { + let cert = await fs.readFile(options.cert); + let key = await fs.readFile(options.key); + return {key, cert}; + } catch (err) { + throw new Error('Certificate and/or key not found'); + } +} + +module.exports = getCertificate; diff --git a/test/getCertificate.js b/test/getCertificate.js new file mode 100644 index 00000000000..841e4be378c --- /dev/null +++ b/test/getCertificate.js @@ -0,0 +1,25 @@ +const assert = require('assert'); +const path = require('path'); +const fs = require('../src/utils/fs'); +const getCertificate = require('../src/utils/getCertificate'); + +const https = { + key: path.join(__dirname, '/integration/https', 'private.pem'), + cert: path.join(__dirname, '/integration/https', 'primary.crt') +}; + +describe('getCertificate', () => { + it('should support custom certificate', async () => { + const key = await fs.readFile( + path.join(__dirname, '/integration/https', 'private.pem') + ); + const cert = await fs.readFile( + path.join(__dirname, '/integration/https', 'primary.crt') + ); + + const retrieved = await getCertificate(https); + + assert.equal(retrieved.cert.toString(), cert.toString()); + assert.equal(retrieved.key.toString(), key.toString()); + }); +}); diff --git a/test/hmr.js b/test/hmr.js index f8dbdb9ab3f..d4255826738 100644 --- a/test/hmr.js +++ b/test/hmr.js @@ -360,4 +360,67 @@ describe('hmr', function() { assert(logs[0].trim().startsWith('[parcel] 🚨')); assert(logs[1].trim().startsWith('[parcel] ✨')); }); + + it('should make a secure connection', async function() { + await ncp(__dirname + '/integration/commonjs', __dirname + '/input'); + + b = bundler(__dirname + '/input/index.js', { + watch: true, + hmr: true, + https: true + }); + await b.bundle(); + + ws = new WebSocket('wss://localhost:' + b.options.hmrPort, { + rejectUnauthorized: false + }); + + const buildEnd = nextEvent(b, 'buildEnd'); + + fs.writeFileSync( + __dirname + '/input/local.js', + 'exports.a = 5;\nexports.b = 5;' + ); + + let msg = json5.parse(await nextEvent(ws, 'message')); + assert.equal(msg.type, 'update'); + assert.equal(msg.assets.length, 1); + assert.equal(msg.assets[0].generated.js, 'exports.a = 5;\nexports.b = 5;'); + assert.deepEqual(msg.assets[0].deps, {}); + + await buildEnd; + }); + + it('should make a secure connection with custom certificate', async function() { + await ncp(__dirname + '/integration/commonjs', __dirname + '/input'); + + b = bundler(__dirname + '/input/index.js', { + watch: true, + hmr: true, + https: { + key: __dirname + '/integration/https/private.pem', + cert: __dirname + '/integration/https/primary.crt' + } + }); + await b.bundle(); + + ws = new WebSocket('wss://localhost:' + b.options.hmrPort, { + rejectUnauthorized: false + }); + + const buildEnd = nextEvent(b, 'buildEnd'); + + fs.writeFileSync( + __dirname + '/input/local.js', + 'exports.a = 5;\nexports.b = 5;' + ); + + let msg = json5.parse(await nextEvent(ws, 'message')); + assert.equal(msg.type, 'update'); + assert.equal(msg.assets.length, 1); + assert.equal(msg.assets[0].generated.js, 'exports.a = 5;\nexports.b = 5;'); + assert.deepEqual(msg.assets[0].deps, {}); + + await buildEnd; + }); }); diff --git a/test/integration/https/primary.crt b/test/integration/https/primary.crt index 35e1a321040..266d735041b 100644 --- a/test/integration/https/primary.crt +++ b/test/integration/https/primary.crt @@ -1 +1,29 @@ -hello cert +-----BEGIN CERTIFICATE----- +MIIE/zCCAuegAwIBAgIJAITHmIqRap3uMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV +BAMMC2V4YW1wbGUuY29tMB4XDTE4MDMwODE5NDIxOFoXDTI4MDMwNTE5NDIxOFow +FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDqi17CnmpA5jfFqcvfB5OZz59lRQpNO098OOeUIbQB/pP868Y5qmL/ +ylCQmj3liIdul6oflNFPmu23numpUTHOSwSxfY/QZwzsYytnbQG6gr1DhrvNDzeN +Arf1cNJ9CqfJdEmNyikfXGOJCk1xewCWYcZ34kugo2kuLGO83ft3Q5J47iqvgTPX +PbTc4qfUgXavprVSUzpWOGbbK2GHeenTGCarsfu7YkP3VVjMpeKcxS94Bnd0wHZF +PJKuTNDMrANBP/7RKh4XBgeuRPAdUjbcHWnlMWBvj0YuIL/Yhw976tIWAlyp0ZR6 +HKF46QOEnPrR5sQGIC7wM6qyB3xCVX7eMEKcV97YYtD52eskBHT8knQhNfYn6OI4 +zin13++en3Kg8I/lDA3uf/Zr2Uj2gwu8v4RqINOQoWrgAHWeActi+5FB9xwebnwX +yax+7eoCPtfe1zDo0GlzNbDKFnd3JfMC4e4gvwnAMnS1Wg5bBNXZJy8ggpniY04b +34HBWBOonscaQLMWrZQpNDfHoszUzGla0nKjru3Cwk8MJqz9qlruPJN0stdqY2C/ +3Q2rB1YgvMFTr43C+KALk7hQ4w1ShJ0KYde1UPgWKooYGJRMKtpSuwu4QcgsxPIB +fH1aXEuHefxQ5EwhZm8FW79Psit+oPMvrBw1esLQnBQA3JhDG2tG3wIDAQABo1Aw +TjAdBgNVHQ4EFgQUkO1iAxx9YovR6zdzp9WTigA0OX4wHwYDVR0jBBgwFoAUkO1i +Axx9YovR6zdzp9WTigA0OX4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEAGk378ptJZMSmxgypj8z2pldHfbRURDL+rI+5opf7JBeVQkyqqDWkdSM1dCMy +mN7QqwCQbQFxKiKM+wbZfS8/JO5Pj0kQwuiaXVx7gPDezXT4aDw+TFH/K3ZNI2oB +S4JWCix5rHpwE6Xdnady0IYloT08t9lMdME0728B7j+XcJgH5exzLIwWdzs9JWYj +hip87tx6ORw1s74HCmcnMshr7Ow1J8DTmWcL5+MLPokFm14jGWiW826odzucU3jf +bh1T8+DbM+ZkxXN0BTh6jFx0qmYg8wyVSKqVSncAo1hdsb+/aA+BpU0eofWInyzs +45c3/wIE2LVC3EsoHAlT3O6ZVtI2va48E4oDMJSf7IinjGZ+VJzn46gXvmZl6KDQ +Get1dsrxxlPqwOLM/WgHkvGBwfw9OyK3igdYtudsT5OcW4Parok9s1ea4szDteHF +5opBcQG91i9/fAeCMSgjAZx0HMlFQi7OxsappUgc0d77JeR04hiVPxOxttu8LuKw +GgmuoZ0HgCso9CaPlOQXfQxOFljcdQkUxrIf/7HOo/D7Lzhc2RIgcDQiZk5o8chv +JX+QSmpQTRl//iBDNlNcUy5WJtSjpPOxvKryVypTw+Xwkbb4HgAmNY5x87K7WucB +j5hfjsLAwrciIsIcKu5qLzheU6vRhPwZE5vW3lWZnf6D/s4= +-----END CERTIFICATE----- diff --git a/test/integration/https/private.pem b/test/integration/https/private.pem index df0fc673871..1e636462d23 100644 --- a/test/integration/https/private.pem +++ b/test/integration/https/private.pem @@ -1 +1,52 @@ -hello private key +-----BEGIN PRIVATE KEY----- +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 +I0ZrfUn75MYvsljzUOiaIQRgK9m1/74= +-----END PRIVATE KEY----- diff --git a/test/server.js b/test/server.js index e62883b7b5e..2ba1ba4c12e 100644 --- a/test/server.js +++ b/test/server.js @@ -95,4 +95,15 @@ describe('server', function() { let data = await get('/dist/index.js', https); assert.equal(data, fs.readFileSync(__dirname + '/dist/index.js', 'utf8')); }); + + it('should support HTTPS via custom certificate', async function() { + let b = bundler(__dirname + '/integration/commonjs/index.js'); + server = await b.serve(0, { + key: __dirname + '/integration/https/private.pem', + cert: __dirname + '/integration/https/primary.crt' + }); + + let data = await get('/dist/index.js', https); + assert.equal(data, fs.readFileSync(__dirname + '/dist/index.js', 'utf8')); + }); });