|
- 'use strict';
-
- var fs = require('fs'),
- union = require('union'),
- ecstatic = require('ecstatic'),
- auth = require('basic-auth'),
- httpProxy = require('http-proxy'),
- corser = require('corser'),
- path = require('path'),
- secureCompare = require('secure-compare');
-
- // a hacky and direct workaround to fix https://github.com/http-party/http-server/issues/525
- function getCaller() {
- try {
- var stack = new Error().stack;
- var stackLines = stack.split('\n');
- var callerStack = stackLines[3];
- return callerStack.match(/at (.+) \(/)[1];
- }
- catch (error) {
- return '';
- }
- }
-
- var _pathNormalize = path.normalize;
- path.normalize = function (p) {
- var caller = getCaller();
- var result = _pathNormalize(p);
- // https://github.com/jfhbrook/node-ecstatic/blob/master/lib/ecstatic.js#L20
- if (caller === 'decodePathname') {
- result = result.replace(/\\/g, '/');
- }
- return result;
- };
-
- //
- // Remark: backwards compatibility for previous
- // case convention of HTTP
- //
- exports.HttpServer = exports.HTTPServer = HttpServer;
-
- /**
- * Returns a new instance of HttpServer with the
- * specified `options`.
- */
- exports.createServer = function (options) {
- return new HttpServer(options);
- };
-
- /**
- * Constructor function for the HttpServer object
- * which is responsible for serving static files along
- * with other HTTP-related features.
- */
- function HttpServer(options) {
- options = options || {};
-
- if (options.root) {
- this.root = options.root;
- }
- else {
- try {
- fs.lstatSync('./public');
- this.root = './public';
- }
- catch (err) {
- this.root = './';
- }
- }
-
- this.headers = options.headers || {};
-
- this.cache = (
- options.cache === undefined ? 3600 :
- // -1 is a special case to turn off caching.
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Preventing_caching
- options.cache === -1 ? 'no-cache, no-store, must-revalidate' :
- options.cache // in seconds.
- );
- this.showDir = options.showDir !== 'false';
- this.autoIndex = options.autoIndex !== 'false';
- this.showDotfiles = options.showDotfiles;
- this.gzip = options.gzip === true;
- this.brotli = options.brotli === true;
- if (options.ext) {
- this.ext = options.ext === true
- ? 'html'
- : options.ext;
- }
- this.contentType = options.contentType ||
- this.ext === 'html' ? 'text/html' : 'application/octet-stream';
-
- var before = options.before ? options.before.slice() : [];
-
- if (options.logFn) {
- before.push(function (req, res) {
- options.logFn(req, res);
- res.emit('next');
- });
- }
-
- if (options.username || options.password) {
- before.push(function (req, res) {
- var credentials = auth(req);
-
- // We perform these outside the if to avoid short-circuiting and giving
- // an attacker knowledge of whether the username is correct via a timing
- // attack.
- if (credentials) {
- // if credentials is defined, name and pass are guaranteed to be string
- // type
- var usernameEqual = secureCompare(options.username.toString(), credentials.name);
- var passwordEqual = secureCompare(options.password.toString(), credentials.pass);
- if (usernameEqual && passwordEqual) {
- return res.emit('next');
- }
- }
-
- res.statusCode = 401;
- res.setHeader('WWW-Authenticate', 'Basic realm=""');
- res.end('Access denied');
- });
- }
-
- if (options.cors) {
- this.headers['Access-Control-Allow-Origin'] = '*';
- this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range';
- if (options.corsHeaders) {
- options.corsHeaders.split(/\s*,\s*/)
- .forEach(function (h) { this.headers['Access-Control-Allow-Headers'] += ', ' + h; }, this);
- }
- before.push(corser.create(options.corsHeaders ? {
- requestHeaders: this.headers['Access-Control-Allow-Headers'].split(/\s*,\s*/)
- } : null));
- }
-
- if (options.robots) {
- before.push(function (req, res) {
- if (req.url === '/robots.txt') {
- res.setHeader('Content-Type', 'text/plain');
- var robots = options.robots === true
- ? 'User-agent: *\nDisallow: /'
- : options.robots.replace(/\\n/, '\n');
-
- return res.end(robots);
- }
-
- res.emit('next');
- });
- }
-
- before.push(ecstatic({
- root: this.root,
- cache: this.cache,
- showDir: this.showDir,
- showDotfiles: this.showDotfiles,
- autoIndex: this.autoIndex,
- defaultExt: this.ext,
- gzip: this.gzip,
- brotli: this.brotli,
- contentType: this.contentType,
- handleError: typeof options.proxy !== 'string'
- }));
-
- if (typeof options.proxy === 'string') {
- var proxy = httpProxy.createProxyServer({});
- before.push(function (req, res) {
- proxy.web(req, res, {
- target: options.proxy,
- changeOrigin: true
- }, function (err, req, res, target) {
- if (options.logFn) {
- options.logFn(req, res, {
- message: err.message,
- status: res.statusCode });
- }
- res.emit('next');
- });
- });
- }
-
- var serverOptions = {
- before: before,
- headers: this.headers,
- onError: function (err, req, res) {
- if (options.logFn) {
- options.logFn(req, res, err);
- }
-
- res.end();
- }
- };
-
- if (options.https) {
- serverOptions.https = options.https;
- }
-
- this.server = union.createServer(serverOptions);
- if (options.timeout !== undefined) {
- this.server.setTimeout(options.timeout);
- }
- }
-
- HttpServer.prototype.listen = function () {
- this.server.listen.apply(this.server, arguments);
- };
-
- HttpServer.prototype.close = function () {
- return this.server.close();
- };
|