@wooksjs/http-static 0.4.10 → 0.4.12
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/dist/index.cjs +148 -207
- package/dist/index.d.ts +17 -20
- package/dist/index.mjs +148 -207
- package/package.json +10 -2
package/dist/index.cjs
CHANGED
|
@@ -4,38 +4,6 @@ var eventHttp = require('@wooksjs/event-http');
|
|
|
4
4
|
var fs = require('fs');
|
|
5
5
|
var path = require('path');
|
|
6
6
|
|
|
7
|
-
/******************************************************************************
|
|
8
|
-
Copyright (c) Microsoft Corporation.
|
|
9
|
-
|
|
10
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
11
|
-
purpose with or without fee is hereby granted.
|
|
12
|
-
|
|
13
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
14
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
15
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
16
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
17
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
18
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
19
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
20
|
-
***************************************************************************** */
|
|
21
|
-
/* global Reflect, Promise, SuppressedError, Symbol */
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
25
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
26
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
27
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
28
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
29
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
30
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
35
|
-
var e = new Error(message);
|
|
36
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
7
|
const extensions = {
|
|
40
8
|
'123': 'application/vnd.lotus-1-2-3',
|
|
41
9
|
ez: 'application/andrew-inset',
|
|
@@ -1230,134 +1198,118 @@ function normalizePath(filePath, baseDir) {
|
|
|
1230
1198
|
}
|
|
1231
1199
|
|
|
1232
1200
|
const { stat, readdir } = fs.promises;
|
|
1233
|
-
function serveFile(filePath, options = {}) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1201
|
+
async function serveFile(filePath, options = {}) {
|
|
1202
|
+
if (!options.allowDotDot && (/\/\.\.\//.test(filePath) || /^\.\.\//.test(filePath))) {
|
|
1203
|
+
throw new Error('Parent Traversal ("/../") is not allowed.');
|
|
1204
|
+
}
|
|
1205
|
+
const { restoreCtx } = eventHttp.useHttpContext();
|
|
1206
|
+
const { status } = eventHttp.useResponse();
|
|
1207
|
+
const { setHeader, removeHeader } = eventHttp.useSetHeaders();
|
|
1208
|
+
const headers = eventHttp.useHeaders();
|
|
1209
|
+
const { method, url } = eventHttp.useRequest();
|
|
1210
|
+
const { setCacheControl, setExpires, setPragmaNoCache } = eventHttp.useSetCacheControl();
|
|
1211
|
+
const normalizedPath = normalizePath(filePath, options.baseDir);
|
|
1212
|
+
let fileStats;
|
|
1213
|
+
try {
|
|
1214
|
+
fileStats = await stat(normalizedPath);
|
|
1215
|
+
}
|
|
1216
|
+
catch (e) {
|
|
1217
|
+
if (options.defaultExt) {
|
|
1218
|
+
const ext = path.extname(filePath);
|
|
1219
|
+
if (!ext) {
|
|
1220
|
+
restoreCtx();
|
|
1221
|
+
return serveFile(filePath + '.' + options.defaultExt);
|
|
1222
|
+
}
|
|
1237
1223
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1224
|
+
throw new eventHttp.HttpError(404);
|
|
1225
|
+
}
|
|
1226
|
+
status(200);
|
|
1227
|
+
const etag = `"${[
|
|
1228
|
+
fileStats.ino,
|
|
1229
|
+
fileStats.size,
|
|
1230
|
+
fileStats.mtime.toISOString(),
|
|
1231
|
+
].join('-')}"`;
|
|
1232
|
+
const lastModified = new Date(fileStats.mtime);
|
|
1233
|
+
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1234
|
+
status(304);
|
|
1235
|
+
return '';
|
|
1236
|
+
}
|
|
1237
|
+
setHeader('etag', etag);
|
|
1238
|
+
setHeader('last-modified', lastModified.toUTCString());
|
|
1239
|
+
if (options.cacheControl !== undefined) {
|
|
1240
|
+
setCacheControl(options.cacheControl);
|
|
1241
|
+
}
|
|
1242
|
+
if (options.expires) {
|
|
1243
|
+
setExpires(options.expires);
|
|
1244
|
+
}
|
|
1245
|
+
if (options.pragmaNoCache) {
|
|
1246
|
+
setPragmaNoCache(options.pragmaNoCache);
|
|
1247
|
+
}
|
|
1248
|
+
if (fileStats.isDirectory()) {
|
|
1249
|
+
if (options.listDirectory) {
|
|
1250
|
+
restoreCtx();
|
|
1251
|
+
return listDirectory(normalizedPath);
|
|
1248
1252
|
}
|
|
1249
|
-
|
|
1250
|
-
if (
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1253
|
+
else if (options.index) {
|
|
1254
|
+
if (filePath[filePath.length - 1] !== '/' &&
|
|
1255
|
+
url &&
|
|
1256
|
+
url[url.length - 1] !== '/') {
|
|
1257
|
+
return new eventHttp.BaseHttpResponse()
|
|
1258
|
+
.setStatus(302)
|
|
1259
|
+
.setHeader('location', url + '/');
|
|
1256
1260
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
const etag = `"${[
|
|
1263
|
-
fileStats.ino,
|
|
1264
|
-
fileStats.size,
|
|
1265
|
-
fileStats.mtime.toISOString(),
|
|
1266
|
-
].join('-')}"`;
|
|
1267
|
-
const lastModified = new Date(fileStats.mtime);
|
|
1268
|
-
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1269
|
-
status(304);
|
|
1270
|
-
return '';
|
|
1261
|
+
restoreCtx();
|
|
1262
|
+
return serveFile(path.join(filePath, options.index), {
|
|
1263
|
+
...options,
|
|
1264
|
+
index: '',
|
|
1265
|
+
});
|
|
1271
1266
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1267
|
+
removeHeader('etag');
|
|
1268
|
+
removeHeader('last-modified');
|
|
1269
|
+
throw new eventHttp.HttpError(404);
|
|
1270
|
+
}
|
|
1271
|
+
let range = headers.range;
|
|
1272
|
+
let start, end;
|
|
1273
|
+
let size = fileStats.size;
|
|
1274
|
+
if (range) {
|
|
1275
|
+
const rangeParts = range
|
|
1276
|
+
.trim()
|
|
1277
|
+
.replace(/bytes=/, '')
|
|
1278
|
+
.split('-');
|
|
1279
|
+
const [s, e] = rangeParts;
|
|
1280
|
+
start = parseInt(s);
|
|
1281
|
+
end = e ? parseInt(e) : size - 1;
|
|
1282
|
+
end = Math.min(size - 1, end);
|
|
1283
|
+
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1284
|
+
throw new eventHttp.HttpError(416);
|
|
1277
1285
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1286
|
+
size = end - start + 1;
|
|
1287
|
+
const ifRange = headers['if-range'] || '';
|
|
1288
|
+
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1289
|
+
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1290
|
+
if (ifRange &&
|
|
1291
|
+
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1292
|
+
status(200);
|
|
1293
|
+
size = fileStats.size;
|
|
1294
|
+
range = '';
|
|
1280
1295
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1296
|
+
else {
|
|
1297
|
+
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1298
|
+
status(206);
|
|
1283
1299
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
url &&
|
|
1292
|
-
url[url.length - 1] !== '/') {
|
|
1293
|
-
return new eventHttp.BaseHttpResponse()
|
|
1294
|
-
.setStatus(302)
|
|
1295
|
-
.setHeader('location', url + '/');
|
|
1296
|
-
}
|
|
1297
|
-
restoreCtx();
|
|
1298
|
-
return serveFile(path.join(filePath, options.index), Object.assign(Object.assign({}, options), { index: '' }));
|
|
1299
|
-
}
|
|
1300
|
-
removeHeader('etag');
|
|
1301
|
-
removeHeader('last-modified');
|
|
1302
|
-
throw new eventHttp.HttpError(404);
|
|
1303
|
-
}
|
|
1304
|
-
// range header processing start
|
|
1305
|
-
let range = headers.range;
|
|
1306
|
-
let start, end;
|
|
1307
|
-
let size = fileStats.size;
|
|
1308
|
-
if (range) {
|
|
1309
|
-
const rangeParts = range
|
|
1310
|
-
.trim()
|
|
1311
|
-
.replace(/bytes=/, '')
|
|
1312
|
-
.split('-');
|
|
1313
|
-
const [s, e] = rangeParts;
|
|
1314
|
-
start = parseInt(s);
|
|
1315
|
-
end = e ? parseInt(e) : size - 1;
|
|
1316
|
-
end = Math.min(size - 1, end);
|
|
1317
|
-
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1318
|
-
throw new eventHttp.HttpError(416);
|
|
1319
|
-
}
|
|
1320
|
-
size = end - start + 1;
|
|
1321
|
-
// if-range processing start
|
|
1322
|
-
// rfc7233#section-3.2\
|
|
1323
|
-
const ifRange = headers['if-range'] || '';
|
|
1324
|
-
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1325
|
-
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1326
|
-
if (ifRange &&
|
|
1327
|
-
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1328
|
-
// If the validator does not match, the server MUST ignore
|
|
1329
|
-
// the Range header field.
|
|
1330
|
-
status(200);
|
|
1331
|
-
size = fileStats.size;
|
|
1332
|
-
range = '';
|
|
1333
|
-
}
|
|
1334
|
-
else {
|
|
1335
|
-
// If the validator given in the If-Range header field matches the
|
|
1336
|
-
// current validator for the selected representation of the target
|
|
1337
|
-
// resource, then the server SHOULD process the Range header field as
|
|
1338
|
-
// requested.
|
|
1339
|
-
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1340
|
-
status(206);
|
|
1341
|
-
}
|
|
1342
|
-
// if-range processing end
|
|
1343
|
-
}
|
|
1344
|
-
// range header processing end
|
|
1345
|
-
setHeader('accept-ranges', 'bytes');
|
|
1346
|
-
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1347
|
-
setHeader('content-length', size);
|
|
1348
|
-
if (options.headers) {
|
|
1349
|
-
for (const header of Object.keys(options.headers)) {
|
|
1350
|
-
setHeader(header, options.headers[header]);
|
|
1351
|
-
}
|
|
1300
|
+
}
|
|
1301
|
+
setHeader('accept-ranges', 'bytes');
|
|
1302
|
+
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1303
|
+
setHeader('content-length', size);
|
|
1304
|
+
if (options.headers) {
|
|
1305
|
+
for (const header of Object.keys(options.headers)) {
|
|
1306
|
+
setHeader(header, options.headers[header]);
|
|
1352
1307
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1308
|
+
}
|
|
1309
|
+
return method === 'HEAD'
|
|
1310
|
+
? ''
|
|
1311
|
+
: fs.createReadStream(normalizedPath, !!range ? { start, end } : undefined);
|
|
1357
1312
|
}
|
|
1358
|
-
// function toWeak(etag: string): string {
|
|
1359
|
-
// return `W/${etag}`
|
|
1360
|
-
// }
|
|
1361
1313
|
function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
1362
1314
|
if (clientEtag) {
|
|
1363
1315
|
const parts = clientEtag.split(',').map((v) => v.trim());
|
|
@@ -1366,71 +1318,60 @@ function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
|
1366
1318
|
return true;
|
|
1367
1319
|
}
|
|
1368
1320
|
}
|
|
1369
|
-
// A recipient MUST ignore If-Modified-Since if the request contains an
|
|
1370
|
-
// If-None-Match header field; the condition in If-None-Match is
|
|
1371
|
-
// considered to be a more accurate replacement for the condition in
|
|
1372
|
-
// If-Modified-Since, and the two are only combined for the sake of
|
|
1373
|
-
// interoperating with older intermediaries that might not implement
|
|
1374
|
-
// If-None-Match.
|
|
1375
1321
|
return false;
|
|
1376
1322
|
}
|
|
1377
1323
|
const date = new Date(clientLM);
|
|
1378
|
-
// A recipient MUST ignore the If-Modified-Since header field if the
|
|
1379
|
-
// received field-value is not a valid HTTP-date, or if the request
|
|
1380
|
-
// method is neither GET nor HEAD.
|
|
1381
1324
|
if (date.toString() !== 'Invalid Date' &&
|
|
1382
1325
|
date.getTime() > lastModified.getTime()) {
|
|
1383
1326
|
return true;
|
|
1384
1327
|
}
|
|
1385
1328
|
return false;
|
|
1386
1329
|
}
|
|
1387
|
-
function listDirectory(dirPath) {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
(d.size
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
'</ul></body></html>');
|
|
1433
|
-
});
|
|
1330
|
+
async function listDirectory(dirPath) {
|
|
1331
|
+
const { setContentType } = eventHttp.useSetHeaders();
|
|
1332
|
+
const { url } = eventHttp.useRequest();
|
|
1333
|
+
const list = await readdir(dirPath);
|
|
1334
|
+
const promises = [];
|
|
1335
|
+
let detailedList = [];
|
|
1336
|
+
for (const item of list) {
|
|
1337
|
+
promises.push({ name: item, promise: stat(path.join(dirPath, item)) });
|
|
1338
|
+
}
|
|
1339
|
+
for (const item of promises) {
|
|
1340
|
+
const data = await item.promise;
|
|
1341
|
+
detailedList.push({
|
|
1342
|
+
name: item.name,
|
|
1343
|
+
size: data.size,
|
|
1344
|
+
mtime: data.mtime,
|
|
1345
|
+
dir: data.isDirectory(),
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
detailedList = detailedList.sort((a, b) => a.dir === b.dir ? (a.name > b.name ? 1 : -1) : a.dir > b.dir ? -1 : 1);
|
|
1349
|
+
detailedList.unshift({ name: '..', dir: true });
|
|
1350
|
+
setContentType('text/html');
|
|
1351
|
+
const styles = '<style type="text/css">\nhtml { font-family: monospace }\n' +
|
|
1352
|
+
'span { padding: 0px 2px }\n' +
|
|
1353
|
+
'.text { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }\n' +
|
|
1354
|
+
'.icon { width: 20px; display: inline-block; text-align: center; }\n' +
|
|
1355
|
+
'.name { width: 250px; display: inline-block; }\n' +
|
|
1356
|
+
'.size { width: 80px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1357
|
+
'.date { width: 200px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1358
|
+
'\n</style>';
|
|
1359
|
+
return ('<html><head><title>Dir</title> ' +
|
|
1360
|
+
styles +
|
|
1361
|
+
' </head><body><ul>' +
|
|
1362
|
+
detailedList
|
|
1363
|
+
.map((d) => `<li> <span class="icon">${d.dir ? '📁' : '🗎'}</span>` +
|
|
1364
|
+
`<a href="${path.join(url || '', d.name)}"><span class="name text">${d.name}</span></a>` +
|
|
1365
|
+
`<span class="size text">${(d.size &&
|
|
1366
|
+
(d.size > 10000
|
|
1367
|
+
? Math.round(d.size / 1024 / 1024).toString() +
|
|
1368
|
+
'Mb'
|
|
1369
|
+
: Math.round(d.size / 1024).toString() +
|
|
1370
|
+
'Kb')) ||
|
|
1371
|
+
''}</span>` +
|
|
1372
|
+
`<span class="date text">${(d.mtime && d.mtime.toISOString()) || ''}</li>`)
|
|
1373
|
+
.join('\n') +
|
|
1374
|
+
'</ul></body></html>');
|
|
1434
1375
|
}
|
|
1435
1376
|
|
|
1436
1377
|
exports.serveFile = serveFile;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export { }
|
|
1
|
+
import { TCacheControl } from '@wooksjs/event-http';
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
|
|
4
|
+
interface TServeFileOptions {
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
cacheControl?: TCacheControl;
|
|
7
|
+
expires?: Date | string | number;
|
|
8
|
+
pragmaNoCache?: boolean;
|
|
9
|
+
baseDir?: string;
|
|
10
|
+
defaultExt?: string;
|
|
11
|
+
listDirectory?: boolean;
|
|
12
|
+
index?: string;
|
|
13
|
+
allowDotDot?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function serveFile(filePath: string, options?: TServeFileOptions): Promise<Readable | string | string[] | unknown>;
|
|
16
|
+
|
|
17
|
+
export { serveFile };
|
package/dist/index.mjs
CHANGED
|
@@ -2,38 +2,6 @@ import { useHttpContext, useResponse, useSetHeaders, useHeaders, useRequest, use
|
|
|
2
2
|
import { createReadStream, promises } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
/******************************************************************************
|
|
6
|
-
Copyright (c) Microsoft Corporation.
|
|
7
|
-
|
|
8
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
9
|
-
purpose with or without fee is hereby granted.
|
|
10
|
-
|
|
11
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
12
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
13
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
14
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
15
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
16
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
17
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
18
|
-
***************************************************************************** */
|
|
19
|
-
/* global Reflect, Promise, SuppressedError, Symbol */
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
23
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
24
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
25
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
26
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
27
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
28
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
33
|
-
var e = new Error(message);
|
|
34
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
5
|
const extensions = {
|
|
38
6
|
'123': 'application/vnd.lotus-1-2-3',
|
|
39
7
|
ez: 'application/andrew-inset',
|
|
@@ -1228,134 +1196,118 @@ function normalizePath(filePath, baseDir) {
|
|
|
1228
1196
|
}
|
|
1229
1197
|
|
|
1230
1198
|
const { stat, readdir } = promises;
|
|
1231
|
-
function serveFile(filePath, options = {}) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1199
|
+
async function serveFile(filePath, options = {}) {
|
|
1200
|
+
if (!options.allowDotDot && (/\/\.\.\//.test(filePath) || /^\.\.\//.test(filePath))) {
|
|
1201
|
+
throw new Error('Parent Traversal ("/../") is not allowed.');
|
|
1202
|
+
}
|
|
1203
|
+
const { restoreCtx } = useHttpContext();
|
|
1204
|
+
const { status } = useResponse();
|
|
1205
|
+
const { setHeader, removeHeader } = useSetHeaders();
|
|
1206
|
+
const headers = useHeaders();
|
|
1207
|
+
const { method, url } = useRequest();
|
|
1208
|
+
const { setCacheControl, setExpires, setPragmaNoCache } = useSetCacheControl();
|
|
1209
|
+
const normalizedPath = normalizePath(filePath, options.baseDir);
|
|
1210
|
+
let fileStats;
|
|
1211
|
+
try {
|
|
1212
|
+
fileStats = await stat(normalizedPath);
|
|
1213
|
+
}
|
|
1214
|
+
catch (e) {
|
|
1215
|
+
if (options.defaultExt) {
|
|
1216
|
+
const ext = path.extname(filePath);
|
|
1217
|
+
if (!ext) {
|
|
1218
|
+
restoreCtx();
|
|
1219
|
+
return serveFile(filePath + '.' + options.defaultExt);
|
|
1220
|
+
}
|
|
1235
1221
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1222
|
+
throw new HttpError(404);
|
|
1223
|
+
}
|
|
1224
|
+
status(200);
|
|
1225
|
+
const etag = `"${[
|
|
1226
|
+
fileStats.ino,
|
|
1227
|
+
fileStats.size,
|
|
1228
|
+
fileStats.mtime.toISOString(),
|
|
1229
|
+
].join('-')}"`;
|
|
1230
|
+
const lastModified = new Date(fileStats.mtime);
|
|
1231
|
+
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1232
|
+
status(304);
|
|
1233
|
+
return '';
|
|
1234
|
+
}
|
|
1235
|
+
setHeader('etag', etag);
|
|
1236
|
+
setHeader('last-modified', lastModified.toUTCString());
|
|
1237
|
+
if (options.cacheControl !== undefined) {
|
|
1238
|
+
setCacheControl(options.cacheControl);
|
|
1239
|
+
}
|
|
1240
|
+
if (options.expires) {
|
|
1241
|
+
setExpires(options.expires);
|
|
1242
|
+
}
|
|
1243
|
+
if (options.pragmaNoCache) {
|
|
1244
|
+
setPragmaNoCache(options.pragmaNoCache);
|
|
1245
|
+
}
|
|
1246
|
+
if (fileStats.isDirectory()) {
|
|
1247
|
+
if (options.listDirectory) {
|
|
1248
|
+
restoreCtx();
|
|
1249
|
+
return listDirectory(normalizedPath);
|
|
1246
1250
|
}
|
|
1247
|
-
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1251
|
+
else if (options.index) {
|
|
1252
|
+
if (filePath[filePath.length - 1] !== '/' &&
|
|
1253
|
+
url &&
|
|
1254
|
+
url[url.length - 1] !== '/') {
|
|
1255
|
+
return new BaseHttpResponse()
|
|
1256
|
+
.setStatus(302)
|
|
1257
|
+
.setHeader('location', url + '/');
|
|
1254
1258
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
const etag = `"${[
|
|
1261
|
-
fileStats.ino,
|
|
1262
|
-
fileStats.size,
|
|
1263
|
-
fileStats.mtime.toISOString(),
|
|
1264
|
-
].join('-')}"`;
|
|
1265
|
-
const lastModified = new Date(fileStats.mtime);
|
|
1266
|
-
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1267
|
-
status(304);
|
|
1268
|
-
return '';
|
|
1259
|
+
restoreCtx();
|
|
1260
|
+
return serveFile(path.join(filePath, options.index), {
|
|
1261
|
+
...options,
|
|
1262
|
+
index: '',
|
|
1263
|
+
});
|
|
1269
1264
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1265
|
+
removeHeader('etag');
|
|
1266
|
+
removeHeader('last-modified');
|
|
1267
|
+
throw new HttpError(404);
|
|
1268
|
+
}
|
|
1269
|
+
let range = headers.range;
|
|
1270
|
+
let start, end;
|
|
1271
|
+
let size = fileStats.size;
|
|
1272
|
+
if (range) {
|
|
1273
|
+
const rangeParts = range
|
|
1274
|
+
.trim()
|
|
1275
|
+
.replace(/bytes=/, '')
|
|
1276
|
+
.split('-');
|
|
1277
|
+
const [s, e] = rangeParts;
|
|
1278
|
+
start = parseInt(s);
|
|
1279
|
+
end = e ? parseInt(e) : size - 1;
|
|
1280
|
+
end = Math.min(size - 1, end);
|
|
1281
|
+
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1282
|
+
throw new HttpError(416);
|
|
1275
1283
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1284
|
+
size = end - start + 1;
|
|
1285
|
+
const ifRange = headers['if-range'] || '';
|
|
1286
|
+
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1287
|
+
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1288
|
+
if (ifRange &&
|
|
1289
|
+
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1290
|
+
status(200);
|
|
1291
|
+
size = fileStats.size;
|
|
1292
|
+
range = '';
|
|
1278
1293
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1294
|
+
else {
|
|
1295
|
+
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1296
|
+
status(206);
|
|
1281
1297
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
url &&
|
|
1290
|
-
url[url.length - 1] !== '/') {
|
|
1291
|
-
return new BaseHttpResponse()
|
|
1292
|
-
.setStatus(302)
|
|
1293
|
-
.setHeader('location', url + '/');
|
|
1294
|
-
}
|
|
1295
|
-
restoreCtx();
|
|
1296
|
-
return serveFile(path.join(filePath, options.index), Object.assign(Object.assign({}, options), { index: '' }));
|
|
1297
|
-
}
|
|
1298
|
-
removeHeader('etag');
|
|
1299
|
-
removeHeader('last-modified');
|
|
1300
|
-
throw new HttpError(404);
|
|
1301
|
-
}
|
|
1302
|
-
// range header processing start
|
|
1303
|
-
let range = headers.range;
|
|
1304
|
-
let start, end;
|
|
1305
|
-
let size = fileStats.size;
|
|
1306
|
-
if (range) {
|
|
1307
|
-
const rangeParts = range
|
|
1308
|
-
.trim()
|
|
1309
|
-
.replace(/bytes=/, '')
|
|
1310
|
-
.split('-');
|
|
1311
|
-
const [s, e] = rangeParts;
|
|
1312
|
-
start = parseInt(s);
|
|
1313
|
-
end = e ? parseInt(e) : size - 1;
|
|
1314
|
-
end = Math.min(size - 1, end);
|
|
1315
|
-
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1316
|
-
throw new HttpError(416);
|
|
1317
|
-
}
|
|
1318
|
-
size = end - start + 1;
|
|
1319
|
-
// if-range processing start
|
|
1320
|
-
// rfc7233#section-3.2\
|
|
1321
|
-
const ifRange = headers['if-range'] || '';
|
|
1322
|
-
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1323
|
-
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1324
|
-
if (ifRange &&
|
|
1325
|
-
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1326
|
-
// If the validator does not match, the server MUST ignore
|
|
1327
|
-
// the Range header field.
|
|
1328
|
-
status(200);
|
|
1329
|
-
size = fileStats.size;
|
|
1330
|
-
range = '';
|
|
1331
|
-
}
|
|
1332
|
-
else {
|
|
1333
|
-
// If the validator given in the If-Range header field matches the
|
|
1334
|
-
// current validator for the selected representation of the target
|
|
1335
|
-
// resource, then the server SHOULD process the Range header field as
|
|
1336
|
-
// requested.
|
|
1337
|
-
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1338
|
-
status(206);
|
|
1339
|
-
}
|
|
1340
|
-
// if-range processing end
|
|
1341
|
-
}
|
|
1342
|
-
// range header processing end
|
|
1343
|
-
setHeader('accept-ranges', 'bytes');
|
|
1344
|
-
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1345
|
-
setHeader('content-length', size);
|
|
1346
|
-
if (options.headers) {
|
|
1347
|
-
for (const header of Object.keys(options.headers)) {
|
|
1348
|
-
setHeader(header, options.headers[header]);
|
|
1349
|
-
}
|
|
1298
|
+
}
|
|
1299
|
+
setHeader('accept-ranges', 'bytes');
|
|
1300
|
+
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1301
|
+
setHeader('content-length', size);
|
|
1302
|
+
if (options.headers) {
|
|
1303
|
+
for (const header of Object.keys(options.headers)) {
|
|
1304
|
+
setHeader(header, options.headers[header]);
|
|
1350
1305
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1306
|
+
}
|
|
1307
|
+
return method === 'HEAD'
|
|
1308
|
+
? ''
|
|
1309
|
+
: createReadStream(normalizedPath, !!range ? { start, end } : undefined);
|
|
1355
1310
|
}
|
|
1356
|
-
// function toWeak(etag: string): string {
|
|
1357
|
-
// return `W/${etag}`
|
|
1358
|
-
// }
|
|
1359
1311
|
function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
1360
1312
|
if (clientEtag) {
|
|
1361
1313
|
const parts = clientEtag.split(',').map((v) => v.trim());
|
|
@@ -1364,71 +1316,60 @@ function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
|
1364
1316
|
return true;
|
|
1365
1317
|
}
|
|
1366
1318
|
}
|
|
1367
|
-
// A recipient MUST ignore If-Modified-Since if the request contains an
|
|
1368
|
-
// If-None-Match header field; the condition in If-None-Match is
|
|
1369
|
-
// considered to be a more accurate replacement for the condition in
|
|
1370
|
-
// If-Modified-Since, and the two are only combined for the sake of
|
|
1371
|
-
// interoperating with older intermediaries that might not implement
|
|
1372
|
-
// If-None-Match.
|
|
1373
1319
|
return false;
|
|
1374
1320
|
}
|
|
1375
1321
|
const date = new Date(clientLM);
|
|
1376
|
-
// A recipient MUST ignore the If-Modified-Since header field if the
|
|
1377
|
-
// received field-value is not a valid HTTP-date, or if the request
|
|
1378
|
-
// method is neither GET nor HEAD.
|
|
1379
1322
|
if (date.toString() !== 'Invalid Date' &&
|
|
1380
1323
|
date.getTime() > lastModified.getTime()) {
|
|
1381
1324
|
return true;
|
|
1382
1325
|
}
|
|
1383
1326
|
return false;
|
|
1384
1327
|
}
|
|
1385
|
-
function listDirectory(dirPath) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
(d.size
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
'</ul></body></html>');
|
|
1431
|
-
});
|
|
1328
|
+
async function listDirectory(dirPath) {
|
|
1329
|
+
const { setContentType } = useSetHeaders();
|
|
1330
|
+
const { url } = useRequest();
|
|
1331
|
+
const list = await readdir(dirPath);
|
|
1332
|
+
const promises = [];
|
|
1333
|
+
let detailedList = [];
|
|
1334
|
+
for (const item of list) {
|
|
1335
|
+
promises.push({ name: item, promise: stat(path.join(dirPath, item)) });
|
|
1336
|
+
}
|
|
1337
|
+
for (const item of promises) {
|
|
1338
|
+
const data = await item.promise;
|
|
1339
|
+
detailedList.push({
|
|
1340
|
+
name: item.name,
|
|
1341
|
+
size: data.size,
|
|
1342
|
+
mtime: data.mtime,
|
|
1343
|
+
dir: data.isDirectory(),
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
detailedList = detailedList.sort((a, b) => a.dir === b.dir ? (a.name > b.name ? 1 : -1) : a.dir > b.dir ? -1 : 1);
|
|
1347
|
+
detailedList.unshift({ name: '..', dir: true });
|
|
1348
|
+
setContentType('text/html');
|
|
1349
|
+
const styles = '<style type="text/css">\nhtml { font-family: monospace }\n' +
|
|
1350
|
+
'span { padding: 0px 2px }\n' +
|
|
1351
|
+
'.text { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }\n' +
|
|
1352
|
+
'.icon { width: 20px; display: inline-block; text-align: center; }\n' +
|
|
1353
|
+
'.name { width: 250px; display: inline-block; }\n' +
|
|
1354
|
+
'.size { width: 80px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1355
|
+
'.date { width: 200px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1356
|
+
'\n</style>';
|
|
1357
|
+
return ('<html><head><title>Dir</title> ' +
|
|
1358
|
+
styles +
|
|
1359
|
+
' </head><body><ul>' +
|
|
1360
|
+
detailedList
|
|
1361
|
+
.map((d) => `<li> <span class="icon">${d.dir ? '📁' : '🗎'}</span>` +
|
|
1362
|
+
`<a href="${path.join(url || '', d.name)}"><span class="name text">${d.name}</span></a>` +
|
|
1363
|
+
`<span class="size text">${(d.size &&
|
|
1364
|
+
(d.size > 10000
|
|
1365
|
+
? Math.round(d.size / 1024 / 1024).toString() +
|
|
1366
|
+
'Mb'
|
|
1367
|
+
: Math.round(d.size / 1024).toString() +
|
|
1368
|
+
'Kb')) ||
|
|
1369
|
+
''}</span>` +
|
|
1370
|
+
`<span class="date text">${(d.mtime && d.mtime.toISOString()) || ''}</li>`)
|
|
1371
|
+
.join('\n') +
|
|
1372
|
+
'</ul></body></html>');
|
|
1432
1373
|
}
|
|
1433
1374
|
|
|
1434
1375
|
export { serveFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/http-static",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "@wooksjs/http-static",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
|
+
"exports": {
|
|
12
|
+
"./package.json": "./package.json",
|
|
13
|
+
".": {
|
|
14
|
+
"require": "./dist/index.cjs",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"types": "./dist/index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
11
19
|
"repository": {
|
|
12
20
|
"type": "git",
|
|
13
21
|
"url": "git+https://github.com/wooksjs/wooksjs.git",
|
|
@@ -31,7 +39,7 @@
|
|
|
31
39
|
"url": "https://github.com/wooksjs/wooksjs/issues"
|
|
32
40
|
},
|
|
33
41
|
"peerDependencies": {
|
|
34
|
-
"@wooksjs/event-http": "0.4.
|
|
42
|
+
"@wooksjs/event-http": "0.4.12"
|
|
35
43
|
},
|
|
36
44
|
"homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/http-static#readme"
|
|
37
45
|
}
|