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

allow explicitly setting the protocol from the public option #1117

Merged
merged 11 commits into from
Oct 8, 2017
11 changes: 10 additions & 1 deletion client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ function reloadApp() {
}
} else {
log.info('[WDS] App updated. Reloading...');
self.location.reload();
let rootWindow = self;
// use parent window for reload (in case we're in an iframe with no valid src)
do {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is almost never a good idea in the browser - on the off chance that the while condition is not met, this will lock the browser forcing a close. please use setInterval in conjunction with clearInterval. the log message should also be moved to just before the actual call to reload

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't really get what you mean with using setInterval and clearInterval here and how this will improve the code.

+1 for moving the log down

if (rootWindow.location.protocol !== 'about:') {
break;
}
rootWindow = self.parent;
} while (rootWindow.parent !== self.parent);

rootWindow.location.reload();
}
}
6 changes: 6 additions & 0 deletions examples/cli-public-protocol/Dockerfile.devserver
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM node:6

EXPOSE 80

VOLUME /devserver
WORKDIR /devserver/examples/cli-public-protocol
6 changes: 6 additions & 0 deletions examples/cli-public-protocol/Dockerfile.nginx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM nginx

COPY nginx-default.conf /etc/nginx/conf.d/default.conf

RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" && \
openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
19 changes: 19 additions & 0 deletions examples/cli-public-protocol/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# CLI - public protocol

NOTE: make sure you have docker and docker-compose (at least version 2) installed and already installed all dependencies via `npm install` in the repository.

```shell
docker-compose up
```

If you're manually injecting an entrypoint to an iframe, the client needs to know where to connect to the server. Since an iframe with `src="about:blank` doesn't know which protocol you're using on the outside we need to explicitly define it with the `public` option (have a look to the config provided in `webpack.config.js`).

To be able to show how this works together with an nginx reverse proxy there's a small docker setup which does this for you.

## What should happen

The `docker-compose up` starts up two containers (nginx and devserver), whereas nginx is exposing port 80 and 443 (with a self-signed certificate). If you open your browser and navigate to `https://localhost` you have to accept the self-signed certificate which is - of course - not secure but ok for testing purposes. After that you can see an iframe which says `I'm sitting in the iframe and the socket connection to dev-server works!`.

If you have a look at the network tab of the inspector you'll see that the `sockjs-node` connection is up and running. If you wouldn't specify the `https://` protocol in the `public` option, it would try to connect to the socket via `http://localhost` which would fail because the parent window is loaded via `https` and therefore violate the security rules of the browser.

If you edit the contents of `app.js` you'll also see that auto-reloading works.
5 changes: 5 additions & 0 deletions examples/cli-public-protocol/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const h3El = document.createElement('h3');
h3El.innerHTML = 'I\'m sitting in the iframe asdfand the socket connection to dev-server works!';
document.body.appendChild(h3El);
20 changes: 20 additions & 0 deletions examples/cli-public-protocol/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: '2'

services:
nginx:
build:
context: .
dockerfile: Dockerfile.nginx
ports:
- 80:80
- 443:443
links:
- devserver

devserver:
build:
context: .
dockerfile: Dockerfile.devserver
volumes:
- ../../:/devserver
command: 'node /devserver/bin/webpack-dev-server.js'
15 changes: 15 additions & 0 deletions examples/cli-public-protocol/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<iframe id="testframe" src="about:blank"></iframe>

<script>
var testframe = document.getElementById('testframe');
var scriptEl = document.createElement('script');
scriptEl.setAttribute('src', 'https://localhost/bundle.js');
testframe.contentDocument.head.appendChild(scriptEl);
</script>
</body>
</html>
32 changes: 32 additions & 0 deletions examples/cli-public-protocol/nginx-default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
server {
listen 80;
listen 443 ssl;

ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;

ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

charset UTF-8;

location /sockjs-node {
proxy_pass http://devserver;

proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1d;
}

location / {
proxy_pass http://devserver;
}
}
12 changes: 12 additions & 0 deletions examples/cli-public-protocol/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

