Server.js 29 KB

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