1 | 'use strict';
|
2 |
|
3 | var 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 |
|
13 | function 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 |
|
25 | var _pathNormalize = path.normalize;
|
26 | path.normalize = function (p) {
|
27 | var caller = getCaller();
|
28 | var result = _pathNormalize(p);
|
29 |
|
30 | if (caller === 'decodePathname') {
|
31 | result = result.replace(/\\/g, '/');
|
32 | }
|
33 | return result;
|
34 | };
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | exports.HttpServer = exports.HTTPServer = HttpServer;
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | exports.createServer = function (options) {
|
47 | return new HttpServer(options);
|
48 | };
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | function 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 |
|
76 |
|
77 | options.cache === -1 ? 'no-cache, no-store, must-revalidate' :
|
78 | options.cache
|
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 |
|
107 |
|
108 |
|
109 | if (credentials) {
|
110 |
|
111 |
|
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 |
|
204 | HttpServer.prototype.listen = function () {
|
205 | this.server.listen.apply(this.server, arguments);
|
206 | };
|
207 |
|
208 | HttpServer.prototype.close = function () {
|
209 | return this.server.close();
|
210 | };
|