Skip to content

Commit

Permalink
Configure listen address separately from hostname
Browse files Browse the repository at this point in the history
This allows for configurations that need to connect to localhost but
listen to all interfaces, like typical Saucelabs usage. It also makes
things clearer and more explicitly user-controllable.
  • Loading branch information
sgravrock committed Jun 15, 2024
1 parent d8729ae commit adc8d2d
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 17 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,24 @@ 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.

## Controlling which network interfaces are listened to

By default, jasmine-browser-runner listens to all available network interfaces.
You might need that if you're using a remote grid such as Saucelabs. If you
don't need that, you can improve security by listening only to localhost.

```javascript
export default {
// ...
"listenAddress": "localhost",
// ...
}
```

## Hostname support

To serve tests on a specific interface or IP, you can specify a hostname in
`jasmine-browser.mjs`:
If you need to access your tests via a specific hostname, you can do that by
setting the `hostname` configuration property:

```javascript
export default {
Expand All @@ -98,6 +112,11 @@ export default {

This can also be specified on the command line with `--hostname`.

Setting `hostname` but not `listenAddress` has the same effect as setting
`listenAddress` to the same value as `hostname`. If you need to set a hostname
but retain the default behavior of listening to all network interfaces, you can
do that by setting `listenAddress` to `"*"`.

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
Expand Down
16 changes: 11 additions & 5 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,20 @@ class Server {
const tlsCert = serverOptions.tlsCert || this.options.tlsCert;
const tlsKey = serverOptions.tlsKey || this.options.tlsKey;
const hostname = serverOptions.hostname || this.options.hostname;
// The last two fallbacks here are necessary for backwards compatibility.
let listenAddress =
serverOptions.listenAddress ||
this.options.listenAddress ||
hostname ||
'';

if (listenAddress === '*') {
listenAddress = '';
}

// 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 = {
port,
host: hostname || '',
host: listenAddress,
};
this._httpHostname = hostname || 'localhost';

Expand Down
23 changes: 18 additions & 5 deletions lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,20 @@
* @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.
* The hostname or IP address of the network interface to listen on. For
* security, this should be set to localhost or an equivalent unless you need
* the server to be accessible over other network interfaces. Set to "*" to
* listen on all interfaces, which may be required by some remote Selenium
* grids.
* @name ServerCtorOptions#listenAddress
* @default The value of {@link ServerCtorOptions#hostname} or {@link ServerStartOptions#hostname} if configured, otherwise "*"
* @type string | undefined
*/
/**
* The hostname to use in the URL given to browsers. For backward compatibility,
* setting this property without also setting {@link ServerCtorOptions#listenAddress}
* or {@link ServerStartOptions#listenAddress} has the same effect as setting
* the listen address to the hostname.
* @name ServerCtorOptions#hostname
* @default "localhost"
* @type string | undefined
Expand Down Expand Up @@ -307,7 +317,10 @@
/**
* @see ServerCtorOptions#hostname
* @name ServerStartOptions#hostname
* @type string
*/
/**
* @see ServerCtorOptions#listenAddress
* @name ServerStartOptions#listenAddress
*/

/**
Expand Down
105 changes: 100 additions & 5 deletions spec/serverSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -482,10 +482,9 @@ describe('server', function() {
}

describe('Passing options to the ctor', function() {
describe('When hostname is not specified', function() {
describe('When neither hostname nor listenAddress is specified', function() {
it('listens to all interfaces', async function() {
const options = baseCtorOptions();
expect(options.hostname).toBeFalsy();
const http = makeMockNodeServer('http');
this.server = new Server(options, { http });

Expand All @@ -498,7 +497,7 @@ describe('server', function() {
});
});

describe('When hostname is specified', function() {
describe('When hostname but not listenAddress is specified', function() {
it('listens to the specified hostname', async function() {
const options = baseCtorOptions();
options.hostname = 'specific.example.com';
Expand All @@ -513,10 +512,59 @@ describe('server', function() {
);
});
});

describe('When listenAddress but not hostname is specified', function() {
it('listens to the specified listenAddress', async function() {
const options = baseCtorOptions();
options.listenAddress = 'specific.example.com';
const http = makeMockNodeServer('http');
this.server = new Server(options, { http });

await this.server.start();

expect(http.listen).toHaveBeenCalledWith(
jasmine.objectContaining({ host: 'specific.example.com' }),
jasmine.any(Function)
);
});
});

describe('When both hostname and listenAddress are specified', function() {
it('listens to the specified listenAddress', async function() {
const options = baseCtorOptions();
options.listenAddress = 'specific.example.com';
options.hostname = 'other.example.com';
const http = makeMockNodeServer('http');
this.server = new Server(options, { http });

await this.server.start();

expect(http.listen).toHaveBeenCalledWith(
jasmine.objectContaining({ host: 'specific.example.com' }),
jasmine.any(Function)
);
});
});

describe('When listenAddress is "*"', function() {
it('listens to all interfaces', async function() {
const options = baseCtorOptions();
options.listenAddress = '*';
const http = makeMockNodeServer('http');
this.server = new Server(options, { http });

await this.server.start();

expect(http.listen).toHaveBeenCalledWith(
jasmine.objectContaining({ host: '' }),
jasmine.any(Function)
);
});
});
});

describe('Passing options to start', function() {
describe('When hostname is not specified', function() {
describe('When neither hostname nor listenAddress is specified', function() {
it('listens to all interfaces', async function() {
const http = makeMockNodeServer('http');
this.server = new Server(baseCtorOptions(), { http });
Expand All @@ -530,7 +578,7 @@ describe('server', function() {
});
});

describe('When hostname is specified', function() {
describe('When hostname but not listenAddress is specified', function() {
it('listens to the specified hostname', async function() {
const http = makeMockNodeServer('http');
this.server = new Server(baseCtorOptions(), { http });
Expand All @@ -543,6 +591,53 @@ describe('server', function() {
);
});
});

describe('When listenAddress but not hostname is specified', function() {
it('listens to the specified listenAddress', async function() {
const http = makeMockNodeServer('http');
this.server = new Server(baseCtorOptions(), { http });

await this.server.start({
listenAddress: 'specific.example.com',
});

expect(http.listen).toHaveBeenCalledWith(
jasmine.objectContaining({ host: 'specific.example.com' }),
jasmine.any(Function)
);
});
});

describe('When both hostname and listenAddress are specified', function() {
it('listens to the specified listenAddress', async function() {
const http = makeMockNodeServer('http');
this.server = new Server(baseCtorOptions(), { http });

await this.server.start({
listenAddress: 'specific.example.com',
hostname: 'other.example.com',
});

expect(http.listen).toHaveBeenCalledWith(
jasmine.objectContaining({ host: 'specific.example.com' }),
jasmine.any(Function)
);
});
});

describe('When listenAddress is "*"', function() {
it('listens to all interfaces', async function() {
const http = makeMockNodeServer('http');
this.server = new Server(baseCtorOptions(), { http });

await this.server.start({ listenAddress: '*' });

expect(http.listen).toHaveBeenCalledWith(
jasmine.objectContaining({ host: '' }),
jasmine.any(Function)
);
});
});
});
});

Expand Down

0 comments on commit adc8d2d

Please sign in to comment.