http-server.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. 'use strict';
  2. var fs = require('fs'),
  3. union = require('union'),
  4. httpServerCore = require('./core'),
  5. auth = require('basic-auth'),
  6. httpProxy = require('http-proxy'),
  7. corser = require('corser'),
  8. secureCompare = require('secure-compare');
  9. //
  10. // Remark: backwards compatibility for previous
  11. // case convention of HTTP
  12. //
  13. exports.HttpServer = exports.HTTPServer = HttpServer;
  14. /**
  15. * Returns a new instance of HttpServer with the
  16. * specified `options`.
  17. */
  18. exports.createServer = function (options) {
  19. return new HttpServer(options);
  20. };
  21. /**
  22. * Constructor function for the HttpServer object
  23. * which is responsible for serving static files along
  24. * with other HTTP-related features.
  25. */
  26. function HttpServer(options) {
  27. options = options || {};
  28. if (options.root) {
  29. this.root = options.root;
  30. } else {
  31. try {
  32. // eslint-disable-next-line no-sync
  33. fs.lstatSync('./public');
  34. this.root = './public';
  35. } catch (err) {
  36. this.root = './';
  37. }
  38. }
  39. this.headers = options.headers || {};
  40. this.headers['Accept-Ranges'] = 'bytes';
  41. this.cache = (
  42. // eslint-disable-next-line no-nested-ternary
  43. options.cache === undefined ? 3600 :
  44. // -1 is a special case to turn off caching.
  45. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Preventing_caching
  46. options.cache === -1 ? 'no-cache, no-store, must-revalidate' :
  47. options.cache // in seconds.
  48. );
  49. this.showDir = options.showDir !== 'false';
  50. this.autoIndex = options.autoIndex !== 'false';
  51. this.showDotfiles = options.showDotfiles;
  52. this.gzip = options.gzip === true;
  53. this.brotli = options.brotli === true;
  54. if (options.ext) {
  55. this.ext = options.ext === true
  56. ? 'html'
  57. : options.ext;
  58. }
  59. this.contentType = options.contentType ||
  60. this.ext === 'html' ? 'text/html' : 'application/octet-stream';
  61. var before = options.before ? options.before.slice() : [];
  62. if (options.logFn) {
  63. before.push(function (req, res) {
  64. options.logFn(req, res);
  65. res.emit('next');
  66. });
  67. }
  68. if (options.username || options.password) {
  69. before.push(function (req, res) {
  70. var credentials = auth(req);
  71. // We perform these outside the if to avoid short-circuiting and giving
  72. // an attacker knowledge of whether the username is correct via a timing
  73. // attack.
  74. if (credentials) {
  75. // if credentials is defined, name and pass are guaranteed to be string
  76. // type
  77. var usernameEqual = secureCompare(options.username.toString(), credentials.name);
  78. var passwordEqual = secureCompare(options.password.toString(), credentials.pass);
  79. if (usernameEqual && passwordEqual) {
  80. return res.emit('next');
  81. }
  82. }
  83. res.statusCode = 401;
  84. res.setHeader('WWW-Authenticate', 'Basic realm=""');
  85. res.end('Access denied');
  86. });
  87. }
  88. if (options.cors) {
  89. this.headers['Access-Control-Allow-Origin'] = '*';
  90. this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range';
  91. if (options.corsHeaders) {
  92. options.corsHeaders.split(/\s*,\s*/)
  93. .forEach(function (h) { this.headers['Access-Control-Allow-Headers'] += ', ' + h; }, this);
  94. }
  95. before.push(corser.create(options.corsHeaders ? {
  96. requestHeaders: this.headers['Access-Control-Allow-Headers'].split(/\s*,\s*/)
  97. } : null));
  98. }
  99. if (options.robots) {
  100. before.push(function (req, res) {
  101. if (req.url === '/robots.txt') {
  102. res.setHeader('Content-Type', 'text/plain');
  103. var robots = options.robots === true
  104. ? 'User-agent: *\nDisallow: /'
  105. : options.robots.replace(/\\n/, '\n');
  106. return res.end(robots);
  107. }
  108. res.emit('next');
  109. });
  110. }
  111. before.push(httpServerCore({
  112. root: this.root,
  113. cache: this.cache,
  114. showDir: this.showDir,
  115. showDotfiles: this.showDotfiles,
  116. autoIndex: this.autoIndex,
  117. defaultExt: this.ext,
  118. gzip: this.gzip,
  119. brotli: this.brotli,
  120. contentType: this.contentType,
  121. mimetypes: options.mimetypes,
  122. handleError: typeof options.proxy !== 'string'
  123. }));
  124. if (typeof options.proxy === 'string') {
  125. var proxyOptions = options.proxyOptions || {};
  126. var proxy = httpProxy.createProxyServer(proxyOptions);
  127. before.push(function (req, res) {
  128. proxy.web(req, res, {
  129. target: options.proxy,
  130. changeOrigin: true
  131. }, function (err, req, res) {
  132. if (options.logFn) {
  133. options.logFn(req, res, {
  134. message: err.message,
  135. status: res.statusCode });
  136. }
  137. res.emit('next');
  138. });
  139. });
  140. }
  141. var serverOptions = {
  142. before: before,
  143. headers: this.headers,
  144. onError: function (err, req, res) {
  145. if (options.logFn) {
  146. options.logFn(req, res, err);
  147. }
  148. res.end();
  149. }
  150. };
  151. if (options.https) {
  152. serverOptions.https = options.https;
  153. }
  154. this.server = serverOptions.https && serverOptions.https.passphrase
  155. // if passphrase is set, shim must be used as union does not support
  156. ? require('./shims/https-server-shim')(serverOptions)
  157. : union.createServer(serverOptions);
  158. if (options.timeout !== undefined) {
  159. this.server.setTimeout(options.timeout);
  160. }
  161. }
  162. HttpServer.prototype.listen = function () {
  163. this.server.listen.apply(this.server, arguments);
  164. };
  165. HttpServer.prototype.close = function () {
  166. return this.server.close();
  167. };