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 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 poweredByHeader = poweredBy
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
- ...poweredByHeader
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
- 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`;
1538
1676
 
1539
- 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
+ }
1540
1686
 
1541
- if (opts.httpOnly) header += '; HttpOnly';
1542
- if (opts.secure) header += '; Secure';
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lieko-express",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/eiwSrvt/lieko-express"