lieko-express 0.0.12 → 0.0.14
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/lib/static.js +2 -60
- package/lieko-express.js +156 -12
- package/package.json +1 -1
package/lib/static.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
+
const { getMimeType } = require('../helpers/mimes');
|
|
5
|
+
|
|
4
6
|
module.exports = function serveStatic(root, options = {}) {
|
|
5
7
|
const opts = {
|
|
6
8
|
maxAge: options.maxAge || 0,
|
|
@@ -16,66 +18,6 @@ module.exports = function serveStatic(root, options = {}) {
|
|
|
16
18
|
cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
|
|
17
19
|
};
|
|
18
20
|
|
|
19
|
-
const mimeTypes = {
|
|
20
|
-
'.html': 'text/html; charset=utf-8',
|
|
21
|
-
'.htm': 'text/html; charset=utf-8',
|
|
22
|
-
'.css': 'text/css; charset=utf-8',
|
|
23
|
-
'.js': 'application/javascript; charset=utf-8',
|
|
24
|
-
'.mjs': 'application/javascript; charset=utf-8',
|
|
25
|
-
'.json': 'application/json; charset=utf-8',
|
|
26
|
-
'.xml': 'application/xml; charset=utf-8',
|
|
27
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
28
|
-
'.md': 'text/markdown; charset=utf-8',
|
|
29
|
-
'.jpg': 'image/jpeg',
|
|
30
|
-
'.jpeg': 'image/jpeg',
|
|
31
|
-
'.png': 'image/png',
|
|
32
|
-
'.gif': 'image/gif',
|
|
33
|
-
'.svg': 'image/svg+xml',
|
|
34
|
-
'.webp': 'image/webp',
|
|
35
|
-
'.ico': 'image/x-icon',
|
|
36
|
-
'.bmp': 'image/bmp',
|
|
37
|
-
'.tiff': 'image/tiff',
|
|
38
|
-
'.tif': 'image/tiff',
|
|
39
|
-
'.mp3': 'audio/mpeg',
|
|
40
|
-
'.wav': 'audio/wav',
|
|
41
|
-
'.ogg': 'audio/ogg',
|
|
42
|
-
'.m4a': 'audio/mp4',
|
|
43
|
-
'.aac': 'audio/aac',
|
|
44
|
-
'.flac': 'audio/flac',
|
|
45
|
-
'.mp4': 'video/mp4',
|
|
46
|
-
'.webm': 'video/webm',
|
|
47
|
-
'.ogv': 'video/ogg',
|
|
48
|
-
'.avi': 'video/x-msvideo',
|
|
49
|
-
'.mov': 'video/quicktime',
|
|
50
|
-
'.wmv': 'video/x-ms-wmv',
|
|
51
|
-
'.flv': 'video/x-flv',
|
|
52
|
-
'.mkv': 'video/x-matroska',
|
|
53
|
-
'.woff': 'font/woff',
|
|
54
|
-
'.woff2': 'font/woff2',
|
|
55
|
-
'.ttf': 'font/ttf',
|
|
56
|
-
'.otf': 'font/otf',
|
|
57
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
58
|
-
'.zip': 'application/zip',
|
|
59
|
-
'.rar': 'application/x-rar-compressed',
|
|
60
|
-
'.tar': 'application/x-tar',
|
|
61
|
-
'.gz': 'application/gzip',
|
|
62
|
-
'.7z': 'application/x-7z-compressed',
|
|
63
|
-
'.pdf': 'application/pdf',
|
|
64
|
-
'.doc': 'application/msword',
|
|
65
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
66
|
-
'.xls': 'application/vnd.ms-excel',
|
|
67
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
68
|
-
'.ppt': 'application/vnd.ms-powerpoint',
|
|
69
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
70
|
-
'.wasm': 'application/wasm',
|
|
71
|
-
'.csv': 'text/csv; charset=utf-8'
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const getMimeType = (filePath) => {
|
|
75
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
76
|
-
return mimeTypes[ext] || 'application/octet-stream';
|
|
77
|
-
};
|
|
78
|
-
|
|
79
21
|
const generateETag = (stats) => {
|
|
80
22
|
const mtime = stats.mtime.getTime().toString(16);
|
|
81
23
|
const size = stats.size.toString(16);
|
package/lieko-express.js
CHANGED
|
@@ -3,6 +3,8 @@ const net = require("net");
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
|
+
const { getMimeType } = require('./helpers/mimes');
|
|
7
|
+
|
|
6
8
|
const {
|
|
7
9
|
Schema,
|
|
8
10
|
ValidationError,
|
|
@@ -1029,7 +1031,7 @@ ${cyan} (req, res, next) => {
|
|
|
1029
1031
|
|
|
1030
1032
|
if (!route) {
|
|
1031
1033
|
if (this.notFoundHandler) return this.notFoundHandler(req, res);
|
|
1032
|
-
return res.status(404).json({ error: 'Route not found' });
|
|
1034
|
+
return res.status(404).json({ success: false, error: { message: 'Route not found', code: 404 } });
|
|
1033
1035
|
}
|
|
1034
1036
|
|
|
1035
1037
|
req.params = route.params;
|
|
@@ -1105,6 +1107,7 @@ ${cyan} (req, res, next) => {
|
|
|
1105
1107
|
}
|
|
1106
1108
|
|
|
1107
1109
|
req.originalUrl = req.url;
|
|
1110
|
+
req.path = req.url.split('?')[0];
|
|
1108
1111
|
req.xhr = (req.headers['x-requested-with'] || '').toLowerCase() === 'xmlhttprequest';
|
|
1109
1112
|
|
|
1110
1113
|
req.get = (name) => {
|
|
@@ -1244,9 +1247,7 @@ ${cyan} (req, res, next) => {
|
|
|
1244
1247
|
|
|
1245
1248
|
const buildHeaders = (contentType, length) => {
|
|
1246
1249
|
const poweredBy = this.settings['x-powered-by'];
|
|
1247
|
-
const
|
|
1248
|
-
? { 'X-Powered-By': poweredBy === true ? 'lieko-express' : poweredBy }
|
|
1249
|
-
: {};
|
|
1250
|
+
const shouldShowPoweredBy = poweredBy !== false;
|
|
1250
1251
|
|
|
1251
1252
|
return {
|
|
1252
1253
|
'Content-Type': contentType,
|
|
@@ -1254,7 +1255,11 @@ ${cyan} (req, res, next) => {
|
|
|
1254
1255
|
'Date': getDateHeader(),
|
|
1255
1256
|
'Connection': 'keep-alive',
|
|
1256
1257
|
'Cache-Control': 'no-store',
|
|
1257
|
-
...
|
|
1258
|
+
...(shouldShowPoweredBy && {
|
|
1259
|
+
'X-Powered-By': poweredBy === true || poweredBy === undefined
|
|
1260
|
+
? 'lieko-express'
|
|
1261
|
+
: poweredBy
|
|
1262
|
+
})
|
|
1258
1263
|
};
|
|
1259
1264
|
};
|
|
1260
1265
|
|
|
@@ -1420,6 +1425,137 @@ ${cyan} (req, res, next) => {
|
|
|
1420
1425
|
return res.end(body);
|
|
1421
1426
|
};
|
|
1422
1427
|
|
|
1428
|
+
res.sendFile = async function (filePath, options = {}, callback) {
|
|
1429
|
+
if (responseSent) {
|
|
1430
|
+
if (callback) callback(new Error('Response already sent'));
|
|
1431
|
+
return res;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const opts = {
|
|
1435
|
+
maxAge: 0,
|
|
1436
|
+
lastModified: true,
|
|
1437
|
+
headers: {},
|
|
1438
|
+
dotfiles: 'ignore', // 'allow', 'deny', 'ignore'
|
|
1439
|
+
acceptRanges: true,
|
|
1440
|
+
root: null,
|
|
1441
|
+
...options
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
let file = filePath;
|
|
1445
|
+
if (opts.root) {
|
|
1446
|
+
file = path.join(opts.root, filePath);
|
|
1447
|
+
} else if (!path.isAbsolute(file)) {
|
|
1448
|
+
file = path.resolve(process.cwd(), file);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const base = opts.root || process.cwd();
|
|
1452
|
+
if (!file.startsWith(base + path.sep) && !file.startsWith(base)) {
|
|
1453
|
+
const err = new Error('Forbidden path');
|
|
1454
|
+
err.code = 'FORBIDDEN';
|
|
1455
|
+
return handleError(err, 403, 'Forbidden');
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const basename = path.basename(file);
|
|
1459
|
+
if (opts.dotfiles === 'ignore' && basename.startsWith('.')) {
|
|
1460
|
+
const err = new Error('File not found');
|
|
1461
|
+
err.code = 'ENOENT';
|
|
1462
|
+
return handleError(err, 404, 'Not Found');
|
|
1463
|
+
}
|
|
1464
|
+
if (opts.dotfiles === 'deny' && basename.startsWith('.')) {
|
|
1465
|
+
const err = new Error('Forbidden');
|
|
1466
|
+
err.code = 'FORBIDDEN';
|
|
1467
|
+
return handleError(err, 403, 'Forbidden');
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
try {
|
|
1471
|
+
const stat = await fs.promises.stat(file);
|
|
1472
|
+
if (!stat.isFile()) {
|
|
1473
|
+
const err = new Error('Not a file');
|
|
1474
|
+
err.code = 'ENOENT';
|
|
1475
|
+
return handleError(err, 404, 'Not Found');
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const contentType = getMimeType(file);
|
|
1479
|
+
const fileSize = stat.size;
|
|
1480
|
+
|
|
1481
|
+
let start = 0;
|
|
1482
|
+
let end = fileSize - 1;
|
|
1483
|
+
const range = req.headers.range;
|
|
1484
|
+
|
|
1485
|
+
if (range) {
|
|
1486
|
+
const parts = range.replace(/bytes=/, '').split('-');
|
|
1487
|
+
start = parseInt(parts[0], 10) || 0;
|
|
1488
|
+
end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
|
1489
|
+
|
|
1490
|
+
if (start >= fileSize || end < start || isNaN(start)) {
|
|
1491
|
+
res.status(416);
|
|
1492
|
+
res.setHeader('Content-Range', `bytes */${fileSize}`);
|
|
1493
|
+
return handleError(new Error('Range Not Satisfiable'), 416, 'Range Not Satisfiable');
|
|
1494
|
+
}
|
|
1495
|
+
end = Math.min(end, fileSize - 1);
|
|
1496
|
+
const chunkSize = end - start + 1;
|
|
1497
|
+
|
|
1498
|
+
res.status(206);
|
|
1499
|
+
res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
|
|
1500
|
+
res.setHeader('Content-Length', chunkSize);
|
|
1501
|
+
} else {
|
|
1502
|
+
res.setHeader('Content-Length', fileSize);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
res.setHeader('Content-Type', contentType);
|
|
1506
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
1507
|
+
if (opts.lastModified) res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
1508
|
+
if (opts.maxAge) res.setHeader('Cache-Control', `public, max-age=${opts.maxAge}`);
|
|
1509
|
+
Object.entries(opts.headers).forEach(([k, v]) => res.setHeader(k, v));
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
const stream = fs.createReadStream(file, { start, end });
|
|
1513
|
+
stream.on('error', err => {
|
|
1514
|
+
if (!responseSent) handleError(err, 500, 'Error reading file');
|
|
1515
|
+
});
|
|
1516
|
+
stream.on('end', () => {
|
|
1517
|
+
responseSent = true;
|
|
1518
|
+
if (callback) callback(null);
|
|
1519
|
+
});
|
|
1520
|
+
stream.pipe(res);
|
|
1521
|
+
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
handleError(err, err.code === 'ENOENT' ? 404 : 500);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function handleError(err, status = 500, defaultMessage = 'Server Error') {
|
|
1527
|
+
responseSent = true;
|
|
1528
|
+
let message = defaultMessage;
|
|
1529
|
+
let details = '';
|
|
1530
|
+
|
|
1531
|
+
if (err.code === 'ENOENT') {
|
|
1532
|
+
status = 404;
|
|
1533
|
+
message = 'File Not Found';
|
|
1534
|
+
details = `The file "${filePath}" does not exist.\nFull path tried: ${file}`;
|
|
1535
|
+
} else if (err.code === 'FORBIDDEN') {
|
|
1536
|
+
status = 403;
|
|
1537
|
+
message = 'Forbidden';
|
|
1538
|
+
details = `Access denied to the file "${filePath}".`;
|
|
1539
|
+
} else if (err.code === 'EACCES') {
|
|
1540
|
+
status = 403;
|
|
1541
|
+
message = 'Permission Denied';
|
|
1542
|
+
details = `No read permissions on "${filePath}".`;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if (err.stack) {
|
|
1546
|
+
details += `\n\nError: ${err.message}\nStack:\n${err.stack}`;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
res.status(status);
|
|
1550
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
1551
|
+
res.end(`${message}\n${details.trim()}`);
|
|
1552
|
+
|
|
1553
|
+
if (callback) callback(err);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return res;
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1423
1559
|
res.html = function (html, status) {
|
|
1424
1560
|
res.statusCode = status !== undefined ? status : (statusCode || 200);
|
|
1425
1561
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
@@ -1531,18 +1667,26 @@ ${cyan} (req, res, next) => {
|
|
|
1531
1667
|
httpOnly: true,
|
|
1532
1668
|
secure: req.secure || false,
|
|
1533
1669
|
sameSite: 'lax',
|
|
1534
|
-
...options
|
|
1670
|
+
...options,
|
|
1671
|
+
expires: new Date(1),
|
|
1672
|
+
maxAge: 0
|
|
1535
1673
|
};
|
|
1536
1674
|
|
|
1537
|
-
|
|
1675
|
+
let cookieString = `${name}=; Path=${opts.path}; Expires=${opts.expires.toUTCString()}; Max-Age=0`;
|
|
1538
1676
|
|
|
1539
|
-
|
|
1677
|
+
if (opts.httpOnly) cookieString += '; HttpOnly';
|
|
1678
|
+
if (opts.secure) cookieString += '; Secure';
|
|
1679
|
+
if (opts.sameSite) cookieString += `; SameSite=${opts.sameSite}`;
|
|
1680
|
+
if (opts.domain) cookieString += `; Domain=${opts.domain}`;
|
|
1681
|
+
|
|
1682
|
+
let existingHeaders = res.getHeader('Set-Cookie') || [];
|
|
1683
|
+
if (!Array.isArray(existingHeaders)) {
|
|
1684
|
+
existingHeaders = [existingHeaders];
|
|
1685
|
+
}
|
|
1540
1686
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
if (opts.sameSite && opts.sameSite !== 'none') header += `; SameSite=${opts.sameSite}`;
|
|
1687
|
+
existingHeaders.push(cookieString);
|
|
1688
|
+
res.setHeader('Set-Cookie', existingHeaders);
|
|
1544
1689
|
|
|
1545
|
-
res.setHeader('Set-Cookie', header);
|
|
1546
1690
|
return res;
|
|
1547
1691
|
};
|
|
1548
1692
|
|