dx-server 0.13.0 → 0.14.0-alpha.1

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.
Files changed (46) hide show
  1. package/README.md +397 -265
  2. package/lib/body.js +1 -1
  3. package/lib/body.js.map +1 -0
  4. package/lib/bodyHelpers.js +22 -9
  5. package/lib/bodyHelpers.js.map +1 -0
  6. package/lib/dx.d.ts +1 -1
  7. package/lib/dx.js +12 -6
  8. package/lib/dx.js.map +1 -0
  9. package/lib/dxHelpers.d.ts +2 -1
  10. package/lib/dxHelpers.js +105 -87
  11. package/lib/dxHelpers.js.map +1 -0
  12. package/lib/helpers.js.map +1 -0
  13. package/lib/index.d.ts +1 -1
  14. package/lib/index.js +1 -1
  15. package/lib/index.js.map +1 -0
  16. package/lib/logger.d.ts +3 -2
  17. package/lib/logger.js +54 -46
  18. package/lib/logger.js.map +1 -0
  19. package/lib/router.js +5 -5
  20. package/lib/router.js.map +1 -0
  21. package/lib/static.js +4 -3
  22. package/lib/static.js.map +1 -0
  23. package/lib/staticHelpers.d.ts +5 -1
  24. package/lib/staticHelpers.js +150 -134
  25. package/lib/staticHelpers.js.map +1 -0
  26. package/lib/stream.d.ts +1 -1
  27. package/lib/stream.js +11 -5
  28. package/lib/stream.js.map +1 -0
  29. package/lib/vendors/contentType.js +7 -30
  30. package/lib/vendors/contentType.js.map +1 -0
  31. package/lib/vendors/etag.d.ts +2 -2
  32. package/lib/vendors/etag.js +15 -25
  33. package/lib/vendors/etag.js.map +1 -0
  34. package/lib/vendors/fresh.js +10 -17
  35. package/lib/vendors/fresh.js.map +1 -0
  36. package/lib/vendors/mime.js +4 -4
  37. package/lib/vendors/mime.js.map +1 -0
  38. package/lib/vendors/mimeDb.d.ts +2544 -2544
  39. package/lib/vendors/mimeDb.js +7100 -7079
  40. package/lib/vendors/mimeDb.js.map +1 -0
  41. package/lib/vendors/mimeScore.js +10 -11
  42. package/lib/vendors/mimeScore.js.map +1 -0
  43. package/lib/vendors/rangeParser.d.ts +2 -10
  44. package/lib/vendors/rangeParser.js +16 -29
  45. package/lib/vendors/rangeParser.js.map +1 -0
  46. package/package.json +32 -27
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,MAAM,EAAC,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAsB3C,4DAA4D;AAC5D,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAU,CAAA;AA+B5G,SAAS,UAAU,CAClB,MAA0B,EAAE,6BAA6B;AACzD,MAAyC,EACzC,EAAC,MAAM,GAAG,EAAE,KAAmB,EAAE;IAEjC,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CACtC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,UAAU,CAAC,EAAC,QAAQ,EAAE,GAAG,MAAM,GAAG,OAAO,EAAE,EAAC,CAAC,EAAE,OAAO,CAAU,CAC7F,CAAA;IACD,OAAO,IAAI,CAAC,EAAE;QACb,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;QACpB,IAAI,MAAM,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,WAAW,EAAE;YAAE,OAAO,IAAI,EAAE,CAAA;QAC9E,KAAK,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC;YAC1D,qBAAqB;YACrB,gEAAgE;YAChE,0EAA0E;YAC1E,2EAA2E;YAE3E,sCAAsC;YACtC,2EAA2E;YAC3E,kBAAkB;YAClB,wDAAwD;YACxD,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,EAAC,QAAQ,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAC,CAAC,CAAA;YACrE,IAAI,OAAO;gBAAE,OAAO,OAAO,CAAC,EAAC,OAAO,EAAE,IAAI,EAAC,CAAC,CAAA;QAC7C,CAAC;QACD,OAAO,IAAI,EAAE,CAAA;IACd,CAAC,CAAA;AACF,CAAC;AACD,MAAM,CAAC,MAAM,MAAM,GAAG;IACrB,MAAM,CAAC,MAAc,EAAE,GAAG,MAAa;QACtC,OAAO,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ;YACnC,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;YACzD,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5D,CAAC;IACD,GAAG,CAAC,GAAG,MAAa;QACnB,OAAO,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ;YACnC,CAAC,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;YAC5D,CAAC,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;CACS,CAAA;AAEX,KAAK,MAAM,MAAM,IAAI,UAAU;IAAG,MAAc,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA","sourcesContent":["import {type Chainable, getReq} from './dx.js'\nimport {urlFromReq} from './bodyHelpers.js'\n\ninterface URLPatternOptions {\n\t// sensitive?: boolean // default false\n\t// strict?: boolean // default false. disallow trailing delimiter\n}\n\ninterface RouteContext {\n\tmatched: URLPatternResult\n\tnext(): any\n}\ninterface Route {\n\t(context: RouteContext): any\n}\ninterface Routes {\n\t[k: string]: Route\n}\n\ninterface RouterOptions extends URLPatternOptions {\n\tprefix?: string\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\nconst allMethods = ['get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch'] as const\n// typescript does not support method multi-signature for object properties\n// type Router = {\n// \t[K in typeof allMethods[number]]: ((routes: Routes, options?: RouterOptions) => Chainable)\n// \t| ((pattern: string, route: Route, options?: RouterOptions) => Chainable)\n// } & {\ntype Router = {\n\tpatch(routes: Routes, options?: RouterOptions): Chainable\n\tpatch(pattern: string, route: Route, options?: RouterOptions): Chainable\n\ttrace(routes: Routes, options?: RouterOptions): Chainable\n\ttrace(pattern: string, route: Route, options?: RouterOptions): Chainable\n\toptions(routes: Routes, options?: RouterOptions): Chainable\n\toptions(pattern: string, route: Route, options?: RouterOptions): Chainable\n\tconnect(routes: Routes, options?: RouterOptions): Chainable\n\tconnect(pattern: string, route: Route, options?: RouterOptions): Chainable\n\tdelete(routes: Routes, options?: RouterOptions): Chainable\n\tdelete(pattern: string, route: Route, options?: RouterOptions): Chainable\n\tput(routes: Routes, options?: RouterOptions): Chainable\n\tput(pattern: string, route: Route, options?: RouterOptions): Chainable\n\tpost(routes: Routes, options?: RouterOptions): Chainable\n\tpost(pattern: string, route: Route, options?: RouterOptions): Chainable\n\thead(routes: Routes, options?: RouterOptions): Chainable\n\thead(pattern: string, route: Route, options?: RouterOptions): Chainable\n\tget(routes: Routes, options?: RouterOptions): Chainable\n\tget(pattern: string, route: Route, options?: RouterOptions): Chainable\n\tall(routes: Routes, options?: RouterOptions): Chainable\n\tall(pattern: string, route: Route, options?: RouterOptions): Chainable\n\tmethod(method: string, routes: Routes, options?: RouterOptions): Chainable\n\tmethod(method: string, pattern: string, route: Route, options?: RouterOptions): Chainable\n}\n\nfunction makeRouter(\n\tmethod: string | undefined, // undefined means any method\n\troutes: [pattern: string, route: Route][],\n\t{prefix = ''}: RouterOptions = {},\n): Chainable {\n\tconst routeWithUrlPatterns = routes.map(\n\t\t([pattern, handler]) => [new URLPattern({pathname: `${prefix}${pattern}`}), handler] as const,\n\t)\n\treturn next => {\n\t\tconst req = getReq()\n\t\tif (method !== undefined && req.method !== method.toUpperCase()) return next()\n\t\tfor (const [urlPattern, handler] of routeWithUrlPatterns) {\n\t\t\t// '' matches nothing\n\t\t\t// '/' matches both https://example.com and https://example.com/\n\t\t\t// '/foo' matches https://example.com/foo but not https://example.com/foo/\n\t\t\t// '/foo/' matches https://example.com/foo/ but not https://example.com/foo\n\n\t\t\t// path can be '*' for OPTIONS request\n\t\t\t// to test: curl -X OPTIONS --request-target '*' http://localhost:3000 -D -\n\t\t\t// req.url === '*'\n\t\t\t// new URL('*', 'https://example.com').pathname === '/*'\n\t\t\tconst matched = urlPattern.exec({pathname: urlFromReq(req).pathname})\n\t\t\tif (matched) return handler({matched, next})\n\t\t}\n\t\treturn next()\n\t}\n}\nexport const router = {\n\tmethod(method: string, ...params: any[]) {\n\t\treturn typeof params[0] === 'string'\n\t\t\t? makeRouter(method, [[params[0], params[1]]], params[2])\n\t\t\t: makeRouter(method, Object.entries(params[0]), params[1])\n\t},\n\tall(...params: any[]) {\n\t\treturn typeof params[0] === 'string'\n\t\t\t? makeRouter(undefined, [[params[0], params[1]]], params[2])\n\t\t\t: makeRouter(undefined, Object.entries(params[0]), params[1])\n\t},\n} as Router\n\nfor (const method of allMethods) (router as any)[method] = router.method.bind(router, method)\n"]}
package/lib/static.js CHANGED
@@ -11,12 +11,13 @@ export function chainStatic(pattern, { getPathname, ...options }) {
11
11
  const matched = urlPattern.exec({ pathname });
12
12
  if (!matched)
13
13
  return next();
14
+ const res = getRes();
14
15
  try {
15
- await sendFileTrusted(req, getRes(), getPathname?.(matched)
16
- ?? decodeURIComponent(pathname), options);
16
+ await sendFileTrusted(req, res, getPathname?.(matched) ?? decodeURIComponent(pathname), options);
17
17
  }
18
18
  catch (e) {
19
- return next(e); // if request's pathname matches pattern, but file is not found, next() will be called with error
19
+ if (!res.headersSent)
20
+ return next(e); // pre-stream error: user error middleware can still respond
20
21
  }
21
22
  };
22
23
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static.js","sourceRoot":"","sources":["../src/static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,MAAM,EAAE,MAAM,EAAC,MAAM,SAAS,CAAA;AACtD,OAAO,EAAuB,eAAe,EAAC,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAE3C,MAAM,UAAU,WAAW,CAC1B,OAAe,EACf,EACC,WAAW,EACX,GAAG,OAAO,EAKV;IAED,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,EAAC,QAAQ,EAAE,OAAO,EAAC,CAAC,CAAA;IACtD,OAAO,KAAK,EAAC,IAAI,EAAC,EAAE;QACnB,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;QACpB,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,IAAI,EAAE,CAAA;QAEhE,MAAM,EAAC,QAAQ,EAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;QAClC,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,EAAC,QAAQ,EAAC,CAAC,CAAA;QAC3C,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,EAAE,CAAA;QAE3B,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;QACpB,IAAI,CAAC;YACJ,MAAM,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAA;QACjG,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACZ,IAAI,CAAC,GAAG,CAAC,WAAW;gBAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,4DAA4D;QAClG,CAAC;IACF,CAAC,CAAA;AACF,CAAC","sourcesContent":["import {type Chainable, getReq, getRes} from './dx.js'\nimport {type SendFileOptions, sendFileTrusted} from './staticHelpers.js'\nimport {urlFromReq} from './bodyHelpers.js'\n\nexport function chainStatic(\n\tpattern: string,\n\t{\n\t\tgetPathname,\n\t\t...options\n\t}: SendFileOptions & {\n\t\t// return URI-encoded pathname\n\t\t// by default: get the full path\n\t\tgetPathname?(matched: any): string // should keep the heading slash\n\t},\n): Chainable {\n\tconst urlPattern = new URLPattern({pathname: pattern})\n\treturn async next => {\n\t\tconst req = getReq()\n\t\tif (req.method !== 'GET' && req.method !== 'HEAD') return next()\n\n\t\tconst {pathname} = urlFromReq(req)\n\t\tconst matched = urlPattern.exec({pathname})\n\t\tif (!matched) return next()\n\n\t\tconst res = getRes()\n\t\ttry {\n\t\t\tawait sendFileTrusted(req, res, getPathname?.(matched) ?? decodeURIComponent(pathname), options)\n\t\t} catch (e) {\n\t\t\tif (!res.headersSent) return next(e) // pre-stream error: user error middleware can still respond\n\t\t}\n\t}\n}\n"]}
@@ -1,4 +1,7 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
+ export type HttpError = Error & {
3
+ statusCode: number;
4
+ };
2
5
  export interface SendFileOptions {
3
6
  allowDotfiles?: boolean;
4
7
  root?: string;
@@ -8,9 +11,10 @@ export interface SendFileOptions {
8
11
  disableCacheControl?: boolean;
9
12
  maxAge?: number;
10
13
  immutable?: boolean;
14
+ disableFollowSymlinks?: boolean;
11
15
  end?: number;
12
16
  start?: number;
13
17
  }
14
18
  export declare function sendFileTrusted(req: IncomingMessage, res: ServerResponse, pathname: string, // plain path, not URI-encoded
15
19
  { root, allowDotfiles, start, end, disableAcceptRanges, disableLastModified, etag, disableCacheControl, maxAge, // 1 year
16
- immutable, }?: SendFileOptions): Promise<void>;
20
+ immutable, disableFollowSymlinks, }?: SendFileOptions): Promise<undefined>;
@@ -1,27 +1,31 @@
1
1
  import path from 'node:path';
2
- import { stat } from 'node:fs/promises';
2
+ import { open, realpath } from 'node:fs/promises';
3
3
  import { entityTagPath, statTag } from './vendors/etag.js';
4
4
  import { contentTypeForExtension } from './vendors/mime.js';
5
5
  import { fresh, parseHttpDate, parseTokenList } from './vendors/fresh.js';
6
6
  import { parseRange } from './vendors/rangeParser.js';
7
- import { createReadStream } from 'node:fs';
8
- import { onFinished } from './vendors/onFinished.js';
9
7
  import { promisify } from 'node:util';
10
- const BYTES_RANGE_REGEXP = /^ *bytes=/;
11
- const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
8
+ import { pipeline } from 'node:stream/promises';
9
+ function httpError(message, statusCode) {
10
+ const e = new Error(message);
11
+ e.statusCode = statusCode;
12
+ return e;
13
+ }
14
+ const bytesRangeRegexp = /^ *bytes=/;
15
+ const upPathRegexp = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
12
16
  export async function sendFileTrusted(req, res, pathname, // plain path, not URI-encoded
13
- { root, allowDotfiles, start = 0, end, disableAcceptRanges, disableLastModified, etag = 'strong', disableCacheControl, maxAge = 60 * 60 * 24 * 365 * 1000, // 1 year
14
- immutable, } = {}) {
17
+ { root, allowDotfiles, start = 0, end, disableAcceptRanges, disableLastModified, etag = 'weak', disableCacheControl, maxAge = 60 * 60 * 24 * 365 * 1000, // 1 year
18
+ immutable, disableFollowSymlinks, } = {}) {
15
19
  // null byte(s)
16
20
  if (pathname.includes('\0'))
17
- throw new Error('Forbidden');
21
+ throw httpError('Forbidden', 403);
18
22
  let parts;
19
23
  if (root) {
20
24
  // normalize
21
25
  pathname = path.normalize(`.${path.sep}${pathname}`);
22
26
  // malicious path
23
- if (UP_PATH_REGEXP.test(pathname))
24
- throw new Error('Forbidden');
27
+ if (upPathRegexp.test(pathname))
28
+ throw httpError('Forbidden', 403);
25
29
  // explode path parts
26
30
  parts = pathname.split(path.sep);
27
31
  // join / normalize from optional root dir
@@ -29,8 +33,8 @@ immutable, } = {}) {
29
33
  }
30
34
  else {
31
35
  // malicious path
32
- if (UP_PATH_REGEXP.test(pathname))
33
- throw new Error('Forbidden');
36
+ if (upPathRegexp.test(pathname))
37
+ throw httpError('Forbidden', 403);
34
38
  // explode path parts
35
39
  parts = path.normalize(pathname).split(path.sep);
36
40
  // join / normalize from optional root dir
@@ -38,144 +42,156 @@ immutable, } = {}) {
38
42
  }
39
43
  // dotfile handling
40
44
  if (parts.some(part => part.length > 1 && part[0] === '.') && !allowDotfiles)
41
- throw new Error('Forbidden: dotfiles are not allowed');
45
+ throw httpError('Forbidden: dotfiles are not allowed', 403);
42
46
  // pathEndsWithSep
43
47
  if (pathname[pathname.length - 1] === path.sep)
44
- throw new Error('Forbidden: directory access is not allowed');
45
- const fileStat = await stat(pathname);
46
- // not found, check extensions
47
- // if (err.code === 'ENOENT' && !path.extname(pathname) && !pathEndsWithSep) throw err
48
- // switch (err.code) {
49
- // case 'ENAMETOOLONG':
50
- // case 'ENOENT':
51
- // case 'ENOTDIR':
52
- // default:
53
- // }
54
- if (fileStat.isDirectory())
55
- throw new Error('Forbidden: directory access is not allowed');
56
- if (res.headersSent)
57
- return;
58
- //region set header fields
59
- if (!disableAcceptRanges && !res.getHeader('Accept-Ranges'))
60
- res.setHeader('Accept-Ranges', 'bytes');
61
- if (!disableCacheControl && !res.getHeader('Cache-Control'))
62
- res.setHeader('Cache-Control', [
63
- `public, max-age=${Math.floor(maxAge / 1000)}`,
64
- immutable && 'immutable',
65
- ].filter(Boolean).join(', '));
66
- if (!disableLastModified && !res.getHeader('Last-Modified'))
67
- res.setHeader('Last-Modified', fileStat.mtime.toUTCString());
68
- if (etag !== 'disabled' && !res.getHeader('ETag'))
69
- res.setHeader('ETag', etag === 'weak' ? statTag(fileStat) : await entityTagPath(fileStat, pathname, false));
70
- //endregion set header fields
71
- // content-type
72
- if (!res.getHeader('Content-Type'))
73
- res.setHeader('Content-Type', contentTypeForExtension(path.extname(pathname).slice(1)) || 'application/octet-stream');
74
- // conditional GET support
75
- // isConditionalGET
76
- if (req.headers['if-match'] || req.headers['if-unmodified-since'] || req.headers['if-none-match'] || req.headers['if-modified-since']) {
77
- //region isPreconditionFailure
78
- // if-match
79
- const match = req.headers['if-match'];
80
- if (match) {
81
- const etag = res.getHeader('ETag');
82
- if (!etag
83
- || (match !== '*' && parseTokenList(match).every(match => match !== etag && match !== 'W/' + etag && 'W/' + match !== etag)))
84
- throw new Error('Precondition Failed: request headers do not match the response');
48
+ throw httpError('Forbidden: directory access is not allowed', 403);
49
+ // Open the file up front. Doing this before any header is set means a missing file or a
50
+ // read-permission error (EACCES) is turned into a clean HTTP status while res is still
51
+ // uncommitted instead of setting Content-Length/ETag and then having the read fail, which
52
+ // would either reset the connection or leave the client waiting for a body that never comes.
53
+ let handle;
54
+ try {
55
+ handle = await open(pathname, 'r');
56
+ }
57
+ catch (e) {
58
+ const code = e.code;
59
+ if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'ENAMETOOLONG')
60
+ throw httpError('Not Found', 404);
61
+ if (code === 'EACCES' || code === 'EPERM')
62
+ throw httpError('Forbidden', 403);
63
+ if (code === 'EISDIR')
64
+ throw httpError('Forbidden: directory access is not allowed', 403);
65
+ throw e;
66
+ }
67
+ try {
68
+ const fileStat = await handle.stat();
69
+ if (fileStat.isDirectory())
70
+ throw httpError('Forbidden: directory access is not allowed', 403);
71
+ // symlink containment: ensure the real target stays inside root (opt-in)
72
+ if (root && disableFollowSymlinks) {
73
+ const [realFile, realRoot] = await Promise.all([realpath(pathname), realpath(root)]);
74
+ const rel = path.relative(realRoot, realFile);
75
+ if (rel.startsWith('..') || path.isAbsolute(rel))
76
+ throw httpError('Forbidden: symlink escapes root', 403);
85
77
  }
86
- // if-unmodified-since (ignore when using strong etag since mtime may be unreliable)
87
- if (etag === 'weak') {
88
- const unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']);
89
- if (!isNaN(unmodifiedSince)) {
90
- const lastModified = parseHttpDate(res.getHeader('Last-Modified'));
91
- if (isNaN(lastModified) || lastModified > unmodifiedSince)
92
- throw new Error('Precondition Failed: resource has been modified since the specified date');
78
+ if (res.headersSent)
79
+ return;
80
+ //region set header fields
81
+ if (!disableAcceptRanges && !res.getHeader('Accept-Ranges'))
82
+ res.setHeader('Accept-Ranges', 'bytes');
83
+ if (!disableCacheControl && !res.getHeader('Cache-Control'))
84
+ res.setHeader('Cache-Control', [`public, max-age=${Math.floor(maxAge / 1000)}`, immutable && 'immutable'].filter(Boolean).join(', '));
85
+ if (!disableLastModified && !res.getHeader('Last-Modified'))
86
+ res.setHeader('Last-Modified', fileStat.mtime.toUTCString());
87
+ if (etag !== 'disabled' && !res.getHeader('ETag'))
88
+ res.setHeader('ETag', etag === 'weak' ? statTag(fileStat) : await entityTagPath(fileStat, pathname));
89
+ //endregion set header fields
90
+ // content-type
91
+ if (!res.getHeader('Content-Type'))
92
+ res.setHeader('Content-Type', contentTypeForExtension(path.extname(pathname).slice(1)) || 'application/octet-stream');
93
+ // conditional GET support
94
+ // isConditionalGET
95
+ if (req.headers['if-match'] ||
96
+ req.headers['if-unmodified-since'] ||
97
+ req.headers['if-none-match'] ||
98
+ req.headers['if-modified-since']) {
99
+ //region isPreconditionFailure
100
+ // if-match
101
+ const match = req.headers['if-match'];
102
+ if (match) {
103
+ const etag = res.getHeader('ETag');
104
+ if (!etag ||
105
+ (match !== '*' &&
106
+ parseTokenList(match).every(match => match !== etag && match !== 'W/' + etag && 'W/' + match !== etag)))
107
+ throw httpError('Precondition Failed: request headers do not match the response', 412);
108
+ }
109
+ // if-unmodified-since (ignore when using strong etag since mtime may be unreliable)
110
+ if (etag === 'weak') {
111
+ const unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']);
112
+ if (!isNaN(unmodifiedSince)) {
113
+ const lastModified = parseHttpDate(res.getHeader('Last-Modified'));
114
+ if (isNaN(lastModified) || lastModified > unmodifiedSince)
115
+ throw httpError('Precondition Failed: resource has been modified since the specified date', 412);
116
+ }
117
+ }
118
+ //endregion isPreconditionFailure
119
+ // isCachable
120
+ if (((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 304) &&
121
+ fresh(req.headers, {
122
+ etag: res.getHeader('ETag'),
123
+ // Only use last-modified for freshness check when using weak Etag
124
+ 'last-modified': etag === 'weak' ? res.getHeader('Last-Modified') : undefined,
125
+ })) {
126
+ // removeContentHeaderFields
127
+ res.removeHeader('Content-Encoding');
128
+ res.removeHeader('Content-Language');
129
+ res.removeHeader('Content-Length');
130
+ res.removeHeader('Content-Range');
131
+ res.removeHeader('Content-Type');
132
+ res.statusCode = 304;
133
+ return void (await promisify(res.end.bind(res))());
93
134
  }
94
135
  }
95
- //endregion isPreconditionFailure
96
- // isCachable
97
- if ((res.statusCode >= 200 && res.statusCode < 300 || res.statusCode === 304)
98
- && fresh(req.headers, {
99
- etag: res.getHeader('ETag'),
100
- // Only use last-modified for freshness check when using weak Etag
101
- 'last-modified': etag === 'weak' ? res.getHeader('Last-Modified') : undefined
102
- })) {
103
- // removeContentHeaderFields
104
- res.removeHeader('Content-Encoding');
105
- res.removeHeader('Content-Language');
106
- res.removeHeader('Content-Length');
107
- res.removeHeader('Content-Range');
108
- res.removeHeader('Content-Type');
109
- res.statusCode = 304;
110
- return void await promisify(res.end.bind(res))();
111
- }
112
- }
113
- // adjust len to start/end options
114
- let len = fileStat.size;
115
- len = Math.max(0, len - start);
116
- if (end !== undefined) {
117
- const bytes = end - start + 1;
118
- if (len > bytes)
119
- len = bytes;
120
- }
121
- // Range support
122
- let ranges = req.headers.range;
123
- if (!disableAcceptRanges && BYTES_RANGE_REGEXP.test(ranges ?? '')) {
124
- // parse
125
- let rangesNum = parseRange(len, ranges, { combine: true });
126
- // If-Range support
127
- if (!isRangeFresh(req, res))
128
- rangesNum = -2;
129
- // unsatisfiable
130
- if (rangesNum === -1) {
131
- // Content-Range
132
- res.setHeader('Content-Range', contentRange('bytes', len));
133
- // 416 Requested Range Not Satisfiable
134
- throw new Error('Requested Range Not Satisfiable: requested range is not satisfiable');
135
- // return this.error(416, {
136
- // headers: {'Content-Range': res.getHeader('Content-Range')}
137
- // })
136
+ // adjust len to start/end options
137
+ let len = fileStat.size;
138
+ len = Math.max(0, len - start);
139
+ if (end !== undefined) {
140
+ const bytes = end - start + 1;
141
+ if (len > bytes)
142
+ len = bytes;
138
143
  }
139
- // valid (syntactically invalid/multiple ranges are treated as a regular response)
140
- if (rangesNum !== -2 && rangesNum.length === 1) {
141
- // Content-Range
142
- res.statusCode = 206;
143
- res.setHeader('Content-Range', contentRange('bytes', len, rangesNum[0]));
144
- // adjust for requested range
145
- start += rangesNum[0].start;
146
- len = rangesNum[0].end - rangesNum[0].start + 1;
144
+ // Range support
145
+ let ranges = req.headers.range;
146
+ if (!disableAcceptRanges && bytesRangeRegexp.test(ranges ?? '')) {
147
+ // parse
148
+ let rangesNum = parseRange(len, ranges, { combine: true });
149
+ // If-Range support
150
+ if (!isRangeFresh(req, res))
151
+ rangesNum = -2;
152
+ // unsatisfiable
153
+ if (rangesNum === -1) {
154
+ // Content-Range
155
+ res.setHeader('Content-Range', contentRange('bytes', len));
156
+ // 416 Requested Range Not Satisfiable
157
+ throw httpError('Requested Range Not Satisfiable: requested range is not satisfiable', 416);
158
+ // return this.error(416, {
159
+ // headers: {'Content-Range': res.getHeader('Content-Range')}
160
+ // })
161
+ }
162
+ // valid (syntactically invalid/multiple ranges are treated as a regular response)
163
+ if (rangesNum !== -2 && rangesNum.length === 1) {
164
+ // Content-Range
165
+ res.statusCode = 206;
166
+ res.setHeader('Content-Range', contentRange('bytes', len, rangesNum[0]));
167
+ // adjust for requested range
168
+ start += rangesNum[0].start;
169
+ len = rangesNum[0].end - rangesNum[0].start + 1;
170
+ }
147
171
  }
172
+ // set read options
173
+ end = Math.max(start, start + len - 1);
174
+ // content-length
175
+ res.setHeader('Content-Length', len);
176
+ // HEAD support
177
+ if (req.method === 'HEAD')
178
+ return void (await promisify(res.end.bind(res))());
179
+ // stream file: pipeline awaits res 'finish' (full flush) and destroys the source on error
180
+ await pipeline(handle.createReadStream({ start, end }), res);
148
181
  }
149
- // set read options
150
- end = Math.max(start, start + len - 1);
151
- // content-length
152
- res.setHeader('Content-Length', len);
153
- // HEAD support
154
- if (req.method === 'HEAD')
155
- return void await promisify(res.end.bind(res))();
156
- // Weak ETag path: stream file
157
- const stream = createReadStream(pathname, { start, end });
158
- stream.pipe(res);
159
- const defer = Promise.withResolvers();
160
- onFinished(res, cleanup);
161
- stream.on('error', err => {
162
- cleanup();
163
- defer.reject(err);
164
- });
165
- stream.on('end', defer.resolve);
166
- function cleanup() {
167
- stream.destroy();
182
+ finally {
183
+ await handle.close().catch(() => { });
168
184
  }
169
- return defer.promise;
170
185
  }
171
186
  function isRangeFresh(req, res) {
172
187
  const ifRange = req.headers['if-range'];
173
188
  if (!ifRange)
174
189
  return true;
175
- // if-range as etag
190
+ // if-range as etag (exact match: If-Range carries a single validator, not a list — a substring
191
+ // test would let If-Range: "ab" match an ETag of "abc")
176
192
  if (ifRange.indexOf('"') !== -1) {
177
193
  const etag = res.getHeader('ETag');
178
- return etag && ifRange.includes(etag);
194
+ return etag !== undefined && ifRange === etag;
179
195
  }
180
196
  // if-range as modified date
181
197
  const lastModified = res.getHeader('Last-Modified');
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staticHelpers.js","sourceRoot":"","sources":["../src/staticHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["import {IncomingMessage, ServerResponse} from 'node:http'\nimport path from 'node:path'\nimport {open, realpath} from 'node:fs/promises'\nimport {entityTagPath, statTag} from './vendors/etag.js'\nimport {contentTypeForExtension} from './vendors/mime.js'\nimport {fresh, parseHttpDate, parseTokenList} from './vendors/fresh.js'\nimport {parseRange} from './vendors/rangeParser.js'\nimport {promisify} from 'node:util'\nimport {pipeline} from 'node:stream/promises'\n\nexport type HttpError = Error & {statusCode: number}\nfunction httpError(message: string, statusCode: number) {\n\tconst e = new Error(message) as HttpError\n\te.statusCode = statusCode\n\treturn e\n}\n\nconst bytesRangeRegexp = /^ *bytes=/\nconst upPathRegexp = /(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)/\n\nexport interface SendFileOptions {\n\tallowDotfiles?: boolean\n\t// extensions?: string[] | string | boolean // disable extensions option\n\t// index?: string[] | string | boolean // disable index option\n\troot?: string\n\tdisableAcceptRanges?: boolean\n\tdisableLastModified?: boolean\n\t// use weak mtime-based etag instead of strong content-based etag (enables streaming and range requests)\n\t// service like GAE reset mtime to Tue, 01 Jan 1980 00:00:01 GMT (Unix timestamp 315532801), so we enable strong tag by default\n\tetag?: 'disabled' | 'strong' | 'weak'\n\n\tdisableCacheControl?: boolean\n\tmaxAge?: number // in milliseconds\n\timmutable?: boolean\n\n\t// when root is set, set true to 403 any file whose real path resolves outside root\n\t// (symlink containment). default: symlinks are followed.\n\tdisableFollowSymlinks?: boolean\n\n\tend?: number\n\tstart?: number\n}\n\nexport async function sendFileTrusted(\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\tpathname: string, // plain path, not URI-encoded\n\t{\n\t\troot,\n\t\tallowDotfiles,\n\t\tstart = 0,\n\t\tend,\n\t\tdisableAcceptRanges,\n\t\tdisableLastModified,\n\t\tetag = 'weak',\n\t\tdisableCacheControl,\n\t\tmaxAge = 60 * 60 * 24 * 365 * 1000, // 1 year\n\t\timmutable,\n\t\tdisableFollowSymlinks,\n\t}: SendFileOptions = {},\n) {\n\t// null byte(s)\n\tif (pathname.includes('\\0')) throw httpError('Forbidden', 403)\n\n\tlet parts: string[]\n\tif (root) {\n\t\t// normalize\n\t\tpathname = path.normalize(`.${path.sep}${pathname}`)\n\n\t\t// malicious path\n\t\tif (upPathRegexp.test(pathname)) throw httpError('Forbidden', 403)\n\n\t\t// explode path parts\n\t\tparts = pathname.split(path.sep)\n\n\t\t// join / normalize from optional root dir\n\t\tpathname = path.normalize(path.join(root, pathname))\n\t} else {\n\t\t// malicious path\n\t\tif (upPathRegexp.test(pathname)) throw httpError('Forbidden', 403)\n\n\t\t// explode path parts\n\t\tparts = path.normalize(pathname).split(path.sep)\n\n\t\t// join / normalize from optional root dir\n\t\tpathname = path.resolve(pathname)\n\t}\n\n\t// dotfile handling\n\tif (parts.some(part => part.length > 1 && part[0] === '.') && !allowDotfiles)\n\t\tthrow httpError('Forbidden: dotfiles are not allowed', 403)\n\n\t// pathEndsWithSep\n\tif (pathname[pathname.length - 1] === path.sep) throw httpError('Forbidden: directory access is not allowed', 403)\n\n\t// Open the file up front. Doing this before any header is set means a missing file or a\n\t// read-permission error (EACCES) is turned into a clean HTTP status while res is still\n\t// uncommitted — instead of setting Content-Length/ETag and then having the read fail, which\n\t// would either reset the connection or leave the client waiting for a body that never comes.\n\tlet handle\n\ttry {\n\t\thandle = await open(pathname, 'r')\n\t} catch (e) {\n\t\tconst code = (e as NodeJS.ErrnoException).code\n\t\tif (code === 'ENOENT' || code === 'ENOTDIR' || code === 'ENAMETOOLONG') throw httpError('Not Found', 404)\n\t\tif (code === 'EACCES' || code === 'EPERM') throw httpError('Forbidden', 403)\n\t\tif (code === 'EISDIR') throw httpError('Forbidden: directory access is not allowed', 403)\n\t\tthrow e\n\t}\n\n\ttry {\n\t\tconst fileStat = await handle.stat()\n\n\t\tif (fileStat.isDirectory()) throw httpError('Forbidden: directory access is not allowed', 403)\n\n\t\t// symlink containment: ensure the real target stays inside root (opt-in)\n\t\tif (root && disableFollowSymlinks) {\n\t\t\tconst [realFile, realRoot] = await Promise.all([realpath(pathname), realpath(root)])\n\t\t\tconst rel = path.relative(realRoot, realFile)\n\t\t\tif (rel.startsWith('..') || path.isAbsolute(rel)) throw httpError('Forbidden: symlink escapes root', 403)\n\t\t}\n\n\t\tif (res.headersSent) return\n\n\t\t//region set header fields\n\n\t\tif (!disableAcceptRanges && !res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes')\n\n\t\tif (!disableCacheControl && !res.getHeader('Cache-Control'))\n\t\t\tres.setHeader(\n\t\t\t\t'Cache-Control',\n\t\t\t\t[`public, max-age=${Math.floor(maxAge / 1000)}`, immutable && 'immutable'].filter(Boolean).join(', '),\n\t\t\t)\n\n\t\tif (!disableLastModified && !res.getHeader('Last-Modified'))\n\t\t\tres.setHeader('Last-Modified', fileStat.mtime.toUTCString())\n\n\t\tif (etag !== 'disabled' && !res.getHeader('ETag'))\n\t\t\tres.setHeader('ETag', etag === 'weak' ? statTag(fileStat) : await entityTagPath(fileStat, pathname))\n\t\t//endregion set header fields\n\n\t\t// content-type\n\t\tif (!res.getHeader('Content-Type'))\n\t\t\tres.setHeader(\n\t\t\t\t'Content-Type',\n\t\t\t\tcontentTypeForExtension(path.extname(pathname).slice(1)) || 'application/octet-stream',\n\t\t\t)\n\n\t\t// conditional GET support\n\t\t// isConditionalGET\n\t\tif (\n\t\t\treq.headers['if-match'] ||\n\t\t\treq.headers['if-unmodified-since'] ||\n\t\t\treq.headers['if-none-match'] ||\n\t\t\treq.headers['if-modified-since']\n\t\t) {\n\t\t\t//region isPreconditionFailure\n\t\t\t// if-match\n\t\t\tconst match = req.headers['if-match']\n\t\t\tif (match) {\n\t\t\t\tconst etag = res.getHeader('ETag')\n\t\t\t\tif (\n\t\t\t\t\t!etag ||\n\t\t\t\t\t(match !== '*' &&\n\t\t\t\t\t\tparseTokenList(match).every(match => match !== etag && match !== 'W/' + etag && 'W/' + match !== etag))\n\t\t\t\t)\n\t\t\t\t\tthrow httpError('Precondition Failed: request headers do not match the response', 412)\n\t\t\t}\n\n\t\t\t// if-unmodified-since (ignore when using strong etag since mtime may be unreliable)\n\t\t\tif (etag === 'weak') {\n\t\t\t\tconst unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since'])\n\t\t\t\tif (!isNaN(unmodifiedSince)) {\n\t\t\t\t\tconst lastModified = parseHttpDate(res.getHeader('Last-Modified'))\n\t\t\t\t\tif (isNaN(lastModified) || lastModified > unmodifiedSince)\n\t\t\t\t\t\tthrow httpError('Precondition Failed: resource has been modified since the specified date', 412)\n\t\t\t\t}\n\t\t\t}\n\t\t\t//endregion isPreconditionFailure\n\n\t\t\t// isCachable\n\t\t\tif (\n\t\t\t\t((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 304) &&\n\t\t\t\tfresh(req.headers, {\n\t\t\t\t\tetag: res.getHeader('ETag'),\n\t\t\t\t\t// Only use last-modified for freshness check when using weak Etag\n\t\t\t\t\t'last-modified': etag === 'weak' ? res.getHeader('Last-Modified') : undefined,\n\t\t\t\t})\n\t\t\t) {\n\t\t\t\t// removeContentHeaderFields\n\t\t\t\tres.removeHeader('Content-Encoding')\n\t\t\t\tres.removeHeader('Content-Language')\n\t\t\t\tres.removeHeader('Content-Length')\n\t\t\t\tres.removeHeader('Content-Range')\n\t\t\t\tres.removeHeader('Content-Type')\n\t\t\t\tres.statusCode = 304\n\t\t\t\treturn void (await promisify(res.end.bind(res))())\n\t\t\t}\n\t\t}\n\n\t\t// adjust len to start/end options\n\t\tlet len = fileStat.size\n\t\tlen = Math.max(0, len - start)\n\t\tif (end !== undefined) {\n\t\t\tconst bytes = end - start + 1\n\t\t\tif (len > bytes) len = bytes\n\t\t}\n\n\t\t// Range support\n\t\tlet ranges = req.headers.range\n\t\tif (!disableAcceptRanges && bytesRangeRegexp.test(ranges ?? '')) {\n\t\t\t// parse\n\t\t\tlet rangesNum = parseRange(len, ranges, {combine: true})\n\n\t\t\t// If-Range support\n\t\t\tif (!isRangeFresh(req, res)) rangesNum = -2\n\n\t\t\t// unsatisfiable\n\t\t\tif (rangesNum === -1) {\n\t\t\t\t// Content-Range\n\t\t\t\tres.setHeader('Content-Range', contentRange('bytes', len))\n\n\t\t\t\t// 416 Requested Range Not Satisfiable\n\t\t\t\tthrow httpError('Requested Range Not Satisfiable: requested range is not satisfiable', 416)\n\t\t\t\t// return this.error(416, {\n\t\t\t\t// \theaders: {'Content-Range': res.getHeader('Content-Range')}\n\t\t\t\t// })\n\t\t\t}\n\n\t\t\t// valid (syntactically invalid/multiple ranges are treated as a regular response)\n\t\t\tif (rangesNum !== -2 && rangesNum.length === 1) {\n\t\t\t\t// Content-Range\n\t\t\t\tres.statusCode = 206\n\t\t\t\tres.setHeader('Content-Range', contentRange('bytes', len, rangesNum[0]))\n\n\t\t\t\t// adjust for requested range\n\t\t\t\tstart += rangesNum[0].start\n\t\t\t\tlen = rangesNum[0].end - rangesNum[0].start + 1\n\t\t\t}\n\t\t}\n\n\t\t// set read options\n\t\tend = Math.max(start, start + len - 1)\n\n\t\t// content-length\n\t\tres.setHeader('Content-Length', len)\n\n\t\t// HEAD support\n\t\tif (req.method === 'HEAD') return void (await promisify(res.end.bind(res))())\n\n\t\t// stream file: pipeline awaits res 'finish' (full flush) and destroys the source on error\n\t\tawait pipeline(handle.createReadStream({start, end}), res)\n\t} finally {\n\t\tawait handle.close().catch(() => {})\n\t}\n}\n\nfunction isRangeFresh(req: IncomingMessage, res: ServerResponse) {\n\tconst ifRange = req.headers['if-range']\n\n\tif (!ifRange) return true\n\n\t// if-range as etag (exact match: If-Range carries a single validator, not a list — a substring\n\t// test would let If-Range: \"ab\" match an ETag of \"abc\")\n\tif (ifRange.indexOf('\"') !== -1) {\n\t\tconst etag = res.getHeader('ETag')\n\t\treturn etag !== undefined && ifRange === etag\n\t}\n\n\t// if-range as modified date\n\tconst lastModified = res.getHeader('Last-Modified')\n\treturn parseHttpDate(lastModified) <= parseHttpDate(ifRange)\n}\n\nfunction contentRange(type: string, size: number, range?: {start: number; end: number}) {\n\treturn `${type} ${range ? range.start + '-' + range.end : '*'}/${size}`\n}\n"]}
package/lib/stream.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { IncomingMessage } from 'node:http';
2
2
  import type { Readable } from 'node:stream';
3
3
  export declare function getContentStream(req: IncomingMessage, encoding: string, disableInflate?: boolean): IncomingMessage | import("node:zlib").Inflate | import("node:zlib").Gunzip | import("node:zlib").BrotliDecompress;
4
- export declare function readStream(stream: Readable, { length, limit }: {
4
+ export declare function readStream(stream: Readable, { length, limit, }: {
5
5
  length?: number;
6
6
  limit?: number;
7
7
  }): Promise<Buffer<ArrayBufferLike>>;
package/lib/stream.js CHANGED
@@ -1,4 +1,10 @@
1
1
  import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib';
2
+ // body errors carry an HTTP status so error middleware can map them (413 too large, 400 malformed)
3
+ function bodyError(message, statusCode) {
4
+ const e = new Error(message);
5
+ e.statusCode = statusCode;
6
+ return e;
7
+ }
2
8
  // note: there might be multiple encodings applied to the stream
3
9
  // we only support one encoding
4
10
  export function getContentStream(req, encoding, disableInflate) {
@@ -27,13 +33,13 @@ export function getContentStream(req, encoding, disableInflate) {
27
33
  }
28
34
  }
29
35
  // https://github.com/stream-utils/raw-body/blob/191e4b6506dcf77198eed01c8feb4b6817008342/index.js#L155
30
- export async function readStream(stream, { length, limit }) {
36
+ export async function readStream(stream, { length, limit, }) {
31
37
  let completed = false;
32
38
  // check the length and limit options.
33
39
  // note: we intentionally leave the stream paused,
34
40
  // so users should handle the stream themselves.
35
41
  if (limit !== undefined && length !== undefined && length > limit)
36
- throw new Error('request entity too large');
42
+ throw bodyError('request entity too large', 413);
37
43
  let received = 0;
38
44
  const buffers = [];
39
45
  const defer = Promise.withResolvers();
@@ -61,7 +67,7 @@ export async function readStream(stream, { length, limit }) {
61
67
  return;
62
68
  received += chunk.length;
63
69
  if (limit !== undefined && received > limit) {
64
- done(new Error('request entity too large'));
70
+ done(bodyError('request entity too large', 413));
65
71
  }
66
72
  else
67
73
  buffers.push(chunk);
@@ -71,12 +77,12 @@ export async function readStream(stream, { length, limit }) {
71
77
  }
72
78
  function onEnd() {
73
79
  if (length !== undefined && received !== length)
74
- done(new Error('request size did not match content length'));
80
+ done(bodyError('request size did not match content length', 400));
75
81
  else
76
82
  done(undefined, Buffer.concat(buffers));
77
83
  }
78
84
  function onAborted() {
79
- done(new Error('request aborted'));
85
+ done(bodyError('request aborted', 400));
80
86
  }
81
87
  function onClose() {
82
88
  buffers.splice(0, buffers.length);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"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","sourcesContent":["// https://github.com/expressjs/body-parser/blob/2a2f47199b443c56b6ebb74cac7acdeb63fac61f/lib/read.js#L152\nimport type {IncomingMessage} from 'node:http'\nimport type {Readable} from 'node:stream'\nimport {createBrotliDecompress, createGunzip, createInflate} from 'node:zlib'\n\n// note: there might be multiple encodings applied to the stream\n// we only support one encoding\nexport function getContentStream(req: IncomingMessage, encoding: string, disableInflate?: boolean) {\n\tif (disableInflate && encoding !== 'identity') throw new Error(`content-encoding ${encoding} is not supported`)\n\n\tswitch (encoding) {\n\t\tcase 'deflate': {\n\t\t\tconst stream = createInflate()\n\t\t\treq.pipe(stream)\n\t\t\treturn stream\n\t\t}\n\t\tcase 'gzip': {\n\t\t\tconst stream = createGunzip()\n\t\t\treq.pipe(stream)\n\t\t\treturn stream\n\t\t}\n\t\tcase 'br': {\n\t\t\tconst stream = createBrotliDecompress()\n\t\t\treq.pipe(stream)\n\t\t\treturn stream\n\t\t}\n\t\tcase 'identity':\n\t\t\treturn req\n\t\tdefault:\n\t\t\tthrow new Error(`unsupported content-encoding ${encoding}`)\n\t}\n}\n\n// https://github.com/stream-utils/raw-body/blob/191e4b6506dcf77198eed01c8feb4b6817008342/index.js#L155\nexport async function readStream(\n\tstream: Readable,\n\t{\n\t\tlength,\n\t\tlimit,\n\t}: {\n\t\tlength?: number\n\t\tlimit?: number\n\t},\n) {\n\tlet completed = false\n\n\t// check the length and limit options.\n\t// note: we intentionally leave the stream paused,\n\t// so users should handle the stream themselves.\n\tif (limit !== undefined && length !== undefined && length > limit) throw new Error('request entity too large')\n\n\tlet received = 0\n\tconst buffers: Buffer[] = []\n\tconst defer = Promise.withResolvers<Buffer>()\n\n\t// attach listeners\n\tstream.on('aborted', onAborted)\n\tstream.on('close', onClose)\n\tstream.on('data', onData)\n\tstream.on('end', onEnd)\n\tstream.on('error', onError)\n\n\tfunction done(err?: Error, result?: Buffer) {\n\t\tif (completed) return\n\t\tcompleted = true\n\t\tonClose()\n\t\tif (err) {\n\t\t\tstream.unpipe?.()\n\t\t\tstream.pause?.()\n\t\t\tdefer.reject(err)\n\t\t} else defer.resolve(result!)\n\t}\n\n\tfunction onData(chunk: Buffer) {\n\t\tif (completed) return\n\t\treceived += chunk.length\n\t\tif (limit !== undefined && received > limit) {\n\t\t\tdone(new Error('request entity too large'))\n\t\t} else buffers.push(chunk)\n\t}\n\n\tfunction onError(err: Error) {\n\t\tdone(err)\n\t}\n\tfunction onEnd() {\n\t\tif (length !== undefined && received !== length) done(new Error('request size did not match content length'))\n\t\telse done(undefined, Buffer.concat(buffers))\n\t}\n\tfunction onAborted() {\n\t\tdone(new Error('request aborted'))\n\t}\n\tfunction onClose() {\n\t\tbuffers.splice(0, buffers.length)\n\t\tstream.off('aborted', onAborted)\n\t\tstream.off('data', onData)\n\t\tstream.off('end', onEnd)\n\t\tstream.off('error', onError)\n\t\tstream.off('close', onClose)\n\t}\n\n\treturn await defer.promise\n}\n"]}
@@ -6,7 +6,7 @@
6
6
  * type = token
7
7
  * subtype = token
8
8
  */
9
- const TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
9
+ const typeRegexp = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
10
10
  /**
11
11
  * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
12
12
  *
@@ -21,22 +21,18 @@ const TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
21
21
  * obs-text = %x80-FF
22
22
  * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
23
23
  */
24
- const PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g; // eslint-disable-line no-control-regex
25
- const TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/; // eslint-disable-line no-control-regex
26
- const TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
24
+ const paramRegexp = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g; // eslint-disable-line no-control-regex
27
25
  /**
28
26
  * RegExp to match quoted-pair in RFC 7230 sec 3.2.6
29
27
  *
30
28
  * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
31
29
  * obs-text = %x80-FF
32
30
  */
33
- const QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g; // eslint-disable-line no-control-regex
31
+ const qescRegexp = /\\([\u000b\u0020-\u00ff])/g; // eslint-disable-line no-control-regex
34
32
  export function parseContentType(header) {
35
33
  let index = header.indexOf(';');
36
- const mediaType = index !== -1
37
- ? header.slice(0, index).trim()
38
- : header.trim();
39
- if (!TYPE_REGEXP.test(mediaType))
34
+ const mediaType = index !== -1 ? header.slice(0, index).trim() : header.trim();
35
+ if (!typeRegexp.test(mediaType))
40
36
  throw new TypeError(`invalid media type: ${mediaType}`);
41
37
  const parameters = Object.create(null);
42
38
  // parse parameters
@@ -44,7 +40,7 @@ export function parseContentType(header) {
44
40
  let key;
45
41
  let match;
46
42
  let value;
47
- const regexp = new RegExp(PARAM_REGEXP);
43
+ const regexp = new RegExp(paramRegexp);
48
44
  regexp.lastIndex = index;
49
45
  while ((match = regexp.exec(header))) {
50
46
  if (match.index !== index)
@@ -57,7 +53,7 @@ export function parseContentType(header) {
57
53
  value = value.slice(1, -1);
58
54
  // remove escapes
59
55
  if (value.indexOf('\\') !== -1)
60
- value = value.replace(QESC_REGEXP, '$1');
56
+ value = value.replace(qescRegexp, '$1');
61
57
  }
62
58
  parameters[key] = value;
63
59
  }
@@ -66,22 +62,3 @@ export function parseContentType(header) {
66
62
  }
67
63
  return { mediaType, parameters };
68
64
  }
69
- /**
70
- * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
71
- */
72
- const QUOTE_REGEXP = /([\\"])/g;
73
- function qstring(str) {
74
- // no need to quote tokens
75
- if (TOKEN_REGEXP.test(str))
76
- return str;
77
- if (str.length > 0 && !TEXT_REGEXP.test(str))
78
- throw new TypeError(`invalid parameter value: ${str}`);
79
- return `"${str.replace(QUOTE_REGEXP, '\\$1')}"`;
80
- }
81
- function formatContentType({ mediaType, parameters }) {
82
- if (!mediaType || !TYPE_REGEXP.test(mediaType))
83
- throw new TypeError(`invalid type: ${mediaType}`);
84
- return `${mediaType}${parameters
85
- ? Object.keys(parameters).sort().map(key => `; ${key}=${qstring(parameters[key])}`).join('')
86
- : ''}`;
87
- }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contentType.js","sourceRoot":"","sources":["../../src/vendors/contentType.ts"],"names":[],"mappings":"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","sourcesContent":["// https://github.com/jshttp/content-type/blob/d02574e9640bd4370f148c767b1b877b5a300070/index.js#L106\n/**\n * RegExp to match type in RFC 7231 sec 3.1.1.1\n *\n * media-type = type \"/\" subtype\n * type = token\n * subtype = token\n */\nconst typeRegexp = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/\n/**\n * RegExp to match *( \";\" parameter ) in RFC 7231 sec 3.1.1.1\n *\n * parameter = token \"=\" ( token / quoted-string )\n * token = 1*tchar\n * tchar = \"!\" / \"#\" / \"$\" / \"%\" / \"&\" / \"'\" / \"*\"\n * / \"+\" / \"-\" / \".\" / \"^\" / \"_\" / \"`\" / \"|\" / \"~\"\n * / DIGIT / ALPHA\n * ; any VCHAR, except delimiters\n * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE\n * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text\n * obs-text = %x80-FF\n * quoted-pair = \"\\\" ( HTAB / SP / VCHAR / obs-text )\n */\nconst paramRegexp =\n\t/; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *(\"(?:[\\u000b\\u0020\\u0021\\u0023-\\u005b\\u005d-\\u007e\\u0080-\\u00ff]|\\\\[\\u000b\\u0020-\\u00ff])*\"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex\nconst textRegexp = /^[\\u000b\\u0020-\\u007e\\u0080-\\u00ff]+$/ // eslint-disable-line no-control-regex\nconst tokenRegexp = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/\n/**\n * RegExp to match quoted-pair in RFC 7230 sec 3.2.6\n *\n * quoted-pair = \"\\\" ( HTAB / SP / VCHAR / obs-text )\n * obs-text = %x80-FF\n */\nconst qescRegexp = /\\\\([\\u000b\\u0020-\\u00ff])/g // eslint-disable-line no-control-regex\n\nexport function parseContentType(header: string) {\n\tlet index = header.indexOf(';')\n\tconst mediaType = index !== -1 ? header.slice(0, index).trim() : header.trim()\n\n\tif (!typeRegexp.test(mediaType)) throw new TypeError(`invalid media type: ${mediaType}`)\n\tconst parameters: Record<string, string> = Object.create(null)\n\n\t// parse parameters\n\tif (index !== -1) {\n\t\tlet key\n\t\tlet match\n\t\tlet value\n\n\t\tconst regexp = new RegExp(paramRegexp)\n\t\tregexp.lastIndex = index\n\n\t\twhile ((match = regexp.exec(header))) {\n\t\t\tif (match.index !== index) throw new TypeError('invalid parameter format')\n\n\t\t\tindex += match[0].length\n\t\t\tkey = match[1].toLowerCase()\n\t\t\tvalue = match[2]\n\n\t\t\tif (value.charCodeAt(0) === 0x22 /* \" */) {\n\t\t\t\t// remove quotes\n\t\t\t\tvalue = value.slice(1, -1)\n\t\t\t\t// remove escapes\n\t\t\t\tif (value.indexOf('\\\\') !== -1) value = value.replace(qescRegexp, '$1')\n\t\t\t}\n\n\t\t\tparameters[key] = value\n\t\t}\n\n\t\tif (index !== header.length) throw new TypeError('invalid parameter format')\n\t}\n\n\treturn {mediaType, parameters}\n}\n\n/**\n * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6\n */\nconst quoteRegexp = /([\\\\\"])/g\nfunction qstring(str: string) {\n\t// no need to quote tokens\n\tif (tokenRegexp.test(str)) return str\n\n\tif (str.length > 0 && !textRegexp.test(str)) throw new TypeError(`invalid parameter value: ${str}`)\n\treturn `\"${str.replace(quoteRegexp, '\\\\$1')}\"`\n}\n\nfunction formatContentType({mediaType, parameters}: {mediaType: string; parameters?: Record<string, string>}) {\n\tif (!mediaType || !typeRegexp.test(mediaType)) throw new TypeError(`invalid type: ${mediaType}`)\n\treturn `${mediaType}${\n\t\tparameters\n\t\t\t? Object.keys(parameters)\n\t\t\t\t\t.sort()\n\t\t\t\t\t.map(key => `; ${key}=${qstring(parameters[key])}`)\n\t\t\t\t\t.join('')\n\t\t\t: ''\n\t}`\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import type { IncomingMessage } from 'node:http';
2
2
  import { type Stats } from 'node:fs';
3
- export declare function entityTag(buf: Buffer, weak?: boolean): string;
4
- export declare function entityTagPath(fileStat: Stats, filePath: string, weak?: boolean): Promise<string>;
3
+ export declare function entityTag(buf: Buffer): string;
4
+ export declare function entityTagPath(fileStat: Stats, filePath: string): Promise<string>;
5
5
  export declare function statTag(stat: Stats): string;
6
6
  export declare function isFreshETag(req: IncomingMessage, etag: string): true | undefined;
7
7
  export declare function isFreshModifiedSince(req: IncomingMessage, lastModified: string): boolean | undefined;