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.
- package/README.md +397 -265
- package/lib/body.js +1 -1
- package/lib/body.js.map +1 -0
- package/lib/bodyHelpers.js +22 -9
- package/lib/bodyHelpers.js.map +1 -0
- package/lib/dx.d.ts +1 -1
- package/lib/dx.js +12 -6
- package/lib/dx.js.map +1 -0
- package/lib/dxHelpers.d.ts +2 -1
- package/lib/dxHelpers.js +105 -87
- package/lib/dxHelpers.js.map +1 -0
- package/lib/helpers.js.map +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -0
- package/lib/logger.d.ts +3 -2
- package/lib/logger.js +54 -46
- package/lib/logger.js.map +1 -0
- package/lib/router.js +5 -5
- package/lib/router.js.map +1 -0
- package/lib/static.js +4 -3
- package/lib/static.js.map +1 -0
- package/lib/staticHelpers.d.ts +5 -1
- package/lib/staticHelpers.js +150 -134
- package/lib/staticHelpers.js.map +1 -0
- package/lib/stream.d.ts +1 -1
- package/lib/stream.js +11 -5
- package/lib/stream.js.map +1 -0
- package/lib/vendors/contentType.js +7 -30
- package/lib/vendors/contentType.js.map +1 -0
- package/lib/vendors/etag.d.ts +2 -2
- package/lib/vendors/etag.js +15 -25
- package/lib/vendors/etag.js.map +1 -0
- package/lib/vendors/fresh.js +10 -17
- package/lib/vendors/fresh.js.map +1 -0
- package/lib/vendors/mime.js +4 -4
- package/lib/vendors/mime.js.map +1 -0
- package/lib/vendors/mimeDb.d.ts +2544 -2544
- package/lib/vendors/mimeDb.js +7100 -7079
- package/lib/vendors/mimeDb.js.map +1 -0
- package/lib/vendors/mimeScore.js +10 -11
- package/lib/vendors/mimeScore.js.map +1 -0
- package/lib/vendors/rangeParser.d.ts +2 -10
- package/lib/vendors/rangeParser.js +16 -29
- package/lib/vendors/rangeParser.js.map +1 -0
- 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,
|
|
16
|
-
?? decodeURIComponent(pathname), options);
|
|
16
|
+
await sendFileTrusted(req, res, getPathname?.(matched) ?? decodeURIComponent(pathname), options);
|
|
17
17
|
}
|
|
18
18
|
catch (e) {
|
|
19
|
-
|
|
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"]}
|
package/lib/staticHelpers.d.ts
CHANGED
|
@@ -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<
|
|
20
|
+
immutable, disableFollowSymlinks, }?: SendFileOptions): Promise<undefined>;
|
package/lib/staticHelpers.js
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
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 = '
|
|
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
|
|
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 (
|
|
24
|
-
throw
|
|
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 (
|
|
33
|
-
throw
|
|
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
|
|
45
|
+
throw httpError('Forbidden: dotfiles are not allowed', 403);
|
|
42
46
|
// pathEndsWithSep
|
|
43
47
|
if (pathname[pathname.length - 1] === path.sep)
|
|
44
|
-
throw
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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"]}
|
package/lib/vendors/etag.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
export declare function entityTagPath(fileStat: Stats, filePath: 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;
|