Server.js 30 KB

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