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.
@@ -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 poweredByHeader = poweredBy
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
- ...poweredByHeader
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
- const cookieValue = `${name}=; Max-Age=0; Expires=${new Date(0).toUTCString()}; Path=${opts.path}`;
1675
+ let cookieString = `${name}=; Path=${opts.path}; Expires=${opts.expires.toUTCString()}; Max-Age=0`;
1539
1676
 
1540
- let header = cookieValue;
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
- if (opts.httpOnly) header += '; HttpOnly';
1543
- if (opts.secure) header += '; Secure';
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.13",
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
  }