UNPKG

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