Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TLS and hostname support #49

Merged
merged 4 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions bin/jasmine-browser-runner
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
12 changes: 6 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -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 = [];
Expand Down Expand Up @@ -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...');
Expand Down
3 changes: 3 additions & 0 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
26 changes: 23 additions & 3 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
78 changes: 68 additions & 10 deletions lib/server.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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 http = require('http');
const https = require('https');
const path = require('path');

/**
* @class Server
Expand Down Expand Up @@ -210,12 +212,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 (no hostname) and point browsers to "localhost". We preserve
// backward compatibility here by using different defaults for these two
// things.
const listenOptions = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I think it'd be better to listen only on loopback by default but that's a change for the next major release.

port,
host: hostname || '',
};
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
Expand All @@ -225,7 +245,23 @@ 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 = http
.createServer(app)
.listen(listenOptions, callback);
this._httpServerScheme = 'http';
}
});
}

Expand Down Expand Up @@ -257,6 +293,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) {
Expand Down
40 changes: 40 additions & 0 deletions lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
38 changes: 38 additions & 0 deletions spec/fixtures/tls-cert.pem
Original file line number Diff line number Diff line change
@@ -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"
Loading