Server.js 30 KB

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