From b64915b4a17c64df48aba654bb4cd6b4002a4d8f Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Thu, 7 Mar 2024 15:56:24 -0800 Subject: [PATCH] Add TLS and hostname support This adds both TLS and hostname support. TLS support is activated by specifying paths to a key and a cert. There are caveats about self-signed certs in the README. This also adds hostname support. This impacts both the URL sent to browsers and the IP the server socket listens to. There are caveats about valid names, valid IPs, and HSTS preload in the README. HSTS-type errors in both Chrome and Firefox are detected automatically and translated into friendlier errors with a short link to the documentation. Closes #42 Replaces PR #43 One part of solving shaka-project/shaka-player#5547 --- README.md | 57 +++++++++++++++++++++++++++++ bin/jasmine-browser-runner | 10 ++--- index.js | 12 +++--- lib/command.js | 3 ++ lib/runner.js | 26 +++++++++++-- lib/server.js | 75 +++++++++++++++++++++++++++++++++----- lib/types.js | 40 ++++++++++++++++++++ spec/fixtures/tls-cert.pem | 38 +++++++++++++++++++ spec/fixtures/tls-key.pem | 56 ++++++++++++++++++++++++++++ spec/indexSpec.js | 29 +++++++++++++-- spec/serverSpec.js | 75 +++++++++++++++++++++++++++++++++++--- 11 files changed, 389 insertions(+), 32 deletions(-) create mode 100644 spec/fixtures/tls-cert.pem create mode 100644 spec/fixtures/tls-key.pem diff --git a/README.md b/README.md index 2ca6ff3..289fb8e 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,63 @@ To use a browser other than Firefox, add a `browser` field to Its value can be `"firefox"`, `"headlessFirefox"`, `"safari"`, `"MicrosoftEdge"`, `"chrome"`, or `"headlessChrome"`. +## TLS support + +To serve tests over HTTPS instead of HTTP, supply a path to a TLS cert and key +in PEM format in `jasmine-browser.json`: + +```javascript +{ + // ... + "tlsKey": "/path/to/tlsKey.pem", + "tlsCert": "/path/to/tlsCert.pem", + // ... +} +``` + +These can also be specified on the command line with `--tlsKey` and `--tlsCert`. + +Note that if you are using a self-signed or otherwise invalid certificate, the +browser will not allow the connection by default. Additional browser configs +or command line options may be necessary to use an invalid TLS certificate. + +## Hostname support + +To serve tests on a specific interface or IP, you can specify a hostname in +`jasmine-browser.json`: + +```javascript +{ + // ... + "hostname": "mymachine.mynetwork", + // ... +} +``` + +This can also be specified on the command line with `--hostname`. + +There are a few important caveats when doing this: + +1. This name must either be an IP or a name that can really be resolved on your + system. Otherwise, you will get `ENOTFOUND` errors. +2. This name must correspond to an IP assigned to one of the network interfaces + on your system. Otherwise, you will get `EADDRNOTAVAIL` errors. +3. If this name matches the [HSTS preload list](https://hstspreload.org/), + browsers will force the connection to HTTPS. If you are not using TLS, you + will get an error that says `The browser tried to speak HTTPS to an HTTP + server. Misconfiguration is likely.` You may be surprised by the names on + that preload list, which include such favorite local network hostnames as: + - dev + - foo + - app + - nexus + - windows + - office + - dad + You can see a full list in [Chromium source](https://raw.githubusercontent.com/chromium/chromium/main/net/http/transport_security_state_static.json) + or query your hostname at the [HSTS preload site](https://hstspreload.org/). + + ## ES module support If a source, spec, or helper file's name ends in `.mjs`, it will be loaded as diff --git a/bin/jasmine-browser-runner b/bin/jasmine-browser-runner index baf0c22..672f995 100755 --- a/bin/jasmine-browser-runner +++ b/bin/jasmine-browser-runner @@ -1,12 +1,12 @@ #!/usr/bin/env node - const path = require('path'), - jasmineCore = require('../lib/jasmineCore'), - Command = require('../lib/command'), - jasmineBrowser = require('../index.js'); +const path = require('path'); +const jasmineCore = require('../lib/jasmineCore'); +const Command = require('../lib/command'); +const jasmineBrowser = require('../index.js'); const UsageError = require('../lib/usage_error'); - const command = new Command({ +const command = new Command({ baseDir: path.resolve(), jasmineCore, jasmineBrowser, diff --git a/index.js b/index.js index dd5e48b..b0d21d1 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,8 @@ -const ConsoleReporter = require('./lib/console_reporter'), - webdriverModule = require('./lib/webdriver'), - Server = require('./lib/server'), - Runner = require('./lib/runner'), - ModuleLoader = require('./lib/moduleLoader'); +const ConsoleReporter = require('./lib/console_reporter'); +const webdriverModule = require('./lib/webdriver'); +const Server = require('./lib/server'); +const Runner = require('./lib/runner'); +const ModuleLoader = require('./lib/moduleLoader'); async function createReporters(options, deps) { const result = []; @@ -98,7 +98,7 @@ module.exports = { const webdriver = buildWebdriver(options.browser); try { - const host = `http://localhost:${server.port()}`; + const host = `${server.scheme()}://${server.hostname()}:${server.port()}`; const runner = new RunnerClass({ webdriver, reporters, host }); console.log('Running tests in the browser...'); diff --git a/lib/command.js b/lib/command.js index 5aba5e9..db5b86f 100644 --- a/lib/command.js +++ b/lib/command.js @@ -11,6 +11,9 @@ const UsageError = require('./usage_error'); const commonOptions = [ { name: 'config', type: 'string', description: 'path to the config file' }, { name: 'port', type: 'number', description: 'port to run the server on' }, + { name: 'tlsCert', type: 'string', description: 'TLS cert for https' }, + { name: 'tlsKey', type: 'string', description: 'TLS key for https' }, + { name: 'hostname', type: 'string', description: 'hostname to listen on' }, ]; const subCommands = [ diff --git a/lib/runner.js b/lib/runner.js index cf4f385..e6083b0 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -100,9 +100,29 @@ class Runner { async run(runOptions) { runOptions = runOptions || {}; - await this._options.webdriver.get( - this._options.host + urlParams(runOptions) - ); + + try { + await this._options.webdriver.get( + this._options.host + urlParams(runOptions) + ); + } catch (error) { + // Looking for Chrome's "WebDriverError: ... net::ERR_SSL_PROTOCOL_ERROR" + // or Firefox's "WebDriverError: ... about:neterror?e=nssFailure2" + if (error.name == 'WebDriverError') { + if ( + error.message.includes('ERR_SSL_PROTOCOL_ERROR') || + error.message.includes('about:neterror?e=nssFailure2') + ) { + // Show a friendlier error. + throw new Error( + 'The browser tried to speak HTTPS to an HTTP server. Misconfiguration is likely. See https://tinyurl.com/y46m83cc for details.' + ); + } + } + + // Rethrow the original error. + throw error; + } return await runTillEnd(this._options.webdriver, this._options.reporters); } diff --git a/lib/server.js b/lib/server.js index 9d3ea55..ec4f8ba 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,8 +1,9 @@ -const defaultExpress = require('express'), - glob = require('glob'), - ejs = require('ejs'), - path = require('path'), - fs = require('fs'); +const defaultExpress = require('express'); +const ejs = require('ejs'); +const fs = require('fs'); +const glob = require('glob'); +const https = require('https'); +const path = require('path'); /** * @class Server @@ -210,12 +211,30 @@ class Server { }); const port = findPort(serverOptions.port, this.options.port); + const tlsCert = serverOptions.tlsCert || this.options.tlsCert; + const tlsKey = serverOptions.tlsKey || this.options.tlsKey; + const hostname = serverOptions.hostname || this.options.hostname; + + // NOTE: Before hostname support, jasmine-browser-runner would listen on + // all IPs (0.0.0.0) and point browsers to "localhost". We preserve + // backward compatibility here by using different defaults for these two + // things. + const listenOptions = { + port, + host: hostname || '0.0.0.0', + }; + this._httpHostname = hostname || 'localhost'; + return new Promise(resolve => { - this._httpServer = app.listen(port, () => { + const callback = () => { const runningPort = this._httpServer.address().port; - console.log( - `Jasmine server is running here: http://localhost:${runningPort}` - ); + const url = + this._httpServerScheme + + '://' + + this._httpHostname + + ':' + + runningPort; + console.log(`Jasmine server is running here: ${url}`); console.log( `Jasmine tests are here: ${path.resolve( self.options.specDir @@ -225,7 +244,21 @@ class Server { `Source files are here: ${path.resolve(self.options.srcDir)}` ); resolve(); - }); + }; + + if (tlsKey && tlsCert) { + const httpsOptions = { + key: fs.readFileSync(tlsKey), + cert: fs.readFileSync(tlsCert), + }; + this._httpServer = https + .createServer(httpsOptions, app) + .listen(listenOptions, callback); + this._httpServerScheme = 'https'; + } else { + this._httpServer = app.listen(listenOptions, callback); + this._httpServerScheme = 'http'; + } }); } @@ -257,6 +290,28 @@ class Server { return this._httpServer.address().port; } + + /** + * Gets the URL scheme that the server is listening on. The server must be + * started before this method is called. + * @function + * @name Server#scheme + * @return {string} The URL scheme ('http' or 'https') + */ + scheme() { + return this._httpServerScheme; + } + + /** + * Gets the hostname that the server is listening on. The server must be + * started before this method is called. + * @function + * @name Server#hostname + * @return {string} The hostname (localhost if not specified) + */ + hostname() { + return this._httpHostname; + } } function findPort(serverPort, optionsPort) { diff --git a/lib/types.js b/lib/types.js index 87c14f7..9ef35b7 100644 --- a/lib/types.js +++ b/lib/types.js @@ -39,6 +39,26 @@ * @name ServerCtorOptions#port * @type number | undefined */ +/** + * The path to a TLS key. Activates HTTPS mode. If specified, tlsCert must also + * be specified. + * @name ServerCtorOptions#tlsKey + * @type string + */ +/** + * The path to a TLS cert. Activates HTTPS mode. If specified, tlsKey must also + * be specified. + * @name ServerCtorOptions#tlsCert + * @type string + */ +/** + * The hostname to use. This influences both the URL given to browsers and the + * addresses on which the socket listens. If blank, for backward + * compatibility, the browsers will be pointed to localhost, but the listening + * socket will listen on all IPs. + * @name ServerCtorOptions#hostname + * @type string + */ /** * The root directory of the project. * @name ServerCtorOptions#projectBaseDir @@ -271,6 +291,26 @@ * @name ServerStartOptions#port * @type number | undefined */ +/** + * The path to a TLS key. Activates HTTPS mode. If specified, tlsCert must also + * be specified. + * @name ServerStartOptions#tlsKey + * @type string + */ +/** + * The path to a TLS cert. Activates HTTPS mode. If specified, tlsKey must also + * be specified. + * @name ServerStartOptions#tlsCert + * @type string + */ +/** + * The hostname to use. This influences both the URL given to browsers and the + * addresses on which the socket listens. If blank, for backward + * compatibility, the browsers will be pointed to localhost, but the listening + * socket will listen on all IPs. + * @name ServerStartOptions#hostname + * @type string + */ /** * Describes an import map. diff --git a/spec/fixtures/tls-cert.pem b/spec/fixtures/tls-cert.pem new file mode 100644 index 0000000..59c9a7b --- /dev/null +++ b/spec/fixtures/tls-cert.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIUMfxE4gzcNJOO5mAy63yoZjNJj+0wDQYJKoZIhvcNAQEL +BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM +CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu +eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y +NDAzMDcyMzEyMjZaFw0zNDAzMDUyMzEyMjZaMIGGMQswCQYDVQQGEwJYWDESMBAG +A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t +cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU +Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDjeoBBH28RexVG5OSjotUU/0A6ji5gcExefDDfQaxizlp6FzQ2UYsciwwd +Kz6q8peKLP6HlITJ73Z9XQIjb1hudiZzFFuXQza9sJsWSmaEMAs30U42PNhptbhS +hfLFnHS9sV7EJEXiJM73mlkhXjA+iy0t55BiphZxEBVicvgEp82RXiBkQBhipKSL +AWIcrXXPy7G7PyRTgTFmQv7lgwAc0lTV/WhOVv98AJceiDgX/CxYNj41NMoMIOrj +KUEdByl0jRmomZGRfE09UCb577FBupMjk5exbNlV8GPBhgXJb2P9hbcfPNZ/h/uO +IcwL6gTv9Ty2G66ASovwKBn2grl29+95MgSlpmMupA31q7WGnh5A34qfMMdmoSh/ +abOLpVi6QCgADAyZAbUwihz+5r8B7lKiSFWvFV36TsGzk/FSznh7m/ZvdUz2v/78 +YAMmo1dPtEItMAIYZqVSGakGK+tiLeZbhkT2+cgzdfybdlRucpg3NfcMRX6i8ADd +fQQmlx02LXatsgNZfLRKMjwJ/NfcZ+C04Po/F4Gkr0SOw/kQ2fF8VDsXifT4jRwj +rpOQTqumVDAITYd9lVLy6riBAO4km0k3rIgW6cphZ72BzoRa4NZEufzdAAj9UomN +XgwZZJGT1r/cep4w5qWYKjH/9f0ozPstnWdGtpm7bWqBdcMWuwIDAQABo1MwUTAd +BgNVHQ4EFgQU0oLv+iSCas5EScSdkA+y9qmHotswHwYDVR0jBBgwFoAU0oLv+iSC +as5EScSdkA+y9qmHotswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEALQxvDAOCO9MvwCedx9d5KhtYgqFvB16X6mnfkMjnbp1Iqm4vf8mPIpo8Lw3f +2ZTMG2x5MFoji94ZtIEJENqxfT9p14ftZs6ICX9/obsGHNRRELBokMFtwXxLTVIf +9wSo1JkMqBYZzxZKg2gAgKJCiqPMDo5retCUNG/iJ/6n2g8TiVWPbc2xieCbR4AH +WSwvmy0320ELngifk2rZAnqhzZEXyDBvPYcZgtRG9ZTvAOrE+RL8F+r8Tg3nOt7c +gAWn2YKX40H0qNV5PXuSahScQFtK12slbsDixgZt2WIuXrkLe8jOFSuEbmPqt0bJ +5nmrFWpl3aTcUDJnzycDeLpBXL1hQ8e1iYxcsYL7Wqicd0long0fY+d0mfC0rw2p +CTSN+A5niR/illavxY41I+FT0VeupdkONpo7dNvlWiD/tXaY5XxXnmBrxSOUNObt +fGBh+nqKPlH8H4ne1h+uIKV5no7frxkmUoIY6dbu3K97aKvzLm+5f00S/mETO/HN +fkzk8DHC78v/1yIxQXcnkWMusSUmyuIt2MLPWdmVcD+HMLEXYqG97E/zOvd/7+af +7YoIjANcYRDKCovA0/dfOLF6Wz8L2h9fsfpQIwYJ2rFPV9LeT2c/0CCIt5qf2u69 +tjhKQ2uQWyy7SpbcPitkrYiEqs4emQDhHWldjgs+eMk6Jjc= +-----END CERTIFICATE----- + +Generated with: + +openssl req -x509 -newkey rsa:4096 -keyout tls-key.pem -out tls-cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname" diff --git a/spec/fixtures/tls-key.pem b/spec/fixtures/tls-key.pem new file mode 100644 index 0000000..ab74a85 --- /dev/null +++ b/spec/fixtures/tls-key.pem @@ -0,0 +1,56 @@ +-----BEGIN PRIVATE KEY----- +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 +YfDYafbxI5ssyb7f2I+fhL0isKAMuw== +-----END PRIVATE KEY----- + +Generated with: + +openssl req -x509 -newkey rsa:4096 -keyout tls-key.pem -out tls-cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname" diff --git a/spec/indexSpec.js b/spec/indexSpec.js index d2cbe6e..d9cadd1 100644 --- a/spec/indexSpec.js +++ b/spec/indexSpec.js @@ -13,6 +13,8 @@ describe('index', function() { 'start', 'stop', 'port', + 'scheme', + 'hostname', ])); server.start.and.callFake(() => Promise.reject(new Error('stop here'))); const runner = jasmine.createSpyObj('Runner', ['run']); @@ -472,6 +474,8 @@ describe('index', function() { 'start', 'stop', 'port', + 'scheme', + 'hostname', ]); server.start.and.returnValue(Promise.resolve(server)); server.stop.and.returnValue(Promise.resolve()); @@ -506,7 +510,13 @@ describe('index', function() { }); it('does not launch a browser if the server fails to start', async function() { - const server = jasmine.createSpyObj('Server', ['start', 'stop', 'port']); + const server = jasmine.createSpyObj('Server', [ + 'start', + 'stop', + 'port', + 'scheme', + 'hostname', + ]); server.start.and.returnValue(Promise.reject(new Error('nope'))); const runner = jasmine.createSpyObj('Runner', ['run']); const buildWebdriver = jasmine @@ -532,7 +542,13 @@ describe('index', function() { }); it('stops the browser and server if the runner fails to start', async function() { - const server = jasmine.createSpyObj('Server', ['start', 'stop', 'port']); + const server = jasmine.createSpyObj('Server', [ + 'start', + 'stop', + 'port', + 'scheme', + 'hostname', + ]); server.start.and.returnValue(Promise.resolve(server)); server.stop.and.returnValue(Promise.resolve()); server.port.and.returnValue(0); @@ -573,9 +589,16 @@ function buildStubWebdriver() { } function buildSpyServer() { - const server = jasmine.createSpyObj('Server', ['start', 'stop', 'port']); + const server = jasmine.createSpyObj('Server', [ + 'start', + 'stop', + 'port', + 'scheme', + 'hostname', + ]); server.start.and.returnValue(Promise.resolve(server)); server.stop.and.returnValue(Promise.resolve()); server.port.and.returnValue(0); + server.scheme.and.returnValue('http'); return server; } diff --git a/spec/serverSpec.js b/spec/serverSpec.js index 840372d..2b9e5e4 100644 --- a/spec/serverSpec.js +++ b/spec/serverSpec.js @@ -1,11 +1,18 @@ -const path = require('path'), - http = require('http'), - Server = require('../lib/server'); +const os = require('os'); +const path = require('path'); +const http = require('http'); +const https = require('https'); +const Server = require('../lib/server'); function getFile(url) { return new Promise(function(resolve, reject) { - http - .get(url, function(response) { + const requestModule = url.startsWith('https:') ? https : http; + const options = { + // Allow testing with a self-signed TLS certificate. + rejectUnauthorized: false, + }; + requestModule + .get(url, options, function(response) { if (response.statusCode !== 200) { reject( new Error(`${url} failed with status code ${response.statusCode}`) @@ -24,6 +31,25 @@ function getFile(url) { }); } +function getIP() { + const interfaces = os.networkInterfaces(); + const validFamilies = ['IPv4', 'IPv6']; + + for (const addressInfos of Object.values(interfaces)) { + for (const addressInfo of addressInfos) { + if ( + validFamilies.includes(addressInfo.family) && + addressInfo.internal == false + ) { + return addressInfo.address; + } + } + } + + // Can't find a non-localhost IP. + return '127.0.0.1'; +} + // Note: various payload specs check for \r?\n instead of either \n or os.EOL // because the payloads sometimes contains \r\n and sometimes \n on Windows. // (TODO why?) @@ -353,6 +379,45 @@ describe('server', function() { expect(html).toContain('/__support__/loaders.js'); }); + it('starts a server with TLS', async function() { + const tlsDir = path.resolve(__dirname, 'fixtures'); + await this.startServer({ + tlsCert: path.resolve(tlsDir, 'tls-cert.pem'), + tlsKey: path.resolve(tlsDir, 'tls-key.pem'), + }); + const baseUrl = `https://localhost:${this.server.port()}`; + + const jazz = await getFile(baseUrl + '/__jasmine__/jazz.js'); + expect(jazz).toMatch(/^Jazzy\r?\n$/); + }); + + it('starts a server with the specified hostname', async function() { + const ip = getIP(); + + if (ip == '127.0.0.1') { + pending('Cannot test hostname without a non-localhost interface.'); + } + + await this.startServer({ + hostname: ip, + }); + + // The server is listening on a specific IP. We should be able to use + // that, but not localhost. + const workingBaseUrl = `http://${ip}:${this.server.port()}`; + const nonWorkingBaseUrl = `http://localhost:${this.server.port()}`; + + const jazz = await getFile(workingBaseUrl + '/__jasmine__/jazz.js'); + expect(jazz).toMatch(/^Jazzy\r?\n$/); + + try { + await getFile(nonWorkingBaseUrl); + fail('We should not be listening on localhost'); + } catch (error) { + expect(error.code).toBe('ECONNREFUSED'); + } + }); + describe('loading specs and helpers', function() { function behavesLikeTopLevelAwaitDisabled() { it('loads .js files as regular scripts', async function() {