create-servers
Advanced tools
Comparing version 2.5.0 to 2.6.0
92
index.js
@@ -13,3 +13,5 @@ 'use strict'; | ||
https = require('https'), | ||
tls = require('tls'), | ||
path = require('path'), | ||
constants = require('constants'), | ||
connected = require('connected'), | ||
@@ -40,2 +42,4 @@ errs = require('errs'), | ||
var secureOptions = constants.SSL_OP_NO_SSLv3; | ||
/** | ||
@@ -125,3 +129,3 @@ * function createServers (dispatch, options, callback) | ||
// | ||
function createHttps(next) { | ||
function createHttps() { | ||
if (typeof options.https === 'undefined') { | ||
@@ -134,19 +138,6 @@ log('https | no options.https; no server'); | ||
port = !isNaN(ssl.port) ? +ssl.port : 443, // accepts string or number | ||
ciphers = ssl.ciphers || CIPHERS, | ||
timeout = options.timeout || ssl.timeout, | ||
ca = ssl.ca, | ||
server, | ||
args; | ||
// | ||
// Remark: If an array is passed in lets join it like we do the defaults | ||
// | ||
if (Array.isArray(ciphers)) { | ||
ciphers = ciphers.join(':'); | ||
} | ||
if (ca && !Array.isArray(ca)) { | ||
ca = [ca]; | ||
} | ||
var finalHttpsOptions = assign({}, ssl, { | ||
@@ -158,3 +149,3 @@ // | ||
cert: normalizeCertContent(ssl.root, ssl.cert, ssl.key), | ||
ca: ca && ca.map(normalizePEMContent.bind(null, ssl.root)), | ||
ca: normalizeCA(ssl.root, ssl.ca), | ||
// | ||
@@ -164,3 +155,3 @@ // Properly expose ciphers for an A+ SSL rating: | ||
// | ||
ciphers: ciphers, | ||
ciphers: normalizeCiphers(ssl.ciphers), | ||
honorCipherOrder: !!ssl.honorCipherOrder, | ||
@@ -172,5 +163,9 @@ // | ||
secureProtocol: 'SSLv23_method', | ||
secureOptions: require('constants').SSL_OP_NO_SSLv3 | ||
secureOptions: secureOptions | ||
}); | ||
if (ssl.sni && !finalHttpsOptions.SNICallback) { | ||
finalHttpsOptions.SNICallback = getSNIHandler(ssl) | ||
} | ||
log('https | listening on %d', port); | ||
@@ -229,2 +224,9 @@ server = https.createServer(finalHttpsOptions, ssl.handler || handler); | ||
function normalizeCA(root, ca) { | ||
if (ca && !Array.isArray(ca)) { | ||
ca = [ca]; | ||
} | ||
return ca && ca.map(normalizePEMContent.bind(null, root)); | ||
} | ||
/** | ||
@@ -250,1 +252,57 @@ * function normalizePEMContent(root, file) | ||
} | ||
function normalizeCiphers(ciphers) { | ||
ciphers = ciphers || CIPHERS; | ||
// | ||
// Remark: If an array is passed in lets join it like we do the defaults | ||
// | ||
if (Array.isArray(ciphers)) { | ||
ciphers = ciphers.join(':'); | ||
} | ||
return ciphers; | ||
} | ||
function getSNIHandler(sslOpts) { | ||
var sniHosts = Object.keys(sslOpts.sni); | ||
// Pre-compile regexps for the hostname | ||
var hostRegexps = sniHosts.map(function (host) { | ||
return new RegExp( | ||
'^' + | ||
host | ||
.replace('.', '\\.') // Match dots, not wildcards | ||
.replace('*\\.', '(?:.*\\.)?') + // Handle optional wildcard sub-domains | ||
'$', | ||
'i' | ||
); | ||
}); | ||
// Prepare secure context params ahead-of-time | ||
var hostTlsOpts = sniHosts.map(function (host) { | ||
var hostOpts = sslOpts.sni[host]; | ||
var root = hostOpts.root || sslOpts.root; | ||
return assign({}, sslOpts, hostOpts, { | ||
key: normalizePEMContent(root, hostOpts.key), | ||
cert: normalizeCertContent(root, hostOpts.cert), | ||
ca: normalizeCA(root, hostOpts.ca || sslOpts.ca), | ||
ciphers: normalizeCiphers(hostOpts.ciphers || sslOpts.ciphers), | ||
honorCipherOrder: !!(hostOpts.honorCipherOrder || sslOpts.honorCipherOrder), | ||
secureProtocol: 'SSLv23_method', | ||
secureOptions: secureOptions | ||
}); | ||
}); | ||
return function (hostname, cb) { | ||
var matchingHostIdx = sniHosts.findIndex(function(candidate, i) { | ||
return hostRegexps[i].test(hostname); | ||
}); | ||
if (matchingHostIdx === -1) { | ||
return void cb(new Error('Unrecognized hostname: ' + hostname)); | ||
} | ||
cb(null, tls.createSecureContext(hostTlsOpts[matchingHostIdx])); | ||
}; | ||
} |
{ | ||
"name": "create-servers", | ||
"version": "2.5.0", | ||
"version": "2.6.0", | ||
"description": "Create an http AND/OR an https server and call the same request handler.", | ||
@@ -30,2 +30,3 @@ "main": "index.js", | ||
"devDependencies": { | ||
"evil-dns": "^0.2.0", | ||
"sinon": "^5.0.7", | ||
@@ -32,0 +33,0 @@ "tape": "~4.9.0" |
@@ -30,2 +30,3 @@ # create-servers | ||
| `https.ca` | Cert or array of certs specifying trusted authorities for peer certificates. Only required if your server accepts client certificate connections signed by authorities that are not trusted by default. See [Certificate normalization](#certificate-normalization) for more details. | | ||
| `https.sni` | See [SNI Support](#sni-support). | | ||
| `https.handler` | Handler for HTTPS requests. If you want to share a handler with all servers, use a top-level `handler` config property instead. | | ||
@@ -42,3 +43,3 @@ | `https.*` | Any other properties supported by [https.createServer](https://nodejs.org/dist/latest-v8.x/docs/api/https.html#https_https_createserver_options_requestlistener) can be added to the https object, except `secureProtocol` and `secureOptions` which are set to recommended values. | | ||
### Certificate normalization | ||
### Certificate Normalization | ||
@@ -104,2 +105,57 @@ `create-servers` provides some conveniences for `https.ca`, `https.key`, and | ||
### SNI Support | ||
[Server Name Indication](https://en.wikipedia.org/wiki/Server_Name_Indication), | ||
or SNI, lets HTTPS clients announce which hostname they wish to connect to | ||
before the server sends its certificate, enabling the use of the same server for | ||
multiple hosts. Although `SNICallback` can be used to support this, you lose the | ||
convenient certificate normalization provided by `create-servers`. The `sni` | ||
config option provides an easier way. | ||
The `sni` option is an object with each key being a supported hostname and each | ||
value being a subset of the HTTPS settings listed above. HTTPS settings defined | ||
at the top level are used as defaults for the hostname-specific settings. | ||
```js | ||
const createServers = require('create-servers'); | ||
createServers( | ||
{ | ||
https: { | ||
port: 443, | ||
sni: { | ||
'example1.com': { | ||
key: '/certs/private/example1.com.key', | ||
cert: '/certs/public/example1.com.crt' | ||
}, | ||
'example2.com': { | ||
key: '/certs/private/example2.com.key', | ||
cert: '/certs/public/example2.com.crt' | ||
} | ||
} | ||
}, | ||
handler: function (req, res) { | ||
res.end('Hello'); | ||
} | ||
}, | ||
function (errs) { | ||
if (errs) { | ||
return console.log(errs.https); | ||
} | ||
console.log('Listening on 443'); | ||
} | ||
); | ||
``` | ||
Use `*` in the hostname for wildcard certs. Example: `*.example.com`. The | ||
following settings are supported in the host-specific configuration: | ||
* key | ||
* cert | ||
* ca | ||
* ciphers | ||
* honorCipherOrder | ||
* Anything else supported by [`tls.createSecureContext`](https://nodejs.org/dist/latest-v8.x/docs/api/tls.html#tls_tls_createsecurecontext_options) | ||
## NOTE on Security | ||
@@ -106,0 +162,0 @@ Inspired by [`iojs`][iojs] and a well written [article][article], we have defaulted |
@@ -10,8 +10,15 @@ /* | ||
fs = require('fs'), | ||
url = require('url'), | ||
http = require('http'), | ||
https = require('https'), | ||
{ promisify } = require('util'), | ||
test = require('tape'), | ||
sinon = require('sinon'), | ||
evilDNS = require('evil-dns'), | ||
createServers = require('../'); | ||
const createServersAsync = promisify(createServers); | ||
const ca = fs.readFileSync(path.join(__dirname, './fixtures/example-ca-cert.pem')); | ||
// | ||
@@ -24,2 +31,25 @@ // Immediately end a response. | ||
// | ||
// Request and download response from a URL | ||
// | ||
async function download(httpsURL) { | ||
return new Promise((resolve, reject) => { | ||
const req = https.get({ | ||
...url.parse(httpsURL), | ||
ca | ||
}, res => { | ||
const chunks = []; | ||
res | ||
.on('data', chunk => chunks.push(chunk)) | ||
.once('end', () => { | ||
resolve(chunks.map(chunk => chunk.toString('utf8')).join('')); | ||
}) | ||
.once('aborted', reject) | ||
.once('close', reject) | ||
.once('error', reject) | ||
}); | ||
req.once('error', reject); | ||
}); | ||
} | ||
test('only http', function (t) { | ||
@@ -326,1 +356,56 @@ t.plan(3); | ||
}); | ||
test('supports SNI', async t => { | ||
t.plan(1); | ||
const hostNames = [ | ||
'example.com', | ||
'example.net', | ||
'foo.example.org', | ||
]; | ||
let httpsServer; | ||
try { | ||
const servers = await createServersAsync({ | ||
https: { | ||
port: 3456, | ||
root: path.join(__dirname, 'fixtures'), | ||
sni: { | ||
'example.com': { | ||
key: 'example-com-key.pem', | ||
cert: 'example-com-cert.pem' | ||
}, | ||
'example.net': { | ||
key: 'example-net-key.pem', | ||
cert: 'example-net-cert.pem' | ||
}, | ||
'*.example.org': { | ||
key: 'example-org-key.pem', | ||
cert: 'example-org-cert.pem' | ||
} | ||
} | ||
}, | ||
handler: (req, res) => { | ||
res.write('Hello'); | ||
res.end(); | ||
} | ||
}); | ||
httpsServer = servers.https; | ||
hostNames.forEach(host => evilDNS.add(host, '0.0.0.0')); | ||
const responses = await Promise.all(hostNames | ||
.map(hostname => download(`https://${hostname}:3456/`))); | ||
t.equals( | ||
responses.every(str => str === 'Hello'), | ||
true, | ||
'responses are as expected'); | ||
} catch (err) { | ||
return void t.error(err); | ||
} finally { | ||
httpsServer && httpsServer.close(); | ||
evilDNS.clear(); | ||
} | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
49301
23
634
285
3
5