You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

998 lines
28 KiB

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