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

Support serving over HTTPS #417

Merged
merged 1 commit into from
Jul 26, 2019
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
8 changes: 7 additions & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ module.exports = function broccoliCLI(args, ui = new UI()) {
.description('start a broccoli server')
.option('--port <port>', 'the port to bind to [4200]', 4200)
.option('--host <host>', 'the host to bind to [localhost]', 'localhost')
.option('--ssl', 'serve via HTTPS', false)
.option('--ssl-key <path>', 'path to SSL key file [ssl/server.key]', 'ssl/server.key')
.option('--ssl-cert <path>', 'path to SSL cert file [ssl/server.crt]', 'ssl/server.crt')
.option('--brocfile-path <path>', 'the path to brocfile')
.option('--output-path <path>', 'the path to target output folder')
.option('--cwd <path>', 'the path to working folder')
Expand Down Expand Up @@ -73,7 +76,10 @@ module.exports = function broccoliCLI(args, ui = new UI()) {
parseInt(options.port, 10),
undefined,
undefined,
ui
ui,
options.ssl,
options.sslKey,
options.sslCert
);
});

Expand Down
51 changes: 40 additions & 11 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@

const promiseFinally = require('promise.prototype.finally');
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const messages = require('./messages');
const middleware = require('./middleware');
const EventEmitter = require('events');

