diff --git a/README.md b/README.md index 1079c00..6b986a4 100644 --- a/README.md +++ b/README.md @@ -50,17 +50,21 @@ startCluster(options, () => { ## Options -| Param | Type | Description | -| ------------ | --------- | ---------------------------------------- | -| baseDir | `String` | directory of application | -| framework | `String` | specify framework that can be absolute path or npm package | -| plugins | `Object` | plugins for unittest | -| workers | `Number` | numbers of app workers | -| sticky | `Boolean` | sticky mode server | -| port | `Number` | port | -| https | `Object` | start a https server, note: `key` / `cert` should be full path to file | -| typescript | `Boolean` | enable loader's typescript support | -| require | `Array\|String` | will inject into worker/agent process | +| Param | Type | Description | +| ---------- | ---------------------- | ------------------------------------------------------------------------- | +| baseDir | `String` | directory of application | +| framework | `String` | specify framework that can be absolute path or npm package | +| plugins | `Object` | plugins for unittest | +| workers | `Number` | numbers of app workers | +| sticky | `Boolean` | sticky mode server | +| port | `Number` | port | +| https | `SecureContextOptions` | start a https server, note: `key\|cert\|ca` must be absolute path if file | +| typescript | `Boolean` | enable loader's typescript support | +| require | `Array\|String` | will inject into worker/agent process | + +## References + +- [SecureContextOptions](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) ## Env diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..647b353 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,22 @@ +import { SecureContextOptions } from 'tls'; + + +/** Cluster Options */ +export interface Options { + /** specify framework that can be absolute path or npm package */ + framework?: string; + /** directory of application, default to `process.cwd()` */ + baseDir?: string; + /** customized plugins, for unittest */ + plugins?: object | null; + /** numbers of app workers, default to `os.cpus().length` */ + workers?: number; + /** listening port, default to 7001(http) or 8443(https) */ + port?: number; + /** Ref: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options */ + https?: SecureContextOptions; + [prop: string]: any; +} + + +export function startCluster(options: Options, callback: () => void): void; diff --git a/lib/app_worker.js b/lib/app_worker.js index 4daf9ce..2f28520 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -9,17 +9,20 @@ if (options.require) { }); } -const fs = require('fs'); const debug = require('debug')('egg-cluster'); const gracefulExit = require('graceful-process'); const ConsoleLogger = require('egg-logger').EggConsoleLogger; const consoleLogger = new ConsoleLogger({ level: process.env.EGG_APP_WORKER_LOGGER_LEVEL }); +const tlsUtil = require('./utils/tls_options'); const Application = require(options.framework).Application; debug('new Application with options %j', options); + const app = new Application(options); const clusterConfig = app.config.cluster || /* istanbul ignore next */ {}; const listenConfig = clusterConfig.listen || /* istanbul ignore next */ {}; +let https = tlsUtil.mergeTLSOpts(options.https, listenConfig.https); +https = tlsUtil.parseTLSOpts(https); const port = options.port = options.port || listenConfig.port; process.send({ to: 'master', action: 'realport', data: port }); app.ready(startServer); @@ -40,16 +43,9 @@ function startServer(err) { app.removeListener('startTimeout', startTimeoutHandler); - let server; - if (options.https) { - const httpsOptions = Object.assign({}, options.https, { - key: fs.readFileSync(options.https.key), - cert: fs.readFileSync(options.https.cert), - }); - server = require('https').createServer(httpsOptions, app.callback()); - } else { - server = require('http').createServer(app.callback()); - } + const server = https && typeof https === 'object' + ? require('https').createServer(https, app.callback()) + : require('http').createServer(app.callback()); server.once('error', err => { consoleLogger.error('[app_worker] server got error: %s, code: %s', err.message, err.code); diff --git a/lib/utils/options.js b/lib/utils/options.js index 7842a97..fa92e90 100644 --- a/lib/utils/options.js +++ b/lib/utils/options.js @@ -5,8 +5,7 @@ const fs = require('fs'); const path = require('path'); const assert = require('assert'); const utils = require('egg-utils'); -const is = require('is-type-of'); -const deprecate = require('depd')('egg'); + module.exports = function(options) { const defaults = { @@ -35,20 +34,6 @@ module.exports = function(options) { assert(egg.Application, `should define Application in ${options.framework}`); assert(egg.Agent, `should define Agent in ${options.framework}`); - // https - if (options.https) { - if (is.boolean(options.https)) { - // TODO: compatible options.key, options.cert, will remove at next major - deprecate('[master] Please use `https: { key, cert }` instead of `https: true`'); - options.https = { - key: options.key, - cert: options.cert, - }; - } - assert(options.https.key && fs.existsSync(options.https.key), 'options.https.key should exists'); - assert(options.https.cert && fs.existsSync(options.https.cert), 'options.https.cert should exists'); - } - options.port = parseInt(options.port, 10) || undefined; options.workers = parseInt(options.workers, 10); if (options.require) options.require = [].concat(options.require); diff --git a/lib/utils/tls_options.js b/lib/utils/tls_options.js new file mode 100644 index 0000000..fe4eed4 --- /dev/null +++ b/lib/utils/tls_options.js @@ -0,0 +1,80 @@ +'use strict'; + +const fs = require('fs'); +const assert = require('assert'); + +/** + * Parse TLS SecureContextOptions + * + * @param {SecureContextOptions|void} options tls + * @return {SecureContextOptions|void} parsed options + */ +function parseTLSOpts(options) { + const msg = '[master] Deprecated: Please use `https: { key, cert }` instead of `https: true`. Docs: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options'; + assert(!(options === true), msg); + + if (!options) { + return; + } + const opts = Object.assign({}, options); + + /* istanbul ignore else */ + if (typeof opts.ca === 'string') { + assert(opts.ca && fs.existsSync(opts.ca), `File of https.ca should exists: "${opts.ca}"`); + opts.ca = fs.readFileSync(opts.ca); + } + /* istanbul ignore else */ + if (typeof opts.cert === 'string') { + assert(opts.cert && fs.existsSync(opts.cert), `File of https.cert should exists: "${opts.cert}"`); + opts.cert = fs.readFileSync(opts.cert); + } + /* istanbul ignore else */ + if (typeof opts.key === 'string') { + assert(opts.key && fs.existsSync(opts.key), `File of https.key should exists: "${opts.key}"`); + opts.key = fs.readFileSync(opts.key); + } + /* istanbul ignore else */ + if (typeof opts.pfx === 'string') { + assert(opts.pfx && fs.existsSync(opts.pfx), `File of https.pfx should exists: "${opts.pfx}"`); + opts.pfx = fs.readFileSync(opts.pfx); + } + + if (Object.keys(opts).length) { + return opts; + } +} + + +/** + * Merge TLS options. first param with higher priority + * + * @param {SecureContextOptions|void} optionsHttps from options + * @param {SecureContextOptions|void} listenHttps from listenConfig + * @return {SecureContextOptions|void} merged + */ +function mergeTLSOpts(optionsHttps, listenHttps) { + const ret = { }; + const msg = '[master] Deprecated: Please use `https: { key, cert }` instead of `https: true`. Docs: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options'; + + assert(!(optionsHttps === true), msg); + assert(!(listenHttps === true), msg); + + /* istanbul ignore else */ + if (listenHttps && typeof listenHttps === 'object') { + Object.assign(ret, listenHttps); + } + /* istanbul ignore else */ + if (optionsHttps && typeof optionsHttps === 'object') { + Object.assign(ret, optionsHttps); + } + + if (Object.keys(ret).length) { + return ret; + } +} + + +module.exports = { + mergeTLSOpts, + parseTLSOpts, +}; diff --git a/package.json b/package.json index 3511069..a0daa2b 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.20.0", "description": "cluster manager for egg", "main": "index.js", + "types": "index.d.ts", "scripts": { "autod": "autod", "lint": "eslint .", @@ -14,6 +15,7 @@ }, "files": [ "index.js", + "index.d.ts", "lib" ], "repository": { diff --git a/test/options.test.js b/test/options.test.js index a8c3d93..42e0ba2 100644 --- a/test/options.test.js +++ b/test/options.test.js @@ -15,11 +15,13 @@ describe('test/options.test.js', () => { assert(options.port === undefined); }); - it('should start with https and listen 8443', () => { + + it('should start with https and listen 8443 with https.{key|cert}', () => { const options = parseOptions({ - https: true, - key: utils.getFilepath('server.key'), - cert: utils.getFilepath('server.crt'), + https: { + key: utils.getFilepath('server.key'), + cert: utils.getFilepath('server.crt'), + }, }); assert(options.port === 8443); assert(options.https.key); diff --git a/test/tls_options.test.js b/test/tls_options.test.js new file mode 100644 index 0000000..aa5a45a --- /dev/null +++ b/test/tls_options.test.js @@ -0,0 +1,193 @@ +'use strict'; + +const readFileSync = require('fs').readFileSync; +const join = require('path').join; +const assert = require('assert'); +const tlsUtil = require('../lib/utils/tls_options'); +const mergeTLSOpts = tlsUtil.mergeTLSOpts; +const parseTLSOpts = tlsUtil.parseTLSOpts; + +describe('test/tls_options.test.js', () => { + describe('Should parseTLSOpts()', () => { + it('with https:true', () => { + assert.throws(() => parseTLSOpts(true)); + }); + + it('with https:false', () => { + const ret = parseTLSOpts(false); + assert(typeof ret === 'undefined'); + }); + + it('with https:{}', () => { + const ret = parseTLSOpts({}); + assert(typeof ret === 'undefined'); + }); + + it('with https:undefined', () => { + const ret = parseTLSOpts(); + assert(typeof ret === 'undefined'); + }); + + it('with invalid https.ca file path', () => { + const opts = { + ca: Math.random().toString(), + }; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.ca should exists: "${opts.ca}"`) + ); + + opts.ca = ''; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.ca should exists: "${opts.ca}"`) + ); + }); + + it('with invalid https.cert file path', () => { + const opts = { + cert: Math.random().toString(), + }; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.cert should exists: "${opts.cert}"`) + ); + + opts.cert = ''; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.cert should exists: "${opts.cert}"`) + ); + }); + + it('with invalid https.key file path', () => { + const opts = { + key: Math.random().toString(), + }; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.key should exists: "${opts.key}"`) + ); + + opts.key = ''; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.key should exists: "${opts.key}"`) + ); + }); + + it('with invalid https.pfx file path', () => { + const opts = { + pfx: Math.random().toString(), + }; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.pfx should exists: "${opts.pfx}"`) + ); + + opts.pfx = ''; + assert.throws( + () => parseTLSOpts(opts), + new RegExp(`File of https.pfx should exists: "${opts.pfx}"`) + ); + }); + + it('with https:key/cert by file path', () => { + const key = join(__dirname, 'fixtures/server.key'); + const cert = join(__dirname, 'fixtures/server.crt'); + const pfx = join(__dirname, 'fixtures/server.crt'); + const opts = { + key, + cert, + pfx, + }; + const ret = parseTLSOpts(opts); + + assert(ret && Object.keys(ret).length === 3); + assert(ret.key.toString() === readFileSync(key).toString()); + assert(ret.cert.toString() === readFileSync(cert).toString()); + assert(ret.pfx.toString() === readFileSync(cert).toString()); + }); + + it('with https:key/cert by Buffer', () => { + const key = join(__dirname, 'fixtures/server.key'); + const cert = join(__dirname, 'fixtures/server.crt'); + const opts = { + key: readFileSync(key), + cert: readFileSync(cert), + pfx: readFileSync(cert), + }; + const ret = parseTLSOpts(opts); + + assert(ret && Object.keys(ret).length === 3); + assert(ret.key.toString() === opts.key.toString()); + assert(ret.cert.toString() === opts.cert.toString()); + assert(ret.pfx.toString() === opts.pfx.toString()); + }); + }); + + + describe('Should mergeTLSOpts()', () => { + it('with both optionsHttps and listenHttps invalid', () => { + let ret = mergeTLSOpts({}, {}); + assert(typeof ret === 'undefined'); + + ret = mergeTLSOpts({}); + assert(typeof ret === 'undefined'); + + ret = mergeTLSOpts(); + assert(typeof ret === 'undefined'); + + assert.throws(() => mergeTLSOpts(true)); + assert.throws(() => mergeTLSOpts(true, true)); + }); + + it('with valid optionsHttps and invalid listenHttps', () => { + const opts = { + key: Math.random().toString(), + cert: Math.random().toString(), + }; + const ret = mergeTLSOpts(opts, {}); + + assert(ret && Object.keys(ret).length === 2); + assert(ret && ret.key === opts.key && ret.cert === opts.cert); + }); + + it('with invalid optionsHttps and valid listenHttps', () => { + const opts = { + key: Math.random().toString(), + cert: Math.random().toString(), + }; + let ret = mergeTLSOpts({}, opts); + assert(ret && Object.keys(ret).length === 2); + assert(ret && ret.key === opts.key && ret.cert === opts.cert); + + assert.throws(() => mergeTLSOpts(true, opts)); + + ret = mergeTLSOpts(false, opts); + assert(ret && Object.keys(ret).length === 2); + assert(ret && ret.key === opts.key && ret.cert === opts.cert); + }); + + it('with both optionsHttps and listenHttps valid', () => { + const optionsHttps = { + key: Math.random().toString(), + cert: Math.random().toString(), + }; + const listenHttps = { + key: Math.random().toString(), + cert: Math.random().toString(), + ca: Math.random().toString(), + }; + const ret = mergeTLSOpts(optionsHttps, listenHttps); + + assert(ret && Object.keys(ret).length === 3); + assert(ret + && ret.key === optionsHttps.key + && ret.cert === optionsHttps.cert + && ret.ca === listenHttps.ca + ); + }); + }); + +});