lieko-express 0.0.13 → 0.0.15
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/helpers/mimes.js +65 -0
- package/lib/static.js +2 -60
- package/lieko-express.js +155 -12
- package/package.json +2 -1
package/helpers/mimes.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const mimeTypes = {
|
|
4
|
+
'.html': 'text/html; charset=utf-8',
|
|
5
|
+
'.htm': 'text/html; charset=utf-8',
|
|
6
|
+
'.css': 'text/css; charset=utf-8',
|
|
7
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
8
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
9
|
+
'.json': 'application/json; charset=utf-8',
|
|
10
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
11
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
12
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.jpeg': 'image/jpeg',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.gif': 'image/gif',
|
|
17
|
+
'.svg': 'image/svg+xml',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
'.ico': 'image/x-icon',
|
|
20
|
+
'.bmp': 'image/bmp',
|
|
21
|
+
'.tiff': 'image/tiff',
|
|
22
|
+
'.tif': 'image/tiff',
|
|
23
|
+
'.mp3': 'audio/mpeg',
|
|
24
|
+
'.wav': 'audio/wav',
|
|
25
|
+
'.ogg': 'audio/ogg',
|
|
26
|
+
'.m4a': 'audio/mp4',
|
|
27
|
+
'.aac': 'audio/aac',
|
|
28
|
+
'.flac': 'audio/flac',
|
|
29
|
+
'.mp4': 'video/mp4',
|
|
30
|
+
'.webm': 'video/webm',
|
|
31
|
+
'.ogv': 'video/ogg',
|
|
32
|
+
'.avi': 'video/x-msvideo',
|
|
33
|
+
'.mov': 'video/quicktime',
|
|
34
|
+
'.wmv': 'video/x-ms-wmv',
|
|
35
|
+
'.flv': 'video/x-flv',
|
|
36
|
+
'.mkv': 'video/x-matroska',
|
|
37
|
+
'.woff': 'font/woff',
|
|
38
|
+
'.woff2': 'font/woff2',
|
|
39
|
+
'.ttf': 'font/ttf',
|
|
40
|
+
'.otf': 'font/otf',
|
|
41
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
42
|
+
'.zip': 'application/zip',
|
|
43
|
+
'.rar': 'application/x-rar-compressed',
|
|
44
|
+
'.tar': 'application/x-tar',
|
|
45
|
+
'.gz': 'application/gzip',
|
|
46
|
+
'.7z': 'application/x-7z-compressed',
|
|
47
|
+
'.pdf': 'application/pdf',
|
|
48
|
+
'.doc': 'application/msword',
|
|
49
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
50
|
+
'.xls': 'application/vnd.ms-excel',
|
|
51
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
52
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
53
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
54
|
+
'.wasm': 'application/wasm',
|
|
55
|
+
'.csv': 'text/csv; charset=utf-8'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getMimeType = (filePath) => {
|
|
59
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
60
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
getMimeType
|
|
65
|
+
}
|
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;
|
|
@@ -1245,9 +1247,7 @@ ${cyan} (req, res, next) => {
|
|
|
1245
1247
|
|
|
1246
1248
|
const buildHeaders = (contentType, length) => {
|
|
1247
1249
|
const poweredBy = this.settings['x-powered-by'];
|
|
1248
|
-
const
|
|
1249
|
-
? { 'X-Powered-By': poweredBy === true ? 'lieko-express' : poweredBy }
|
|
1250
|
-
: {};
|
|
1250
|
+
const shouldShowPoweredBy = poweredBy !== false;
|
|
1251
1251
|
|
|
1252
1252
|
return {
|
|
1253
1253
|
'Content-Type': contentType,
|
|
@@ -1255,7 +1255,11 @@ ${cyan} (req, res, next) => {
|
|
|
1255
1255
|
'Date': getDateHeader(),
|
|
1256
1256
|
'Connection': 'keep-alive',
|
|
1257
1257
|
'Cache-Control': 'no-store',
|
|
1258
|
-
...
|
|
1258
|
+
...(shouldShowPoweredBy && {
|
|
1259
|
+
'X-Powered-By': poweredBy === true || poweredBy === undefined
|
|
1260
|
+
? 'lieko-express'
|
|
1261
|
+
: poweredBy
|
|
1262
|
+
})
|
|
1259
1263
|
};
|
|
1260
1264
|
};
|
|
1261
1265
|
|
|
@@ -1421,6 +1425,137 @@ ${cyan} (req, res, next) => {
|
|
|
1421
1425
|
return res.end(body);
|
|
1422
1426
|
};
|
|
1423
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
|
+
|
|
1424
1559
|
res.html = function (html, status) {
|
|
1425
1560
|
res.statusCode = status !== undefined ? status : (statusCode || 200);
|
|
1426
1561
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
@@ -1532,18 +1667,26 @@ ${cyan} (req, res, next) => {
|
|
|
1532
1667
|
httpOnly: true,
|
|
1533
1668
|
secure: req.secure || false,
|
|
1534
1669
|
sameSite: 'lax',
|
|
1535
|
-
...options
|
|
1670
|
+
...options,
|
|
1671
|
+
expires: new Date(1),
|
|
1672
|
+
maxAge: 0
|
|
1536
1673
|
};
|
|
1537
1674
|
|
|
1538
|
-
|
|
1675
|
+
let cookieString = `${name}=; Path=${opts.path}; Expires=${opts.expires.toUTCString()}; Max-Age=0`;
|
|
1539
1676
|
|
|
1540
|
-
|
|
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
|
+
}
|
|
1541
1686
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
if (opts.sameSite && opts.sameSite !== 'none') header += `; SameSite=${opts.sameSite}`;
|
|
1687
|
+
existingHeaders.push(cookieString);
|
|
1688
|
+
res.setHeader('Set-Cookie', existingHeaders);
|
|
1545
1689
|
|
|
1546
|
-
res.setHeader('Set-Cookie', header);
|
|
1547
1690
|
return res;
|
|
1548
1691
|
};
|
|
1549
1692
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lieko-express",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/eiwSrvt/lieko-express"
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"lieko-express.js",
|
|
20
20
|
"lieko-express.d.ts",
|
|
21
21
|
"README.md",
|
|
22
|
+
"helpers",
|
|
22
23
|
"lib"
|
|
23
24
|
]
|
|
24
25
|
}
|