module.exports = {
context: __dirname,
entry: './app.js',
devServer: {
host: '0.0.0.0',
port: 80,
public: 'https://localhost',
disableHostCheck: true
}
};
6 changes: 5 additions & 1 deletion lib/util/createDomain.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ module.exports = function createDomain(options, listeningApp) {
const port = options.socket ? 0 : appPort;
const hostname = options.useLocalIp ? internalIp.v4() : options.host;

// use explicitly defined public url (prefix with protocol if not explicitly given)
if (options.public) {
return /^[a-zA-Z]+:\/\//.test(options.public) ? `${options.public}` : `${protocol}://${options.public}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

sorry, but this is not what was requested in previous comments

this is not a good way to check a url for a protocol. please use url.parse instead, and examine the result for a protocol.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wrote in the commit comment that url.parse is not appropriate here.. I did it with url.parse before but the new tests I provided showed it's not working as it should..

found a bug due to tests: url.parse doesn't properly parse a provided public option since it counts everything before the port colon as the protocol which is just wrong. I replaced it with a protocol regex.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, but I looked up url.parse documentation again, as well as searching for issues with what I'm experiencing, and there's no easy solution possible by using url.parse.

Why? The public parameter can be a non-valid url like "localhost:8080" which is parsed to somethin like this:

> url.parse("localhost:8080")
Url {
  protocol: 'localhost:',
  slashes: null,
  auth: null,
  host: '8080',
  port: null,
  hostname: '8080',
  hash: null,
  search: null,
  query: null,
  pathname: null,
  path: null,
  href: 'localhost:8080' }

As you can see, the protocol is incorrect and I would need to do more processing to find out what's going on. I think the solution using regex is the better option here.

Also have a look at nodejs/node#5755, where they're discussing the same issue on parsing urls.

Copy link
Contributor

Choose a reason for hiding this comment

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

great documentation of the why. that'll certainly be beneficial in the future

}
// the formatted domain (url without path) of the webpack server
return options.public ? `${protocol}://${options.public}` : url.format({
return url.format({
protocol,
hostname,
port
Expand Down
94 changes: 94 additions & 0 deletions test/Util.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict';

const webpack = require('webpack');
const internalIp = require('internal-ip');
const Server = require('../lib/Server');
const createDomain = require('../lib/util/createDomain');
const config = require('./fixtures/simple-config/webpack.config');

describe('check utility funcitons', () => {
let compiler;
before(() => {
compiler = webpack(config);
});

const tests = [{
name: 'default',
options: {
host: 'localhost',
port: 8080
},
expected: 'http://localhost:8080'
}, {
name: 'https',
options: {
host: 'localhost',
port: 8080,
https: true
},
expected: 'https://localhost:8080',
timeout: 10000
}, {
name: 'override with public',
options: {
host: 'localhost',
port: 8080,
public: 'myhost.test'
},
expected: 'http://myhost.test'
}, {
name: 'override with public (port)',
options: {
host: 'localhost',
port: 8080,
public: 'myhost.test:9090'
},
expected: 'http://myhost.test:9090'
}, {
name: 'override with public (protocol)',
options: {
host: 'localhost',
port: 8080,
public: 'https://myhost.test'
},
expected: 'https://myhost.test'
}, {
name: 'override with public (protocol + port)',
options: {
host: 'localhost',
port: 8080,
public: 'https://myhost.test:9090'
},
expected: 'https://myhost.test:9090'
}, {
name: 'localIp',
options: {
useLocalIp: true,
port: 8080
},
expected: `http://${internalIp.v4()}:8080`
}];

tests.forEach((t) => {
const itInstance = it(`test createDomain '${t.name}'`, (done) => {
const options = t.options;
const server = new Server(compiler, options);
const expected = t.expected;
server.listen(options.port, options.host, (err) => {
if (err) {
done(err);
}
const generatedDomain = createDomain(options, server.listeningApp);
if (generatedDomain !== expected) {
done(`generated domain ${generatedDomain} doesn't match expected ${expected}`);
} else {
done();
}
server.close();
});
});
if (t.timeout) {
itInstance.timeout(t.timeout);
}
});
});