class Server extends EventEmitter {
constructor(watcher, host, port, connect = require('connect'), ui) {
constructor(watcher, host, port, connect = require('connect'), ui, ssl, sslKey, sslCert) {
super();

this._watcher = watcher;
this._builder = watcher.builder;
this._host = host;
this._port = parseInt(port, 10);
this._ssl = ssl;
this._sslKey = sslKey;
this._sslCert = sslCert;
this._connect = connect;
this._url = `http://${this._host}:${this._port}`;
this.app = this.http = null;
this._url = `http${this._ssl ? 's' : ''}://${this._host}:${this._port}`;
this.app = this.instance = null;
this._boundStop = this.stop.bind(this);
this._started = false;
this.ui = ui;
Expand All @@ -42,15 +48,35 @@ class Server extends EventEmitter {
const promise = new Promise((resolve, reject) => {
this.app = this._connect().use(middleware(this._watcher));

this.http = http.createServer(this.app);
this.http.listen(this._port, this._host);
if (this._ssl) {
let sslOptions;
try {
sslOptions = {
key: fs.readFileSync(this._sslKey),
cert: fs.readFileSync(this._sslCert),
};
} catch (err) {
throw new Error(
`SSL key and certificate files should be present at ${path.join(
process.cwd(),
this._sslKey
)} and ${path.join(process.cwd(), this._sslCert)} respectively.`
);
}

this.instance = https.createServer(sslOptions, this.app);
} else {
this.instance = http.createServer(this.app);
}

this.instance.listen(this._port, this._host);

this.http.on('listening', () => {
this.instance.on('listening', () => {
this.ui.writeLine(`Serving on ${this._url}\n`);
resolve(this._watcher.start());
});

this.http.on('error', error => {
this.instance.on('error', error => {
if (error.code !== 'EADDRINUSE') {
throw error;
}
Expand Down Expand Up @@ -85,8 +111,8 @@ class Server extends EventEmitter {

stop() {
this._detachListeners();
if (this.http) {
this.http.close();
if (this.instance) {
this.instance.close();
}

return this._watcher.quit().then(() => this._builder.cleanup());
Expand All @@ -99,9 +125,12 @@ exports.serve = function serve(
port,
_connect = require('connect'),
_process = process,
ui
ui,
ssl,
sslKey,
sslCert
) {
const server = new Server(watcher, host, port, _connect, ui);
const server = new Server(watcher, host, port, _connect, ui, ssl, sslKey, sslCert);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This kind of looks error prone. We should consider changing this signature to accept an object instead of individual parameters like these.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good, although I suspect we don't need to block this PR on it.


return server
.start()
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"handlebars": "^4.0.11",
"heimdalljs": "^0.2.6",
"heimdalljs-logger": "^0.1.9",
"https": "^1.0.0",
"mime-types": "^2.1.19",
"promise.prototype.finally": "^3.1.0",
"resolve-path": "^1.4.0",
Expand Down
22 changes: 22 additions & 0 deletions test/fixtures/ssl/ssl/server.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDlzCCAn+gAwIBAgIJAPLOmDH8o4hfMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
BAYTAklOMQswCQYDVQQIEwJUTjEQMA4GA1UEBxMHQ2hlbm5haTENMAsGA1UEChME
U2l2YTAeFw0xOTA3MTMxNDU1MTZaFw00NjExMjgxNDU1MTZaMDsxCzAJBgNVBAYT
AklOMQswCQYDVQQIEwJUTjEQMA4GA1UEBxMHQ2hlbm5haTENMAsGA1UEChMEU2l2
YTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAONjIZBNW4f6heS+JRtG
aJlFIdTN0gcjudtcAtADs9wSULA5QR0/xZplTpoSvb52Bec8RqpDzRNfXw8R4xVk
PwMxUFU+g66pgapBuRLvKmQ+yIztOap3rM+nJoHTZKhdHrsma+/eczbiCsL7fjfs
HVPSzQQIK4Lk+c7fb0G9LQOE6bUDawS/MGCqgl+3s/MS4lNjFO0paakv8r05oBps
NVzEMZVFdd5sO9c5fj2/nW3VtcP8askOapeFA60HZIihr/IOef5MyPA+vlZRWPeK
ghwoDVJeRgaDPnA+/WxKESh4B3lr2MxYQrNEpwdqXni6V6dmUJ2LWNRgWFdYUa+5
2h0CAwEAAaOBnTCBmjAdBgNVHQ4EFgQU95UUFuj43LZ6k4HoWJo7eYjIBy4wawYD
VR0jBGQwYoAU95UUFuj43LZ6k4HoWJo7eYjIBy6hP6Q9MDsxCzAJBgNVBAYTAklO
MQswCQYDVQQIEwJUTjEQMA4GA1UEBxMHQ2hlbm5haTENMAsGA1UEChMEU2l2YYIJ
APLOmDH8o4hfMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBACvjDIk6
vSQD3nJiW+4UOzMHQEoRnoGFjEg+GQcsPZetU6fSSqGUNrVoCdlUzy9n4XAcCmIP
Ye9H8HgfWi2GzahfnNZPES3peeDt+t0tffOAkzRVcmR36B5f13bXGLqNNxMPR5t0
LabuoTp3Xtw7nNrfWfetrvhYTNrBlt6QgzRiMn1oMuHKZyWNRJbmR6DC0AqkUPnl
WKsCg7xFYIFbocqtCQyfoAZhG8/ivSVA9UhYkIDOEVFEzru5TBiudUb0v9scas6v
Ndz8+07RgdjtrV+mtE6mCEc838TZs0LL8wJnXktLSLPSAwCdrAXSShKgYRYyyFvw
LmFId+bcICiBZwI=
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions test/fixtures/ssl/ssl/server.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA42MhkE1bh/qF5L4lG0ZomUUh1M3SByO521wC0AOz3BJQsDlB
HT/FmmVOmhK9vnYF5zxGqkPNE19fDxHjFWQ/AzFQVT6DrqmBqkG5Eu8qZD7IjO05
qnesz6cmgdNkqF0euyZr795zNuIKwvt+N+wdU9LNBAgrguT5zt9vQb0tA4TptQNr
BL8wYKqCX7ez8xLiU2MU7SlpqS/yvTmgGmw1XMQxlUV13mw71zl+Pb+dbdW1w/xq
yQ5ql4UDrQdkiKGv8g55/kzI8D6+VlFY94qCHCgNUl5GBoM+cD79bEoRKHgHeWvY
zFhCs0SnB2peeLpXp2ZQnYtY1GBYV1hRr7naHQIDAQABAoIBAETTk6w3DhaJqQ+E
0nyAAlcqSsQ8DG+my7HvFtbZ2A4r6Qp+OgxdajWCppkSRSaqWL0WTJlq7l8HxiEs
m9y0vDH+Mj+rLXanzhy3ygpGJEG1k0S85XCIyuELyicP0m73yL6DMbaoUZ1yLCm6
sAjDSlk9/FpVEr6LCmYo0WdHmKFwfnwZzd32N/id0TMEGO1MBwUmYh36vVRgv/Kd
pQda+DyHV5yZwW8RqeflJlgEs5yzykay200BCx/FInUHS/kf9pA0fkftFVX17DSW
bXtOsE1bvPI2VD9T6WTWL3GHCB1KjDWBvcxDS3/mIfNFmV1LMKNA3NmITB6rmWDK
U9t3aM0CgYEA/pBZg7JuaCe46VJrxF4kSQqdqKkUtNh4/uoEdLGUMjtMXWtzyFsF
9vyDwP6BJq3U+EneU194tb+fRUZrD9mbdOkaRAJuBiVqrC8lDo1tGPqO4n67XQAw
gODIDG44TWDmXaEHBs3rcNPazyUzTN9jE0uQ+wlvXCfpFqVPHfL6uZ8CgYEA5KuI
LsserBQQQAIkiX8pXWbmSfjiX0PuWaRkmcZNmLibJf7uL1QnAhNjhGxivr4m+nyE
JUY++BTsNNEORUT7utUwyBCn2+EgDfB5AtU5xLStl540k/VMmegaUTrl9NComjdn
Ap7ZH4VJ1ySRAnZs27fDa6rIJYTdDVI7UURZysMCgYAqHwBwxil2mnwCP10NlPY0
D8meirueYONEarxqAqge16j/HDSARm9qOsYiPPppyAGhQ7fB295BVH+qGsjESqFq
atepwS0rXy2TaAmdqtEOfQb/ezDNZqaf7JGtXN98DjiP7YEYIyJ9/NALzn/6jEv8
eVh38Uu31/K9iITa08WgyQKBgQCWvw3z6yu5PTDJQaBclFvsmx1NvqldLCdBKUK4
rSYSfACrt7fFs7BTifydG+as4CZKEzP6bqisWv6sgvTpbWSV8l1KRtCh/3NmPRVJ
bM+8gW+++CipMZjiDUTsL+vQzI2ZvNRHwfhY80GOdyixeuigoDBQFjIDqecgFQnN
tUWMiQKBgQDKENCKQmLxG1EbrYaR8LYqRJUGgIvEdjq92Bwo44MsI1okFP406rGw
47W5QJSbmTGAvkmf61IyhmGEjAxoz6sZZLtSEXai67kix0bXZaHNH+1lsEj1qInS
q1mHzUIrLou6TCe3nvbCat/2CjPeMHsGxYXKda0/gkWpjvloEGA2+w==
-----END RSA PRIVATE KEY-----
45 changes: 39 additions & 6 deletions test/server_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,39 @@ describe('server', function() {
});
}).timeout(10000);

it('starts with ssl if ssl option is passed', function() {
const mockUI = new MockUI();
const builder = new Builder(new broccoliSource.WatchedDir('test/fixtures/ssl'));
const watcher = new Watcher(builder, []);

server = new Server.Server(
watcher,
'127.0.0.1',
PORT,
undefined,
mockUI,
true,
'test/fixtures/ssl/ssl/server.key',
'test/fixtures/ssl/ssl/server.crt'
);
server.start();

return new Promise((resolve, reject) => {
server.instance.on('listening', resolve);
server.instance.on('close', reject);
server.instance.on('error', reject);
}).then(() => {
return got(`https://127.0.0.1:${PORT}/`, { rejectUnauthorized: false }).then(
res => {
expect(res.statusCode).to.eql(200);
},
() => {
throw new Error('should not be reached');
}
);
});
}).timeout(5000);

it('buildSuccess is handled', function() {
const mockUI = new MockUI();
const builder = new Builder(new broccoliSource.WatchedDir('test/fixtures/basic'));
Expand Down Expand Up @@ -122,9 +155,9 @@ describe('server', function() {
server.start();

return new Promise((resolve, reject) => {
server.http.on('listening', resolve);
server.http.on('close', reject);
server.http.on('error', reject);
server.instance.on('listening', resolve);
server.instance.on('close', reject);
server.instance.on('error', reject);
}).then(() => {
return got(`http://127.0.0.1:${PORT}/`).then(
() => {
Expand Down Expand Up @@ -171,9 +204,9 @@ describe('server', function() {
server.start();

return new Promise((resolve, reject) => {
server.http.on('listening', resolve);
server.http.on('close', reject);
server.http.on('error', reject);
server.instance.on('listening', resolve);
server.instance.on('close', reject);
server.instance.on('error', reject);
}).then(() => {
return got(`http://127.0.0.1:${PORT}/foo.txt`) // basic serving
.then(res => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,11 @@ http-errors@~1.6.2:
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"

https@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https/-/https-1.0.0.tgz#3c37c7ae1a8eeb966904a2ad1e975a194b7ed3a4"
integrity sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=

iconv-lite@^0.4.17, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
Expand Down