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

211 lines
5.6KB

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