diff --git a/lib/Server.js b/lib/Server.js index 050a6c7fbd..2be6a5fdbc 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -1,44 +1,33 @@ 'use strict'; /* eslint-disable - import/order, no-shadow, no-undefined, func-names */ const fs = require('fs'); const path = require('path'); - -const ip = require('ip'); const tls = require('tls'); const url = require('url'); const http = require('http'); const https = require('https'); +const ip = require('ip'); const sockjs = require('sockjs'); - const semver = require('semver'); - const killable = require('killable'); - -const del = require('del'); const chokidar = require('chokidar'); - const express = require('express'); - const httpProxyMiddleware = require('http-proxy-middleware'); const historyApiFallback = require('connect-history-api-fallback'); const compress = require('compression'); const serveIndex = require('serve-index'); - const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); - +const validateOptions = require('schema-utils'); const updateCompiler = require('./utils/updateCompiler'); const createLogger = require('./utils/createLogger'); -const createCertificate = require('./utils/createCertificate'); +const getCertificate = require('./utils/getCertificate'); const routes = require('./utils/routes'); - -const validateOptions = require('schema-utils'); const schema = require('./options.json'); // Workaround for sockjs@~0.3.19 @@ -96,43 +85,41 @@ class Server { this.log = _log || createLogger(options); - // if the user enables http2, we can safely enable https - if (options.http2 && !options.https) { - options.https = true; - } - this.originalStats = - options.stats && Object.keys(options.stats).length ? options.stats : {}; + this.options.stats && Object.keys(this.options.stats).length + ? this.options.stats + : {}; - this.hot = options.hot || options.hotOnly; - this.headers = options.headers; - this.progress = options.progress; + this.sockets = []; + this.contentBaseWatchers = []; - this.serveIndex = options.serveIndex; + // TODO this. is deprecated (remove them in next major release.) in favor this.options. + this.hot = this.options.hot || this.options.hotOnly; + this.headers = this.options.headers; + this.progress = this.options.progress; - this.clientOverlay = options.overlay; - this.clientLogLevel = options.clientLogLevel; + this.serveIndex = this.options.serveIndex; - this.publicHost = options.public; - this.allowedHosts = options.allowedHosts; - this.disableHostCheck = !!options.disableHostCheck; + this.clientOverlay = this.options.overlay; + this.clientLogLevel = this.options.clientLogLevel; - this.sockets = []; + this.publicHost = this.options.public; + this.allowedHosts = this.options.allowedHosts; + this.disableHostCheck = !!this.options.disableHostCheck; - if (!options.watchOptions) { - options.watchOptions = {}; + if (!this.options.watchOptions) { + this.options.watchOptions = {}; } - // ignoring node_modules folder by default - options.watchOptions.ignored = options.watchOptions.ignored || [ + // Ignoring node_modules folder by default + this.options.watchOptions.ignored = this.options.watchOptions.ignored || [ /node_modules/, ]; - this.watchOptions = options.watchOptions; + this.watchOptions = this.options.watchOptions; - this.contentBaseWatchers = []; // Replace leading and trailing slashes to normalize path this.sockPath = `/${ - options.sockPath - ? options.sockPath.replace(/^\/|\/$/g, '') + this.options.sockPath + ? this.options.sockPath.replace(/^\/|\/$/g, '') : 'sockjs-node' }`; @@ -146,28 +133,33 @@ class Server { this.setupDevMiddleware(); // set express routes - routes(this.app, this.middleware, options); + routes(this.app, this.middleware, this.options); // Keep track of websocket proxies for external websocket upgrade. this.websocketProxies = []; this.setupFeatures(); - if (options.https) { + // if the user enables http2, we can safely enable https + if (this.options.http2 && !this.options.https) { + this.options.https = true; + } + + if (this.options.https) { // for keep supporting CLI parameters - if (typeof options.https === 'boolean') { - options.https = { - ca: options.ca, - pfx: options.pfx, - key: options.key, - cert: options.cert, - passphrase: options.pfxPassphrase, - requestCert: options.requestCert || false, + if (typeof this.options.https === 'boolean') { + this.options.https = { + ca: this.options.ca, + pfx: this.options.pfx, + key: this.options.key, + cert: this.options.cert, + passphrase: this.options.pfxPassphrase, + requestCert: this.options.requestCert || false, }; } for (const property of ['ca', 'pfx', 'key', 'cert']) { - const value = options.https[property]; + const value = this.options.https[property]; const isBuffer = value instanceof Buffer; if (value && !isBuffer) { @@ -181,65 +173,30 @@ class Server { if (stats) { // It is file - options.https[property] = fs.readFileSync(path.resolve(value)); + this.options.https[property] = fs.readFileSync(path.resolve(value)); } else { - options.https[property] = value; + this.options.https[property] = value; } } } let fakeCert; - if (!options.https.key || !options.https.cert) { - // Use a self-signed certificate if no certificate was configured. - // Cycle certs every 24 hours - const certPath = path.join(__dirname, '../ssl/server.pem'); - - let certExists = fs.existsSync(certPath); - - if (certExists) { - const certTtl = 1000 * 60 * 60 * 24; - const certStat = fs.statSync(certPath); - - const now = new Date(); - - // cert is more than 30 days old, kill it with fire - if ((now - certStat.ctime) / certTtl > 30) { - this.log.info( - 'SSL Certificate is more than 30 days old. Removing.' - ); - - del.sync([certPath], { force: true }); - - certExists = false; - } - } - - if (!certExists) { - this.log.info('Generating SSL Certificate'); - - const attrs = [{ name: 'commonName', value: 'localhost' }]; - const pems = createCertificate(attrs); - - fs.writeFileSync(certPath, pems.private + pems.cert, { - encoding: 'utf8', - }); - } - - fakeCert = fs.readFileSync(certPath); + if (!this.options.https.key || !this.options.https.cert) { + fakeCert = getCertificate(this.log); } - options.https.key = options.https.key || fakeCert; - options.https.cert = options.https.cert || fakeCert; + this.options.https.key = this.options.https.key || fakeCert; + this.options.https.cert = this.options.https.cert || fakeCert; // Only prevent HTTP/2 if http2 is explicitly set to false - const isHttp2 = options.http2 !== false; + const isHttp2 = this.options.http2 !== false; // note that options.spdy never existed. The user was able // to set options.https.spdy before, though it was not in the // docs. Keep options.https.spdy if the user sets it for // backwards compatability, but log a deprecation warning. - if (options.https.spdy) { + if (this.options.https.spdy) { // for backwards compatability: if options.https.spdy was passed in before, // it was not altered in any way this.log.warn( @@ -247,7 +204,7 @@ class Server { ); } else { // if the normal https server gets this option, it will not affect it. - options.https.spdy = { + this.options.https.spdy = { protocols: ['h2', 'http/1.1'], }; } @@ -262,21 +219,21 @@ class Server { // - https://github.com/webpack/webpack-dev-server/issues/1449 // - https://github.com/expressjs/express/issues/3388 if (semver.gte(process.version, '10.0.0') || !isHttp2) { - if (options.http2) { + if (this.options.http2) { // the user explicitly requested http2 but is not getting it because // of the node version. this.log.warn( 'HTTP/2 is currently unsupported for Node 10.0.0 and above, but will be supported once Express supports it' ); } - this.listeningApp = https.createServer(options.https, this.app); + this.listeningApp = https.createServer(this.options.https, this.app); } else { /* eslint-disable global-require */ // The relevant issues are: // https://github.com/spdy-http2/node-spdy/issues/350 // https://github.com/webpack/webpack-dev-server/issues/1592 this.listeningApp = require('spdy').createServer( - options.https, + this.options.https, this.app ); /* eslint-enable global-require */ @@ -941,7 +898,7 @@ class Server { ? this.watchOptions.poll : undefined; - const options = { + const watchOptions = { ignoreInitial: true, persistent: true, followSymlinks: false, @@ -953,7 +910,7 @@ class Server { interval, }; - const watcher = chokidar.watch(watchPath, options); + const watcher = chokidar.watch(watchPath, watchOptions); watcher.on('change', () => { this.sockWrite(this.sockets, 'content-changed'); diff --git a/lib/utils/createCertificate.js b/lib/utils/createCertificate.js index 4060373a68..c58fc05d0d 100644 --- a/lib/utils/createCertificate.js +++ b/lib/utils/createCertificate.js @@ -2,8 +2,8 @@ const selfsigned = require('selfsigned'); -function createCertificate(attrs) { - return selfsigned.generate(attrs, { +function createCertificate(attributes) { + return selfsigned.generate(attributes, { algorithm: 'sha256', days: 30, keySize: 2048, diff --git a/lib/utils/getCertificate.js b/lib/utils/getCertificate.js new file mode 100644 index 0000000000..6b12451389 --- /dev/null +++ b/lib/utils/getCertificate.js @@ -0,0 +1,45 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const del = require('del'); +const createCertificate = require('./createCertificate'); + +function getCertificate(logger) { + // Use a self-signed certificate if no certificate was configured. + // Cycle certs every 24 hours + const certificatePath = path.join(__dirname, '../../ssl/server.pem'); + + let certificateExists = fs.existsSync(certificatePath); + + if (certificateExists) { + const certificateTtl = 1000 * 60 * 60 * 24; + const certificateStat = fs.statSync(certificatePath); + + const now = new Date(); + + // cert is more than 30 days old, kill it with fire + if ((now - certificateStat.ctime) / certificateTtl > 30) { + logger.info('SSL Certificate is more than 30 days old. Removing.'); + + del.sync([certificatePath], { force: true }); + + certificateExists = false; + } + } + + if (!certificateExists) { + logger.info('Generating SSL Certificate'); + + const attributes = [{ name: 'commonName', value: 'localhost' }]; + const pems = createCertificate(attributes); + + fs.writeFileSync(certificatePath, pems.private + pems.cert, { + encoding: 'utf8', + }); + } + + return fs.readFileSync(certificatePath); +} + +module.exports = getCertificate;