UNPKG

30.2 kBJavaScriptView Raw
1'use strict';
2
3/* eslint-disable
4 no-shadow,
5 no-undefined,
6 func-names
7*/
8const fs = require('fs');
9const path = require('path');
10const tls = require('tls');
11const url = require('url');
12const http = require('http');
13const https = require('https');
14const ip = require('ip');
15const semver = require('semver');
16const killable = require('killable');
17const chokidar = require('chokidar');
18const express = require('express');
19const httpProxyMiddleware = require('http-proxy-middleware');
20const historyApiFallback = require('connect-history-api-fallback');
21const compress = require('compression');
22const serveIndex = require('serve-index');
23const webpack = require('webpack');
24const webpackDevMiddleware = require('webpack-dev-middleware');
25const validateOptions = require('schema-utils');
26const isAbsoluteUrl = require('is-absolute-url');
27const normalizeOptions = require('./utils/normalizeOptions');
28const updateCompiler = require('./utils/updateCompiler');
29const createLogger = require('./utils/createLogger');
30const getCertificate = require('./utils/getCertificate');
31const status = require('./utils/status');
32const createDomain = require('./utils/createDomain');
33const runBonjour = require('./utils/runBonjour');
34const routes = require('./utils/routes');
35const getSocketServerImplementation = require('./utils/getSocketServerImplementation');
36const schema = require('./options.json');
37
38// Workaround for node ^8.6.0, ^9.0.0
39// DEFAULT_ECDH_CURVE is default to prime256v1 in these version
40// breaking connection when certificate is not signed with prime256v1
41// change it to auto allows OpenSSL to select the curve automatically
42// See https://github.com/nodejs/node/issues/16196 for more information
43if (semver.satisfies(process.version, '8.6.0 - 9')) {
44 tls.DEFAULT_ECDH_CURVE = 'auto';
45}
46
47if (!process.env.WEBPACK_DEV_SERVER) {
48 process.env.WEBPACK_DEV_SERVER = true;
49}
50
51class Server {
52 constructor(compiler, options = {}, _log) {
53 if (options.lazy && !options.filename) {
54 throw new Error("'filename' option must be set in lazy mode.");
55 }
56
57 validateOptions(schema, options, 'webpack Dev Server');
58
59 this.compiler = compiler;
60 this.options = options;
61
62 this.log = _log || createLogger(options);
63
64 if (this.options.transportMode !== undefined) {
65 this.log.warn(
66 'transportMode is an experimental option, meaning its usage could potentially change without warning'
67 );
68 }
69
70 normalizeOptions(this.compiler, this.options);
71
72 updateCompiler(this.compiler, this.options);
73
74 this.heartbeatInterval = 30000;
75 // this.SocketServerImplementation is a class, so it must be instantiated before use
76 this.socketServerImplementation = getSocketServerImplementation(
77 this.options
78 );
79
80 this.originalStats =
81 this.options.stats && Object.keys(this.options.stats).length
82 ? this.options.stats
83 : {};
84
85 this.sockets = [];
86 this.contentBaseWatchers = [];
87
88 // TODO this.<property> is deprecated (remove them in next major release.) in favor this.options.<property>
89 this.hot = this.options.hot || this.options.hotOnly;
90 this.headers = this.options.headers;
91 this.progress = this.options.progress;
92
93 this.serveIndex = this.options.serveIndex;
94
95 this.clientOverlay = this.options.overlay;
96 this.clientLogLevel = this.options.clientLogLevel;
97
98 this.publicHost = this.options.public;
99 this.allowedHosts = this.options.allowedHosts;
100 this.disableHostCheck = !!this.options.disableHostCheck;
101
102 this.watchOptions = options.watchOptions || {};
103
104 // Replace leading and trailing slashes to normalize path
105 this.sockPath = `/${
106 this.options.sockPath
107 ? this.options.sockPath.replace(/^\/|\/$/g, '')
108 : 'sockjs-node'
109 }`;
110
111 if (this.progress) {
112 this.setupProgressPlugin();
113 }
114
115 this.setupHooks();
116 this.setupApp();
117 this.setupCheckHostRoute();
118 this.setupDevMiddleware();
119
120 // set express routes
121 routes(this);
122
123 // Keep track of websocket proxies for external websocket upgrade.
124 this.websocketProxies = [];
125
126 this.setupFeatures();
127 this.setupHttps();
128 this.createServer();
129
130 killable(this.listeningApp);
131
132 // Proxy websockets without the initial http request
133 // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
134 this.websocketProxies.forEach(function(wsProxy) {
135 this.listeningApp.on('upgrade', wsProxy.upgrade);
136 }, this);
137 }
138
139 setupProgressPlugin() {
140 // for CLI output
141 new webpack.ProgressPlugin({
142 profile: !!this.options.profile,
143 }).apply(this.compiler);
144
145 // for browser console output
146 new webpack.ProgressPlugin((percent, msg, addInfo) => {
147 percent = Math.floor(percent * 100);
148
149 if (percent === 100) {
150 msg = 'Compilation completed';
151 }
152
153 if (addInfo) {
154 msg = `${msg} (${addInfo})`;
155 }
156
157 this.sockWrite(this.sockets, 'progress-update', { percent, msg });
158
159 if (this.listeningApp) {
160 this.listeningApp.emit('progress-update', { percent, msg });
161 }
162 }).apply(this.compiler);
163 }
164
165 setupApp() {
166 // Init express server
167 // eslint-disable-next-line new-cap
168 this.app = new express();
169 }
170
171 setupHooks() {
172 // Listening for events
173 const invalidPlugin = () => {
174 this.sockWrite(this.sockets, 'invalid');
175 };
176
177 const addHooks = (compiler) => {
178 const { compile, invalid, done } = compiler.hooks;
179
180 compile.tap('webpack-dev-server', invalidPlugin);
181 invalid.tap('webpack-dev-server', invalidPlugin);
182 done.tap('webpack-dev-server', (stats) => {
183 this._sendStats(this.sockets, this.getStats(stats));
184 this._stats = stats;
185 });
186 };
187
188 if (this.compiler.compilers) {
189 this.compiler.compilers.forEach(addHooks);
190 } else {
191 addHooks(this.compiler);
192 }
193 }
194
195 setupCheckHostRoute() {
196 this.app.all('*', (req, res, next) => {
197 if (this.checkHost(req.headers)) {
198 return next();
199 }
200
201 res.send('Invalid Host header');
202 });
203 }
204
205 setupDevMiddleware() {
206 // middleware for serving webpack bundle
207 this.middleware = webpackDevMiddleware(
208 this.compiler,
209 Object.assign({}, this.options, { logLevel: this.log.options.level })
210 );
211 }
212
213 setupCompressFeature() {
214 this.app.use(compress());
215 }
216
217 setupProxyFeature() {
218 /**
219 * Assume a proxy configuration specified as:
220 * proxy: {
221 * 'context': { options }
222 * }
223 * OR
224 * proxy: {
225 * 'context': 'target'
226 * }
227 */
228 if (!Array.isArray(this.options.proxy)) {
229 if (Object.prototype.hasOwnProperty.call(this.options.proxy, 'target')) {
230 this.options.proxy = [this.options.proxy];
231 } else {
232 this.options.proxy = Object.keys(this.options.proxy).map((context) => {
233 let proxyOptions;
234 // For backwards compatibility reasons.
235 const correctedContext = context
236 .replace(/^\*$/, '**')
237 .replace(/\/\*$/, '');
238
239 if (typeof this.options.proxy[context] === 'string') {
240 proxyOptions = {
241 context: correctedContext,
242 target: this.options.proxy[context],
243 };
244 } else {
245 proxyOptions = Object.assign({}, this.options.proxy[context]);
246 proxyOptions.context = correctedContext;
247 }
248
249 proxyOptions.logLevel = proxyOptions.logLevel || 'warn';
250
251 return proxyOptions;
252 });
253 }
254 }
255
256 const getProxyMiddleware = (proxyConfig) => {
257 const context = proxyConfig.context || proxyConfig.path;
258
259 // It is possible to use the `bypass` method without a `target`.
260 // However, the proxy middleware has no use in this case, and will fail to instantiate.
261 if (proxyConfig.target) {
262 return httpProxyMiddleware(context, proxyConfig);
263 }
264 };
265 /**
266 * Assume a proxy configuration specified as:
267 * proxy: [
268 * {
269 * context: ...,
270 * ...options...
271 * },
272 * // or:
273 * function() {
274 * return {
275 * context: ...,
276 * ...options...
277 * };
278 * }
279 * ]
280 */
281 this.options.proxy.forEach((proxyConfigOrCallback) => {
282 let proxyMiddleware;
283
284 let proxyConfig =
285 typeof proxyConfigOrCallback === 'function'
286 ? proxyConfigOrCallback()
287 : proxyConfigOrCallback;
288
289 proxyMiddleware = getProxyMiddleware(proxyConfig);
290
291 if (proxyConfig.ws) {
292 this.websocketProxies.push(proxyMiddleware);
293 }
294
295 const handle = (req, res, next) => {
296 if (typeof proxyConfigOrCallback === 'function') {
297 const newProxyConfig = proxyConfigOrCallback();
298
299 if (newProxyConfig !== proxyConfig) {
300 proxyConfig = newProxyConfig;
301 proxyMiddleware = getProxyMiddleware(proxyConfig);
302 }
303 }
304
305 // - Check if we have a bypass function defined
306 // - In case the bypass function is defined we'll retrieve the
307 // bypassUrl from it otherwise bypassUrl would be null
308 const isByPassFuncDefined = typeof proxyConfig.bypass === 'function';
309 const bypassUrl = isByPassFuncDefined
310 ? proxyConfig.bypass(req, res, proxyConfig)
311 : null;
312
313 if (typeof bypassUrl === 'boolean') {
314 // skip the proxy
315 req.url = null;
316 next();
317 } else if (typeof bypassUrl === 'string') {
318 // byPass to that url
319 req.url = bypassUrl;
320 next();
321 } else if (proxyMiddleware) {
322 return proxyMiddleware(req, res, next);
323 } else {
324 next();
325 }
326 };
327
328 this.app.use(handle);
329 // Also forward error requests to the proxy so it can handle them.
330 this.app.use((error, req, res, next) => handle(req, res, next));
331 });
332 }
333
334 setupHistoryApiFallbackFeature() {
335 const fallback =
336 typeof this.options.historyApiFallback === 'object'
337 ? this.options.historyApiFallback
338 : null;
339
340 // Fall back to /index.html if nothing else matches.
341 this.app.use(historyApiFallback(fallback));
342 }
343
344 setupStaticFeature() {
345 const contentBase = this.options.contentBase;
346 const contentBasePublicPath = this.options.contentBasePublicPath;
347
348 if (Array.isArray(contentBase)) {
349 contentBase.forEach((item, index) => {
350 let publicPath = contentBasePublicPath;
351
352 if (
353 Array.isArray(contentBasePublicPath) &&
354 contentBasePublicPath[index]
355 ) {
356 publicPath = contentBasePublicPath[index] || contentBasePublicPath[0];
357 }
358
359 this.app.use(publicPath, express.static(item));
360 });
361 } else if (isAbsoluteUrl(String(contentBase))) {
362 this.log.warn(
363 'Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
364 );
365
366 this.log.warn(
367 'proxy: {\n\t"*": "<your current contentBase configuration>"\n}'
368 );
369
370 // Redirect every request to contentBase
371 this.app.get('*', (req, res) => {
372 res.writeHead(302, {
373 Location: contentBase + req.path + (req._parsedUrl.search || ''),
374 });
375
376 res.end();
377 });
378 } else if (typeof contentBase === 'number') {
379 this.log.warn(
380 'Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.'
381 );
382
383 this.log.warn(
384 'proxy: {\n\t"*": "//localhost:<your current contentBase configuration>"\n}'
385 );
386
387 // Redirect every request to the port contentBase
388 this.app.get('*', (req, res) => {
389 res.writeHead(302, {
390 Location: `//localhost:${contentBase}${req.path}${req._parsedUrl
391 .search || ''}`,
392 });
393
394 res.end();
395 });
396 } else {
397 // route content request
398 this.app.use(
399 contentBasePublicPath,
400 express.static(contentBase, this.options.staticOptions)
401 );
402 }
403 }
404
405 setupServeIndexFeature() {
406 const contentBase = this.options.contentBase;
407 const contentBasePublicPath = this.options.contentBasePublicPath;
408
409 if (Array.isArray(contentBase)) {
410 contentBase.forEach((item) => {
411 this.app.use(contentBasePublicPath, (req, res, next) => {
412 // serve-index doesn't fallthrough non-get/head request to next middleware
413 if (req.method !== 'GET' && req.method !== 'HEAD') {
414 return next();
415 }
416
417 serveIndex(item, { icons: true })(req, res, next);
418 });
419 });
420 } else if (
421 typeof contentBase !== 'number' &&
422 !isAbsoluteUrl(String(contentBase))
423 ) {
424 this.app.use(contentBasePublicPath, (req, res, next) => {
425 // serve-index doesn't fallthrough non-get/head request to next middleware
426 if (req.method !== 'GET' && req.method !== 'HEAD') {
427 return next();
428 }
429
430 serveIndex(contentBase, { icons: true })(req, res, next);
431 });
432 }
433 }
434
435 setupWatchStaticFeature() {
436 const contentBase = this.options.contentBase;
437
438 if (isAbsoluteUrl(String(contentBase)) || typeof contentBase === 'number') {
439 throw new Error('Watching remote files is not supported.');
440 } else if (Array.isArray(contentBase)) {
441 contentBase.forEach((item) => {
442 if (isAbsoluteUrl(String(item)) || typeof item === 'number') {
443 throw new Error('Watching remote files is not supported.');
444 }
445 this._watch(item);
446 });
447 } else {
448 this._watch(contentBase);
449 }
450 }
451
452 setupBeforeFeature() {
453 // Todo rename onBeforeSetupMiddleware in next major release
454 // Todo pass only `this` argument
455 this.options.before(this.app, this, this.compiler);
456 }
457
458 setupMiddleware() {
459 this.app.use(this.middleware);
460 }
461
462 setupAfterFeature() {
463 // Todo rename onAfterSetupMiddleware in next major release
464 // Todo pass only `this` argument
465 this.options.after(this.app, this, this.compiler);
466 }
467
468 setupHeadersFeature() {
469 this.app.all('*', this.setContentHeaders.bind(this));
470 }
471
472 setupMagicHtmlFeature() {
473 this.app.get('*', this.serveMagicHtml.bind(this));
474 }
475
476 setupSetupFeature() {
477 this.log.warn(
478 'The `setup` option is deprecated and will be removed in v4. Please update your config to use `before`'
479 );
480
481 this.options.setup(this.app, this);
482 }
483
484 setupFeatures() {
485 const features = {
486 compress: () => {
487 if (this.options.compress) {
488 this.setupCompressFeature();
489 }
490 },
491 proxy: () => {
492 if (this.options.proxy) {
493 this.setupProxyFeature();
494 }
495 },
496 historyApiFallback: () => {
497 if (this.options.historyApiFallback) {
498 this.setupHistoryApiFallbackFeature();
499 }
500 },
501 // Todo rename to `static` in future major release
502 contentBaseFiles: () => {
503 this.setupStaticFeature();
504 },
505 // Todo rename to `serveIndex` in future major release
506 contentBaseIndex: () => {
507 this.setupServeIndexFeature();
508 },
509 // Todo rename to `watchStatic` in future major release
510 watchContentBase: () => {
511 this.setupWatchStaticFeature();
512 },
513 before: () => {
514 if (typeof this.options.before === 'function') {
515 this.setupBeforeFeature();
516 }
517 },
518 middleware: () => {
519 // include our middleware to ensure
520 // it is able to handle '/index.html' request after redirect
521 this.setupMiddleware();
522 },
523 after: () => {
524 if (typeof this.options.after === 'function') {
525 this.setupAfterFeature();
526 }
527 },
528 headers: () => {
529 this.setupHeadersFeature();
530 },
531 magicHtml: () => {
532 this.setupMagicHtmlFeature();
533 },
534 setup: () => {
535 if (typeof this.options.setup === 'function') {
536 this.setupSetupFeature();
537 }
538 },
539 };
540
541 const runnableFeatures = [];
542
543 // compress is placed last and uses unshift so that it will be the first middleware used
544 if (this.options.compress) {
545 runnableFeatures.push('compress');
546 }
547
548 runnableFeatures.push('setup', 'before', 'headers', 'middleware');
549
550 if (this.options.proxy) {
551 runnableFeatures.push('proxy', 'middleware');
552 }
553
554 if (this.options.contentBase !== false) {
555 runnableFeatures.push('contentBaseFiles');
556 }
557
558 if (this.options.historyApiFallback) {
559 runnableFeatures.push('historyApiFallback', 'middleware');
560
561 if (this.options.contentBase !== false) {
562 runnableFeatures.push('contentBaseFiles');
563 }
564 }
565
566 // checking if it's set to true or not set (Default : undefined => true)
567 this.serveIndex = this.serveIndex || this.serveIndex === undefined;
568
569 if (this.options.contentBase && this.serveIndex) {
570 runnableFeatures.push('contentBaseIndex');
571 }
572
573 if (this.options.watchContentBase) {
574 runnableFeatures.push('watchContentBase');
575 }
576
577 runnableFeatures.push('magicHtml');
578
579 if (this.options.after) {
580 runnableFeatures.push('after');
581 }
582
583 (this.options.features || runnableFeatures).forEach((feature) => {
584 features[feature]();
585 });
586 }
587
588 setupHttps() {
589 // if the user enables http2, we can safely enable https
590 if (this.options.http2 && !this.options.https) {
591 this.options.https = true;
592 }
593
594 if (this.options.https) {
595 // for keep supporting CLI parameters
596 if (typeof this.options.https === 'boolean') {
597 this.options.https = {
598 ca: this.options.ca,
599 pfx: this.options.pfx,
600 key: this.options.key,
601 cert: this.options.cert,
602 passphrase: this.options.pfxPassphrase,
603 requestCert: this.options.requestCert || false,
604 };
605 }
606
607 for (const property of ['ca', 'pfx', 'key', 'cert']) {
608 const value = this.options.https[property];
609 const isBuffer = value instanceof Buffer;
610
611 if (value && !isBuffer) {
612 let stats = null;
613
614 try {
615 stats = fs.lstatSync(fs.realpathSync(value)).isFile();
616 } catch (error) {
617 // ignore error
618 }
619
620 // It is file
621 this.options.https[property] = stats
622 ? fs.readFileSync(path.resolve(value))
623 : value;
624 }
625 }
626
627 let fakeCert;
628
629 if (!this.options.https.key || !this.options.https.cert) {
630 fakeCert = getCertificate(this.log);
631 }
632
633 this.options.https.key = this.options.https.key || fakeCert;
634 this.options.https.cert = this.options.https.cert || fakeCert;
635
636 // note that options.spdy never existed. The user was able
637 // to set options.https.spdy before, though it was not in the
638 // docs. Keep options.https.spdy if the user sets it for
639 // backwards compatibility, but log a deprecation warning.
640 if (this.options.https.spdy) {
641 // for backwards compatibility: if options.https.spdy was passed in before,
642 // it was not altered in any way
643 this.log.warn(
644 'Providing custom spdy server options is deprecated and will be removed in the next major version.'
645 );
646 } else {
647 // if the normal https server gets this option, it will not affect it.
648 this.options.https.spdy = {
649 protocols: ['h2', 'http/1.1'],
650 };
651 }
652 }
653 }
654
655 createServer() {
656 if (this.options.https) {
657 // Only prevent HTTP/2 if http2 is explicitly set to false
658 const isHttp2 = this.options.http2 !== false;
659
660 // `spdy` is effectively unmaintained, and as a consequence of an
661 // implementation that extensively relies on Node’s non-public APIs, broken
662 // on Node 10 and above. In those cases, only https will be used for now.
663 // Once express supports Node's built-in HTTP/2 support, migrating over to
664 // that should be the best way to go.
665 // The relevant issues are:
666 // - https://github.com/nodejs/node/issues/21665
667 // - https://github.com/webpack/webpack-dev-server/issues/1449
668 // - https://github.com/expressjs/express/issues/3388
669 if (semver.gte(process.version, '10.0.0') || !isHttp2) {
670 if (this.options.http2) {
671 // the user explicitly requested http2 but is not getting it because
672 // of the node version.
673 this.log.warn(
674 'HTTP/2 is currently unsupported for Node 10.0.0 and above, but will be supported once Express supports it'
675 );
676 }
677 this.listeningApp = https.createServer(this.options.https, this.app);
678 } else {
679 // The relevant issues are:
680 // https://github.com/spdy-http2/node-spdy/issues/350
681 // https://github.com/webpack/webpack-dev-server/issues/1592
682 this.listeningApp = require('spdy').createServer(
683 this.options.https,
684 this.app
685 );
686 }
687 } else {
688 this.listeningApp = http.createServer(this.app);
689 }
690
691 this.listeningApp.on('error', (err) => {
692 this.log.error(err);
693 });
694 }
695
696 createSocketServer() {
697 const SocketServerImplementation = this.socketServerImplementation;
698 this.socketServer = new SocketServerImplementation(this);
699
700 this.socketServer.onConnection((connection, headers) => {
701 if (!connection) {
702 return;
703 }
704
705 if (!headers) {
706 this.log.warn(
707 'transportMode.server implementation must pass headers to the callback of onConnection(f) ' +
708 'via f(connection, headers) in order for clients to pass a headers security check'
709 );
710 }
711
712 if (!headers || !this.checkHost(headers) || !this.checkOrigin(headers)) {
713 this.sockWrite([connection], 'error', 'Invalid Host/Origin header');
714
715 this.socketServer.close(connection);
716
717 return;
718 }
719
720 this.sockets.push(connection);
721
722 this.socketServer.onConnectionClose(connection, () => {
723 const idx = this.sockets.indexOf(connection);
724
725 if (idx >= 0) {
726 this.sockets.splice(idx, 1);
727 }
728 });
729
730 if (this.clientLogLevel) {
731 this.sockWrite([connection], 'log-level', this.clientLogLevel);
732 }
733
734 if (this.hot) {
735 this.sockWrite([connection], 'hot');
736 }
737
738 // TODO: change condition at major version
739 if (this.options.liveReload !== false) {
740 this.sockWrite([connection], 'liveReload', this.options.liveReload);
741 }
742
743 if (this.progress) {
744 this.sockWrite([connection], 'progress', this.progress);
745 }
746
747 if (this.clientOverlay) {
748 this.sockWrite([connection], 'overlay', this.clientOverlay);
749 }
750
751 if (!this._stats) {
752 return;
753 }
754
755 this._sendStats([connection], this.getStats(this._stats), true);
756 });
757 }
758
759 showStatus() {
760 const suffix =
761 this.options.inline !== false || this.options.lazy === true
762 ? '/'
763 : '/webpack-dev-server/';
764 const uri = `${createDomain(this.options, this.listeningApp)}${suffix}`;
765
766 status(
767 uri,
768 this.options,
769 this.log,
770 this.options.stats && this.options.stats.colors
771 );
772 }
773
774 listen(port, hostname, fn) {
775 this.hostname = hostname;
776
777 return this.listeningApp.listen(port, hostname, (err) => {
778 this.createSocketServer();
779
780 if (this.options.bonjour) {
781 runBonjour(this.options);
782 }
783
784 this.showStatus();
785
786 if (fn) {
787 fn.call(this.listeningApp, err);
788 }
789
790 if (typeof this.options.onListening === 'function') {
791 this.options.onListening(this);
792 }
793 });
794 }
795
796 close(cb) {
797 this.sockets.forEach((socket) => {
798 this.socketServer.close(socket);
799 });
800
801 this.sockets = [];
802
803 this.contentBaseWatchers.forEach((watcher) => {
804 watcher.close();
805 });
806
807 this.contentBaseWatchers = [];
808
809 this.listeningApp.kill(() => {
810 this.middleware.close(cb);
811 });
812 }
813
814 static get DEFAULT_STATS() {
815 return {
816 all: false,
817 hash: true,
818 assets: true,
819 warnings: true,
820 errors: true,
821 errorDetails: false,
822 };
823 }
824
825 getStats(statsObj) {
826 const stats = Server.DEFAULT_STATS;
827
828 if (this.originalStats.warningsFilter) {
829 stats.warningsFilter = this.originalStats.warningsFilter;
830 }
831
832 return statsObj.toJson(stats);
833 }
834
835 use() {
836 // eslint-disable-next-line
837 this.app.use.apply(this.app, arguments);
838 }
839
840 setContentHeaders(req, res, next) {
841 if (this.headers) {
842 // eslint-disable-next-line
843 for (const name in this.headers) {
844 res.setHeader(name, this.headers[name]);
845 }
846 }
847
848 next();
849 }
850
851 checkHost(headers) {
852 return this.checkHeaders(headers, 'host');
853 }
854
855 checkOrigin(headers) {
856 return this.checkHeaders(headers, 'origin');
857 }
858
859 checkHeaders(headers, headerToCheck) {
860 // allow user to opt-out this security check, at own risk
861 if (this.disableHostCheck) {
862 return true;
863 }
864
865 if (!headerToCheck) {
866 headerToCheck = 'host';
867 }
868
869 // get the Host header and extract hostname
870 // we don't care about port not matching
871 const hostHeader = headers[headerToCheck];
872
873 if (!hostHeader) {
874 return false;
875 }
876
877 // use the node url-parser to retrieve the hostname from the host-header.
878 const hostname = url.parse(
879 // if hostHeader doesn't have scheme, add // for parsing.
880 /^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
881 false,
882 true
883 ).hostname;
884 // always allow requests with explicit IPv4 or IPv6-address.
885 // A note on IPv6 addresses:
886 // hostHeader will always contain the brackets denoting
887 // an IPv6-address in URLs,
888 // these are removed from the hostname in url.parse(),
889 // so we have the pure IPv6-address in hostname.
890 // always allow localhost host, for convenience (hostname === 'localhost')
891 // allow hostname of listening address (hostname === this.hostname)
892 const isValidHostname =
893 ip.isV4Format(hostname) ||
894 ip.isV6Format(hostname) ||
895 hostname === 'localhost' ||
896 hostname === this.hostname;
897
898 if (isValidHostname) {
899 return true;
900 }
901 // always allow localhost host, for convenience
902 // allow if hostname is in allowedHosts
903 if (this.allowedHosts && this.allowedHosts.length) {
904 for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {
905 const allowedHost = this.allowedHosts[hostIdx];
906
907 if (allowedHost === hostname) {
908 return true;
909 }
910
911 // support "." as a subdomain wildcard
912 // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
913 if (allowedHost[0] === '.') {
914 // "example.com" (hostname === allowedHost.substring(1))
915 // "*.example.com" (hostname.endsWith(allowedHost))
916 if (
917 hostname === allowedHost.substring(1) ||
918 hostname.endsWith(allowedHost)
919 ) {
920 return true;
921 }
922 }
923 }
924 }
925
926 // also allow public hostname if provided
927 if (typeof this.publicHost === 'string') {
928 const idxPublic = this.publicHost.indexOf(':');
929 const publicHostname =
930 idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;
931
932 if (hostname === publicHostname) {
933 return true;
934 }
935 }
936
937 // disallow
938 return false;
939 }
940
941 // eslint-disable-next-line
942 sockWrite(sockets, type, data) {
943 sockets.forEach((socket) => {
944 this.socketServer.send(socket, JSON.stringify({ type, data }));
945 });
946 }
947
948 serveMagicHtml(req, res, next) {
949 const _path = req.path;
950
951 try {
952 const isFile = this.middleware.fileSystem
953 .statSync(this.middleware.getFilenameFromUrl(`${_path}.js`))
954 .isFile();
955
956 if (!isFile) {
957 return next();
958 }
959 // Serve a page that executes the javascript
960 const queries = req._parsedUrl.search || '';
961 const responsePage = `<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body><script type="text/javascript" charset="utf-8" src="${_path}.js${queries}"></script></body></html>`;
962 res.send(responsePage);
963 } catch (err) {
964 return next();
965 }
966 }
967
968 // send stats to a socket or multiple sockets
969 _sendStats(sockets, stats, force) {
970 const shouldEmit =
971 !force &&
972 stats &&
973 (!stats.errors || stats.errors.length === 0) &&
974 stats.assets &&
975 stats.assets.every((asset) => !asset.emitted);
976
977 if (shouldEmit) {
978 return this.sockWrite(sockets, 'still-ok');
979 }
980
981 this.sockWrite(sockets, 'hash', stats.hash);
982
983 if (stats.errors.length > 0) {
984 this.sockWrite(sockets, 'errors', stats.errors);
985 } else if (stats.warnings.length > 0) {
986 this.sockWrite(sockets, 'warnings', stats.warnings);
987 } else {
988 this.sockWrite(sockets, 'ok');
989 }
990 }
991
992 _watch(watchPath) {
993 // duplicate the same massaging of options that watchpack performs
994 // https://github.com/webpack/watchpack/blob/master/lib/DirectoryWatcher.js#L49
995 // this isn't an elegant solution, but we'll improve it in the future
996 const usePolling = this.watchOptions.poll ? true : undefined;
997 const interval =
998 typeof this.watchOptions.poll === 'number'
999 ? this.watchOptions.poll
1000 : undefined;
1001
1002 const watchOptions = {
1003 ignoreInitial: true,
1004 persistent: true,
1005 followSymlinks: false,
1006 atomic: false,
1007 alwaysStat: true,
1008 ignorePermissionErrors: true,
1009 ignored: this.watchOptions.ignored,
1010 usePolling,
1011 interval,
1012 };
1013
1014 const watcher = chokidar.watch(watchPath, watchOptions);
1015 // disabling refreshing on changing the content
1016 if (this.options.liveReload !== false) {
1017 watcher.on('change', () => {
1018 this.sockWrite(this.sockets, 'content-changed');
1019 });
1020 }
1021 this.contentBaseWatchers.push(watcher);
1022 }
1023
1024 invalidate(callback) {
1025 if (this.middleware) {
1026 this.middleware.invalidate(callback);
1027 }
1028 }
1029}
1030
1031// Export this logic,
1032// so that other implementations,
1033// like task-runners can use it
1034Server.addDevServerEntrypoints = require('./utils/addEntries');
1035
1036module.exports = Server;