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() {