@pkg-nec/http-server 14.1.1-pkgnec.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +147 -0
- package/bin/http-server +278 -0
- package/doc/http-server.1 +165 -0
- package/lib/core/aliases.json +34 -0
- package/lib/core/defaults.json +18 -0
- package/lib/core/etag.js +9 -0
- package/lib/core/index.js +487 -0
- package/lib/core/opts.js +203 -0
- package/lib/core/show-dir/icons.json +65 -0
- package/lib/core/show-dir/index.js +173 -0
- package/lib/core/show-dir/last-modified-to-string.js +10 -0
- package/lib/core/show-dir/perms-to-string.js +21 -0
- package/lib/core/show-dir/size-to-string.js +30 -0
- package/lib/core/show-dir/sort-files.js +36 -0
- package/lib/core/show-dir/styles.js +20 -0
- package/lib/core/status-handlers.js +96 -0
- package/lib/http-server.js +192 -0
- package/lib/shims/https-server-shim.js +67 -0
- package/package.json +122 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const url = require('url');
|
|
8
|
+
const { Readable } = require('stream');
|
|
9
|
+
const buffer = require('buffer');
|
|
10
|
+
const mime = require('mime');
|
|
11
|
+
const urlJoin = require('url-join');
|
|
12
|
+
const showDir = require('./show-dir');
|
|
13
|
+
const version = require('../../package.json').version;
|
|
14
|
+
const status = require('./status-handlers');
|
|
15
|
+
const generateEtag = require('./etag');
|
|
16
|
+
const optsParser = require('./opts');
|
|
17
|
+
const htmlEncodingSniffer = require('html-encoding-sniffer');
|
|
18
|
+
|
|
19
|
+
let httpServerCore = null;
|
|
20
|
+
|
|
21
|
+
function decodePathname(pathname) {
|
|
22
|
+
const pieces = pathname.replace(/\\/g, '/').split('/');
|
|
23
|
+
|
|
24
|
+
const normalized = path.normalize(pieces.map((rawPiece) => {
|
|
25
|
+
const piece = decodeURIComponent(rawPiece);
|
|
26
|
+
|
|
27
|
+
if (process.platform === 'win32' && /\\/.test(piece)) {
|
|
28
|
+
throw new Error('Invalid forward slash character');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return piece;
|
|
32
|
+
}).join('/'));
|
|
33
|
+
return process.platform === 'win32'
|
|
34
|
+
? normalized.replace(/\\/g, '/') : normalized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nonUrlSafeCharsRgx = /[\x00-\x1F\x20\x7F-\uFFFF]+/g;
|
|
38
|
+
function ensureUriEncoded(text) {
|
|
39
|
+
return text
|
|
40
|
+
return String(text).replace(nonUrlSafeCharsRgx, encodeURIComponent);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check to see if we should try to compress a file with gzip.
|
|
44
|
+
function shouldCompressGzip(req) {
|
|
45
|
+
const headers = req.headers;
|
|
46
|
+
|
|
47
|
+
return headers && headers['accept-encoding'] &&
|
|
48
|
+
headers['accept-encoding']
|
|
49
|
+
.split(',')
|
|
50
|
+
.some(el => ['*', 'compress', 'gzip', 'deflate'].indexOf(el.trim()) !== -1)
|
|
51
|
+
;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function shouldCompressBrotli(req) {
|
|
55
|
+
const headers = req.headers;
|
|
56
|
+
|
|
57
|
+
return headers && headers['accept-encoding'] &&
|
|
58
|
+
headers['accept-encoding']
|
|
59
|
+
.split(',')
|
|
60
|
+
.some(el => ['*', 'br'].indexOf(el.trim()) !== -1)
|
|
61
|
+
;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasGzipId12(gzipped, cb) {
|
|
65
|
+
const stream = fs.createReadStream(gzipped, { start: 0, end: 1 });
|
|
66
|
+
let buffer = Buffer.from('');
|
|
67
|
+
let hasBeenCalled = false;
|
|
68
|
+
|
|
69
|
+
stream.on('data', (chunk) => {
|
|
70
|
+
buffer = Buffer.concat([buffer, chunk], 2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
stream.on('error', (err) => {
|
|
74
|
+
if (hasBeenCalled) {
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
hasBeenCalled = true;
|
|
79
|
+
cb(err);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
stream.on('close', () => {
|
|
83
|
+
if (hasBeenCalled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
hasBeenCalled = true;
|
|
88
|
+
cb(null, buffer[0] === 31 && buffer[1] === 139);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
module.exports = function createMiddleware(_dir, _options) {
|
|
94
|
+
let dir;
|
|
95
|
+
let options;
|
|
96
|
+
if (typeof _dir === 'string') {
|
|
97
|
+
dir = _dir;
|
|
98
|
+
options = _options;
|
|
99
|
+
} else {
|
|
100
|
+
options = _dir;
|
|
101
|
+
dir = options.root;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const root = path.join(path.resolve(dir), '/');
|
|
105
|
+
const opts = optsParser(options);
|
|
106
|
+
const cache = opts.cache;
|
|
107
|
+
const autoIndex = opts.autoIndex;
|
|
108
|
+
const baseDir = opts.baseDir;
|
|
109
|
+
let defaultExt = opts.defaultExt;
|
|
110
|
+
const handleError = opts.handleError;
|
|
111
|
+
const headers = opts.headers;
|
|
112
|
+
const weakEtags = opts.weakEtags;
|
|
113
|
+
const handleOptionsMethod = opts.handleOptionsMethod;
|
|
114
|
+
|
|
115
|
+
opts.root = dir;
|
|
116
|
+
if (defaultExt && /^\./.test(defaultExt)) {
|
|
117
|
+
defaultExt = defaultExt.replace(/^\./, '');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Support hashes and .types files in mimeTypes @since 0.8
|
|
121
|
+
if (opts.mimeTypes) {
|
|
122
|
+
try {
|
|
123
|
+
// You can pass a JSON blob here---useful for CLI use
|
|
124
|
+
opts.mimeTypes = JSON.parse(opts.mimeTypes);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// swallow parse errors, treat this as a string mimetype input
|
|
127
|
+
}
|
|
128
|
+
if (typeof opts.mimeTypes === 'string') {
|
|
129
|
+
mime.load(opts.mimeTypes);
|
|
130
|
+
} else if (typeof opts.mimeTypes === 'object') {
|
|
131
|
+
mime.define(opts.mimeTypes);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function shouldReturn304(req, serverLastModified, serverEtag) {
|
|
136
|
+
if (!req || !req.headers) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const clientModifiedSince = req.headers['if-modified-since'];
|
|
141
|
+
const clientEtag = req.headers['if-none-match'];
|
|
142
|
+
let clientModifiedDate;
|
|
143
|
+
|
|
144
|
+
if (!clientModifiedSince && !clientEtag) {
|
|
145
|
+
// Client did not provide any conditional caching headers
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (clientModifiedSince) {
|
|
150
|
+
// Catch "illegal access" dates that will crash v8
|
|
151
|
+
try {
|
|
152
|
+
clientModifiedDate = new Date(Date.parse(clientModifiedSince));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (clientModifiedDate.toString() === 'Invalid Date') {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// If the client's copy is older than the server's, don't return 304
|
|
161
|
+
if (clientModifiedDate < new Date(serverLastModified)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (clientEtag) {
|
|
167
|
+
// Do a strong or weak etag comparison based on setting
|
|
168
|
+
// https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3
|
|
169
|
+
if (opts.weakCompare && clientEtag !== serverEtag
|
|
170
|
+
&& clientEtag !== `W/${serverEtag}` && `W/${clientEtag}` !== serverEtag) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return function middleware(req, res, next) {
|
|
182
|
+
// Figure out the path for the file from the given url
|
|
183
|
+
const parsed = url.parse(req.url);
|
|
184
|
+
let pathname = null;
|
|
185
|
+
let file = null;
|
|
186
|
+
let gzippedFile = null;
|
|
187
|
+
let brotliFile = null;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
decodeURIComponent(req.url); // check validity of url
|
|
191
|
+
pathname = decodePathname(parsed.pathname);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
status[400](res, next, { error: err });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
file = path.normalize(
|
|
198
|
+
path.join(
|
|
199
|
+
root,
|
|
200
|
+
path.relative(path.join('/', baseDir), pathname)
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
// determine compressed forms if they were to exist
|
|
204
|
+
gzippedFile = `${file}.gz`;
|
|
205
|
+
brotliFile = `${file}.br`;
|
|
206
|
+
|
|
207
|
+
Object.keys(headers).forEach((key) => {
|
|
208
|
+
res.setHeader(key, headers[key]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (req.method === 'OPTIONS' && handleOptionsMethod) {
|
|
212
|
+
res.end();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// TODO: This check is broken, which causes the 403 on the
|
|
217
|
+
// expected 404.
|
|
218
|
+
if (file.slice(0, root.length) !== root) {
|
|
219
|
+
status[403](res, next);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (req.method && (req.method !== 'GET' && req.method !== 'HEAD')) {
|
|
224
|
+
status[405](res, next);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
function serve(stat) {
|
|
230
|
+
// Do a MIME lookup, fall back to octet-stream and handle gzip
|
|
231
|
+
// and brotli special case.
|
|
232
|
+
const defaultType = opts.contentType || 'application/octet-stream';
|
|
233
|
+
let contentType = mime.lookup(file, defaultType);
|
|
234
|
+
const range = (req.headers && req.headers.range);
|
|
235
|
+
const lastModified = (new Date(stat.mtime)).toUTCString();
|
|
236
|
+
const etag = generateEtag(stat, weakEtags);
|
|
237
|
+
let cacheControl = cache;
|
|
238
|
+
let stream = null;
|
|
239
|
+
if (contentType && isTextFile(contentType)) {
|
|
240
|
+
if (stat.size < buffer.constants.MAX_LENGTH) {
|
|
241
|
+
const bytes = fs.readFileSync(file);
|
|
242
|
+
const sniffedEncoding = htmlEncodingSniffer(bytes, {
|
|
243
|
+
defaultEncoding: 'UTF-8'
|
|
244
|
+
});
|
|
245
|
+
contentType += `; charset=${sniffedEncoding}`;
|
|
246
|
+
stream = Readable.from(bytes)
|
|
247
|
+
} else {
|
|
248
|
+
// Assume text types are utf8
|
|
249
|
+
contentType += '; charset=UTF-8';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (file === gzippedFile) { // is .gz picked up
|
|
254
|
+
res.setHeader('Content-Encoding', 'gzip');
|
|
255
|
+
// strip gz ending and lookup mime type
|
|
256
|
+
contentType = mime.lookup(path.basename(file, '.gz'), defaultType);
|
|
257
|
+
} else if (file === brotliFile) { // is .br picked up
|
|
258
|
+
res.setHeader('Content-Encoding', 'br');
|
|
259
|
+
// strip br ending and lookup mime type
|
|
260
|
+
contentType = mime.lookup(path.basename(file, '.br'), defaultType);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (typeof cacheControl === 'function') {
|
|
264
|
+
cacheControl = cache(pathname);
|
|
265
|
+
}
|
|
266
|
+
if (typeof cacheControl === 'number') {
|
|
267
|
+
cacheControl = `max-age=${cacheControl}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (range) {
|
|
271
|
+
const total = stat.size;
|
|
272
|
+
const parts = range.trim().replace(/bytes=/, '').split('-');
|
|
273
|
+
const partialstart = parts[0];
|
|
274
|
+
const partialend = parts[1];
|
|
275
|
+
const start = parseInt(partialstart, 10);
|
|
276
|
+
const end = Math.min(
|
|
277
|
+
total - 1,
|
|
278
|
+
partialend ? parseInt(partialend, 10) : total - 1
|
|
279
|
+
);
|
|
280
|
+
const chunksize = (end - start) + 1;
|
|
281
|
+
let fstream = null;
|
|
282
|
+
|
|
283
|
+
if (start > end || isNaN(start) || isNaN(end)) {
|
|
284
|
+
status['416'](res, next);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fstream = fs.createReadStream(file, { start, end });
|
|
289
|
+
fstream.on('error', (err) => {
|
|
290
|
+
status['500'](res, next, { error: err });
|
|
291
|
+
});
|
|
292
|
+
res.on('close', () => {
|
|
293
|
+
fstream.destroy();
|
|
294
|
+
});
|
|
295
|
+
res.writeHead(206, {
|
|
296
|
+
'Content-Range': `bytes ${start}-${end}/${total}`,
|
|
297
|
+
'Accept-Ranges': 'bytes',
|
|
298
|
+
'Content-Length': chunksize,
|
|
299
|
+
'Content-Type': contentType,
|
|
300
|
+
'cache-control': cacheControl,
|
|
301
|
+
'last-modified': lastModified,
|
|
302
|
+
etag,
|
|
303
|
+
});
|
|
304
|
+
fstream.pipe(res);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// TODO: Helper for this, with default headers.
|
|
309
|
+
res.setHeader('cache-control', cacheControl);
|
|
310
|
+
res.setHeader('last-modified', lastModified);
|
|
311
|
+
res.setHeader('etag', etag);
|
|
312
|
+
|
|
313
|
+
// Return a 304 if necessary
|
|
314
|
+
if (shouldReturn304(req, lastModified, etag)) {
|
|
315
|
+
status[304](res, next);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
res.setHeader('content-length', stat.size);
|
|
320
|
+
res.setHeader('content-type', contentType);
|
|
321
|
+
|
|
322
|
+
// set the response statusCode if we have a request statusCode.
|
|
323
|
+
// This only can happen if we have a 404 with some kind of 404.html
|
|
324
|
+
// In all other cases where we have a file we serve the 200
|
|
325
|
+
res.statusCode = req.statusCode || 200;
|
|
326
|
+
|
|
327
|
+
if (req.method === 'HEAD') {
|
|
328
|
+
res.end();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// stream may already have been assigned during encoding sniffing.
|
|
333
|
+
if (stream === null) {
|
|
334
|
+
stream = fs.createReadStream(file);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
stream.pipe(res);
|
|
338
|
+
stream.on('error', (err) => {
|
|
339
|
+
status['500'](res, next, { error: err });
|
|
340
|
+
});
|
|
341
|
+
stream.on('close', () => {
|
|
342
|
+
stream.destroy();
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
function statFile() {
|
|
348
|
+
try {
|
|
349
|
+
fs.stat(file, (err, stat) => {
|
|
350
|
+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
|
|
351
|
+
if (req.statusCode === 404) {
|
|
352
|
+
// This means we're already trying ./404.html and can not find it.
|
|
353
|
+
// So send plain text response with 404 status code
|
|
354
|
+
status[404](res, next);
|
|
355
|
+
} else if (!path.extname(parsed.pathname).length && defaultExt) {
|
|
356
|
+
// If there is no file extension in the path and we have a default
|
|
357
|
+
// extension try filename and default extension combination before rendering 404.html.
|
|
358
|
+
middleware({
|
|
359
|
+
url: `${parsed.pathname}.${defaultExt}${(parsed.search) ? parsed.search : ''}`,
|
|
360
|
+
headers: req.headers,
|
|
361
|
+
}, res, next);
|
|
362
|
+
} else {
|
|
363
|
+
// Try to serve default ./404.html
|
|
364
|
+
const rawUrl = (handleError ? `/${path.join(baseDir, `404.${defaultExt}`)}` : req.url);
|
|
365
|
+
const encodedUrl = ensureUriEncoded(rawUrl);
|
|
366
|
+
middleware({
|
|
367
|
+
url: encodedUrl,
|
|
368
|
+
headers: req.headers,
|
|
369
|
+
statusCode: 404,
|
|
370
|
+
}, res, next);
|
|
371
|
+
}
|
|
372
|
+
} else if (err) {
|
|
373
|
+
status[500](res, next, { error: err });
|
|
374
|
+
} else if (stat.isDirectory()) {
|
|
375
|
+
if (!autoIndex && !opts.showDir) {
|
|
376
|
+
status[404](res, next);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
// 302 to / if necessary
|
|
382
|
+
if (!pathname.match(/\/$/)) {
|
|
383
|
+
res.statusCode = 302;
|
|
384
|
+
const q = parsed.query ? `?${parsed.query}` : '';
|
|
385
|
+
res.setHeader(
|
|
386
|
+
'location',
|
|
387
|
+
ensureUriEncoded(`${parsed.pathname}/${q}`)
|
|
388
|
+
);
|
|
389
|
+
res.end();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (autoIndex) {
|
|
394
|
+
middleware({
|
|
395
|
+
url: urlJoin(
|
|
396
|
+
encodeURIComponent(pathname),
|
|
397
|
+
`/index.${defaultExt}`
|
|
398
|
+
),
|
|
399
|
+
headers: req.headers,
|
|
400
|
+
}, res, (autoIndexError) => {
|
|
401
|
+
if (autoIndexError) {
|
|
402
|
+
status[500](res, next, { error: autoIndexError });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (opts.showDir) {
|
|
406
|
+
showDir(opts, stat)(req, res);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
status[403](res, next);
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (opts.showDir) {
|
|
416
|
+
showDir(opts, stat)(req, res);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
serve(stat);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
} catch (err) {
|
|
423
|
+
status[500](res, next, { error: err.message });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isTextFile(mimeType) {
|
|
428
|
+
return (/^text\/|^application\/(javascript|json)/).test(mimeType);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// serve gzip file if exists and is valid
|
|
432
|
+
function tryServeWithGzip() {
|
|
433
|
+
try {
|
|
434
|
+
fs.stat(gzippedFile, (err, stat) => {
|
|
435
|
+
if (!err && stat.isFile()) {
|
|
436
|
+
hasGzipId12(gzippedFile, (gzipErr, isGzip) => {
|
|
437
|
+
if (!gzipErr && isGzip) {
|
|
438
|
+
file = gzippedFile;
|
|
439
|
+
serve(stat);
|
|
440
|
+
} else {
|
|
441
|
+
statFile();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
statFile();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
} catch (err) {
|
|
449
|
+
status[500](res, next, { error: err.message });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// serve brotli file if exists, otherwise try gzip
|
|
454
|
+
function tryServeWithBrotli(shouldTryGzip) {
|
|
455
|
+
try {
|
|
456
|
+
fs.stat(brotliFile, (err, stat) => {
|
|
457
|
+
if (!err && stat.isFile()) {
|
|
458
|
+
file = brotliFile;
|
|
459
|
+
serve(stat);
|
|
460
|
+
} else if (shouldTryGzip) {
|
|
461
|
+
tryServeWithGzip();
|
|
462
|
+
} else {
|
|
463
|
+
statFile();
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
} catch (err) {
|
|
467
|
+
status[500](res, next, { error: err.message });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const shouldTryBrotli = opts.brotli && shouldCompressBrotli(req);
|
|
472
|
+
const shouldTryGzip = opts.gzip && shouldCompressGzip(req);
|
|
473
|
+
// always try brotli first, next try gzip, finally serve without compression
|
|
474
|
+
if (shouldTryBrotli) {
|
|
475
|
+
tryServeWithBrotli(shouldTryGzip);
|
|
476
|
+
} else if (shouldTryGzip) {
|
|
477
|
+
tryServeWithGzip();
|
|
478
|
+
} else {
|
|
479
|
+
statFile();
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
httpServerCore = module.exports;
|
|
486
|
+
httpServerCore.version = version;
|
|
487
|
+
httpServerCore.showDir = showDir;
|
package/lib/core/opts.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// This is so you can have options aliasing and defaults in one place.
|
|
4
|
+
|
|
5
|
+
const defaults = require('./defaults.json');
|
|
6
|
+
const aliases = require('./aliases.json');
|
|
7
|
+
|
|
8
|
+
module.exports = (opts) => {
|
|
9
|
+
let autoIndex = defaults.autoIndex;
|
|
10
|
+
let showDir = defaults.showDir;
|
|
11
|
+
let showDotfiles = defaults.showDotfiles;
|
|
12
|
+
let humanReadable = defaults.humanReadable;
|
|
13
|
+
let hidePermissions = defaults.hidePermissions;
|
|
14
|
+
let si = defaults.si;
|
|
15
|
+
let cache = defaults.cache;
|
|
16
|
+
let gzip = defaults.gzip;
|
|
17
|
+
let brotli = defaults.brotli;
|
|
18
|
+
let defaultExt = defaults.defaultExt;
|
|
19
|
+
let handleError = defaults.handleError;
|
|
20
|
+
const headers = {};
|
|
21
|
+
let contentType = defaults.contentType;
|
|
22
|
+
let mimeTypes;
|
|
23
|
+
let weakEtags = defaults.weakEtags;
|
|
24
|
+
let weakCompare = defaults.weakCompare;
|
|
25
|
+
let handleOptionsMethod = defaults.handleOptionsMethod;
|
|
26
|
+
|
|
27
|
+
function isDeclared(k) {
|
|
28
|
+
return typeof opts[k] !== 'undefined' && opts[k] !== null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setHeader(str) {
|
|
32
|
+
const m = /^(.+?)\s*:\s*(.*)$/.exec(str);
|
|
33
|
+
if (!m) {
|
|
34
|
+
headers[str] = true;
|
|
35
|
+
} else {
|
|
36
|
+
headers[m[1]] = m[2];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if (opts) {
|
|
42
|
+
aliases.autoIndex.some((k) => {
|
|
43
|
+
if (isDeclared(k)) {
|
|
44
|
+
autoIndex = opts[k];
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
aliases.showDir.some((k) => {
|
|
51
|
+
if (isDeclared(k)) {
|
|
52
|
+
showDir = opts[k];
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
aliases.showDotfiles.some((k) => {
|
|
59
|
+
if (isDeclared(k)) {
|
|
60
|
+
showDotfiles = opts[k];
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
aliases.humanReadable.some((k) => {
|
|
67
|
+
if (isDeclared(k)) {
|
|
68
|
+
humanReadable = opts[k];
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
aliases.hidePermissions.some((k) => {
|
|
75
|
+
if (isDeclared(k)) {
|
|
76
|
+
hidePermissions = opts[k];
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
aliases.si.some((k) => {
|
|
83
|
+
if (isDeclared(k)) {
|
|
84
|
+
si = opts[k];
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (opts.defaultExt && typeof opts.defaultExt === 'string') {
|
|
91
|
+
defaultExt = opts.defaultExt;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof opts.cache !== 'undefined' && opts.cache !== null) {
|
|
95
|
+
if (typeof opts.cache === 'string') {
|
|
96
|
+
cache = opts.cache;
|
|
97
|
+
} else if (typeof opts.cache === 'number') {
|
|
98
|
+
cache = `max-age=${opts.cache}`;
|
|
99
|
+
} else if (typeof opts.cache === 'function') {
|
|
100
|
+
cache = opts.cache;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof opts.gzip !== 'undefined' && opts.gzip !== null) {
|
|
105
|
+
gzip = opts.gzip;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof opts.brotli !== 'undefined' && opts.brotli !== null) {
|
|
109
|
+
brotli = opts.brotli;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
aliases.handleError.some((k) => {
|
|
113
|
+
if (isDeclared(k)) {
|
|
114
|
+
handleError = opts[k];
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
aliases.cors.forEach((k) => {
|
|
121
|
+
if (isDeclared(k) && opts[k]) {
|
|
122
|
+
handleOptionsMethod = true;
|
|
123
|
+
headers['Access-Control-Allow-Origin'] = '*';
|
|
124
|
+
headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since';
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
aliases.headers.forEach((k) => {
|
|
129
|
+
if (isDeclared(k)) {
|
|
130
|
+
if (Array.isArray(opts[k])) {
|
|
131
|
+
opts[k].forEach(setHeader);
|
|
132
|
+
} else if (opts[k] && typeof opts[k] === 'object') {
|
|
133
|
+
Object.keys(opts[k]).forEach((key) => {
|
|
134
|
+
headers[key] = opts[k][key];
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
setHeader(opts[k]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
aliases.contentType.some((k) => {
|
|
143
|
+
if (isDeclared(k)) {
|
|
144
|
+
contentType = opts[k];
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
aliases.mimeType.some((k) => {
|
|
151
|
+
if (isDeclared(k)) {
|
|
152
|
+
mimeTypes = opts[k];
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
aliases.weakEtags.some((k) => {
|
|
159
|
+
if (isDeclared(k)) {
|
|
160
|
+
weakEtags = opts[k];
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
aliases.weakCompare.some((k) => {
|
|
167
|
+
if (isDeclared(k)) {
|
|
168
|
+
weakCompare = opts[k];
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
aliases.handleOptionsMethod.some((k) => {
|
|
175
|
+
if (isDeclared(k)) {
|
|
176
|
+
handleOptionsMethod = handleOptionsMethod || opts[k];
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
cache,
|
|
185
|
+
autoIndex,
|
|
186
|
+
showDir,
|
|
187
|
+
showDotfiles,
|
|
188
|
+
humanReadable,
|
|
189
|
+
hidePermissions,
|
|
190
|
+
si,
|
|
191
|
+
defaultExt,
|
|
192
|
+
baseDir: (opts && opts.baseDir) || '/',
|
|
193
|
+
gzip,
|
|
194
|
+
brotli,
|
|
195
|
+
handleError,
|
|
196
|
+
headers,
|
|
197
|
+
contentType,
|
|
198
|
+
mimeTypes,
|
|
199
|
+
weakEtags,
|
|
200
|
+
weakCompare,
|
|
201
|
+
handleOptionsMethod,
|
|
202
|
+
};
|
|
203
|
+
};
|