dx-server 0.12.2 → 0.13.0-alpha.2
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 +417 -293
- package/{cjs → lib}/body.d.ts +2 -3
- package/lib/body.js +10 -0
- package/lib/body.js.map +1 -0
- package/{esm → lib}/bodyHelpers.d.ts +2 -4
- package/lib/bodyHelpers.js +102 -0
- package/lib/bodyHelpers.js.map +1 -0
- package/{esm → lib}/dx.d.ts +6 -9
- package/lib/dx.js +133 -0
- package/lib/dx.js.map +1 -0
- package/{cjs → lib}/dxHelpers.d.ts +2 -5
- package/lib/dxHelpers.js +135 -0
- package/lib/dxHelpers.js.map +1 -0
- package/lib/helpers.js.map +1 -0
- package/{cjs → lib}/index.d.ts +1 -2
- package/{esm/index.d.ts → lib/index.js} +1 -2
- package/lib/index.js.map +1 -0
- package/lib/logger.d.ts +4 -0
- package/lib/logger.js +64 -0
- package/lib/logger.js.map +1 -0
- package/lib/router.d.ts +42 -0
- package/lib/router.js +43 -0
- package/lib/router.js.map +1 -0
- package/lib/static.js +23 -0
- package/lib/static.js.map +1 -0
- package/{cjs → lib}/staticHelpers.d.ts +5 -3
- package/lib/staticHelpers.js +202 -0
- package/lib/staticHelpers.js.map +1 -0
- package/{cjs → lib}/stream.d.ts +3 -8
- package/lib/stream.js +96 -0
- package/lib/stream.js.map +1 -0
- package/lib/vendors/contentType.js +64 -0
- package/lib/vendors/contentType.js.map +1 -0
- package/{cjs → lib}/vendors/etag.d.ts +2 -5
- package/lib/vendors/etag.js +94 -0
- package/lib/vendors/etag.js.map +1 -0
- package/{cjs → lib}/vendors/fresh.d.ts +2 -2
- package/lib/vendors/fresh.js +88 -0
- package/lib/vendors/fresh.js.map +1 -0
- package/lib/vendors/mime.d.ts +1 -0
- package/lib/vendors/mime.js +35 -0
- package/lib/vendors/mime.js.map +1 -0
- package/{cjs → lib}/vendors/mimeDb.d.ts +2544 -2544
- package/lib/vendors/mimeDb.js +9435 -0
- package/lib/vendors/mimeDb.js.map +1 -0
- package/{cjs → lib}/vendors/mimeScore.d.ts +1 -1
- package/lib/vendors/mimeScore.js +44 -0
- package/lib/vendors/mimeScore.js.map +1 -0
- package/{cjs → lib}/vendors/onFinished.d.ts +1 -1
- package/lib/vendors/onFinished.js +231 -0
- package/lib/vendors/rangeParser.d.ts +12 -0
- package/lib/vendors/rangeParser.js +108 -0
- package/lib/vendors/rangeParser.js.map +1 -0
- package/package.json +32 -36
- package/cjs/body.js +0 -14
- package/cjs/bodyHelpers.d.ts +0 -16
- package/cjs/bodyHelpers.js +0 -101
- package/cjs/connect.d.ts +0 -5
- package/cjs/connect.js +0 -44
- package/cjs/dx.d.ts +0 -46
- package/cjs/dx.js +0 -144
- package/cjs/dxHelpers.js +0 -123
- package/cjs/express.d.ts +0 -4
- package/cjs/express.js +0 -43
- package/cjs/helpers.js +0 -14
- package/cjs/index.js +0 -38
- package/cjs/logger.d.ts +0 -3
- package/cjs/logger.js +0 -61
- package/cjs/package.json +0 -3
- package/cjs/polyfillWithResolvers.d.ts +0 -1
- package/cjs/polyfillWithResolvers.js +0 -17
- package/cjs/router.js +0 -47
- package/cjs/static.js +0 -27
- package/cjs/staticHelpers.js +0 -195
- package/cjs/stream.js +0 -97
- package/cjs/vendors/contentType.js +0 -92
- package/cjs/vendors/etag.js +0 -136
- package/cjs/vendors/fresh.js +0 -102
- package/cjs/vendors/mime.d.ts +0 -1
- package/cjs/vendors/mime.js +0 -42
- package/cjs/vendors/mimeDb.js +0 -9417
- package/cjs/vendors/mimeScore.js +0 -50
- package/cjs/vendors/onFinished.js +0 -245
- package/cjs/vendors/rangeParser.d.ts +0 -10
- package/cjs/vendors/rangeParser.js +0 -126
- package/esm/body.d.ts +0 -8
- package/esm/body.js +0 -11
- package/esm/bodyHelpers.js +0 -90
- package/esm/connect.d.ts +0 -5
- package/esm/connect.js +0 -40
- package/esm/dx.js +0 -128
- package/esm/dxHelpers.d.ts +0 -49
- package/esm/dxHelpers.js +0 -119
- package/esm/express.d.ts +0 -4
- package/esm/express.js +0 -35
- package/esm/helpers.js +0 -3
- package/esm/index.js +0 -9
- package/esm/logger.d.ts +0 -3
- package/esm/logger.js +0 -57
- package/esm/polyfillWithResolvers.d.ts +0 -1
- package/esm/polyfillWithResolvers.js +0 -16
- package/esm/router.js +0 -44
- package/esm/static.d.ts +0 -5
- package/esm/static.js +0 -23
- package/esm/staticHelpers.d.ts +0 -18
- package/esm/staticHelpers.js +0 -188
- package/esm/stream.d.ts +0 -12
- package/esm/stream.js +0 -92
- package/esm/vendors/contentType.d.ts +0 -4
- package/esm/vendors/contentType.js +0 -88
- package/esm/vendors/etag.d.ts +0 -10
- package/esm/vendors/etag.js +0 -105
- package/esm/vendors/fresh.d.ts +0 -23
- package/esm/vendors/fresh.js +0 -96
- package/esm/vendors/mime.d.ts +0 -1
- package/esm/vendors/mime.js +0 -35
- package/esm/vendors/mimeDb.d.ts +0 -9413
- package/esm/vendors/mimeDb.js +0 -9415
- package/esm/vendors/mimeScore.d.ts +0 -5
- package/esm/vendors/mimeScore.js +0 -46
- package/esm/vendors/onFinished.d.ts +0 -14
- package/esm/vendors/onFinished.js +0 -241
- package/esm/vendors/rangeParser.d.ts +0 -10
- package/esm/vendors/rangeParser.js +0 -122
- /package/{cjs → lib}/helpers.d.ts +0 -0
- /package/{esm/helpers.d.ts → lib/helpers.js} +0 -0
- /package/{cjs → lib}/static.d.ts +0 -0
- /package/{cjs → lib}/vendors/contentType.d.ts +0 -0
package/lib/logger.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getReq, getRes } from './dx.js';
|
|
2
|
+
import { hrtime } from 'node:process';
|
|
3
|
+
export function logJson(json) {
|
|
4
|
+
console.log(JSON.stringify(json));
|
|
5
|
+
}
|
|
6
|
+
let requestCount = 0;
|
|
7
|
+
export default function makeLogger(log = logJson, { timezoneOffset = 0 } = {}) {
|
|
8
|
+
// offset designator so a shifted timestamp is unambiguous (e.g. +09:00, -05:00, +00:00)
|
|
9
|
+
const offsetMin = Math.round(timezoneOffset * 60);
|
|
10
|
+
const offset = `${offsetMin < 0 ? '-' : '+'}${[
|
|
11
|
+
String(Math.floor(Math.abs(offsetMin) / 60)).padStart(2, '0'),
|
|
12
|
+
String(Math.abs(offsetMin) % 60).padStart(2, '0'),
|
|
13
|
+
].join(':')}`;
|
|
14
|
+
return function logger(next) {
|
|
15
|
+
const res = getRes();
|
|
16
|
+
const req = getReq();
|
|
17
|
+
const logId = requestCount++;
|
|
18
|
+
const start = hrtime.bigint();
|
|
19
|
+
const now = new Date(Date.now() + timezoneOffset * 60 * 60 * 1000);
|
|
20
|
+
log({
|
|
21
|
+
level: 'info',
|
|
22
|
+
id: logId,
|
|
23
|
+
timestamp: [
|
|
24
|
+
[
|
|
25
|
+
now.getUTCFullYear(),
|
|
26
|
+
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
27
|
+
String(now.getUTCDate()).padStart(2, '0'),
|
|
28
|
+
].join('-'),
|
|
29
|
+
[
|
|
30
|
+
String(now.getUTCHours()).padStart(2, '0'),
|
|
31
|
+
String(now.getUTCMinutes()).padStart(2, '0'),
|
|
32
|
+
[String(now.getUTCSeconds()).padStart(2, '0'), String(now.getUTCMilliseconds()).padStart(3, '0')].join('.'),
|
|
33
|
+
].join(':'),
|
|
34
|
+
].join('T') + offset,
|
|
35
|
+
remoteAddress: req.socket.remoteAddress,
|
|
36
|
+
method: req.method,
|
|
37
|
+
url: req.url,
|
|
38
|
+
httpVersion: `HTTP/${req.httpVersion}`,
|
|
39
|
+
headers: process.env.NODE_ENV === 'production'
|
|
40
|
+
? req.headers
|
|
41
|
+
: Object.fromEntries(Object.entries(req.headers).filter(([k]) => [
|
|
42
|
+
'host',
|
|
43
|
+
'referer',
|
|
44
|
+
'referrer',
|
|
45
|
+
'user-agent',
|
|
46
|
+
'x-forwarded-proto',
|
|
47
|
+
'x-forwarded-host',
|
|
48
|
+
'x-forwarded-for',
|
|
49
|
+
].includes(k))),
|
|
50
|
+
});
|
|
51
|
+
res.once('finish', end).once('close', end).once('error', end);
|
|
52
|
+
return next();
|
|
53
|
+
function end() {
|
|
54
|
+
res.off('finish', end).off('close', end).off('error', end);
|
|
55
|
+
const durationNs = hrtime.bigint() - start;
|
|
56
|
+
log({
|
|
57
|
+
level: 'info',
|
|
58
|
+
id: logId,
|
|
59
|
+
duration: Number(durationNs) / 1e6, // ms
|
|
60
|
+
headers: res.getHeaders(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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","sourcesContent":["import {getReq, getRes} from './dx.js'\nimport {hrtime} from 'node:process'\n\nexport function logJson(json: any) {\n\tconsole.log(JSON.stringify(json))\n}\n\nlet requestCount = 0\nexport default function makeLogger(log = logJson, {timezoneOffset = 0} = {}) {\n\treturn function logger(next: () => any) {\n\t\tconst res = getRes()\n\t\tconst req = getReq()\n\t\tconst logId = requestCount++\n\n\t\tconst start = hrtime.bigint()\n\t\tconst now = new Date(Date.now() + timezoneOffset * 60 * 60 * 1000)\n\n\t\tlog({\n\t\t\tlevel: 'info',\n\t\t\tid: logId,\n\t\t\ttimestamp: [\n\t\t\t\t[\n\t\t\t\t\tnow.getUTCFullYear(),\n\t\t\t\t\tString(now.getUTCMonth() + 1).padStart(2, '0'),\n\t\t\t\t\tString(now.getUTCDate()).padStart(2, '0'),\n\t\t\t\t].join('-'),\n\t\t\t\t[\n\t\t\t\t\tString(now.getUTCHours()).padStart(2, '0'),\n\t\t\t\t\tString(now.getUTCMinutes()).padStart(2, '0'),\n\t\t\t\t\t[String(now.getUTCSeconds()).padStart(2, '0'), String(now.getUTCMilliseconds()).padStart(3, '0')].join('.'),\n\t\t\t\t].join(':'),\n\t\t\t].join('T'),\n\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\tmethod: req.method,\n\t\t\turl: req.url,\n\t\t\thttpVersion: `HTTP/${req.httpVersion}`,\n\t\t\theaders:\n\t\t\t\tprocess.env.NODE_ENV === 'production'\n\t\t\t\t\t? req.headers\n\t\t\t\t\t: Object.fromEntries(\n\t\t\t\t\t\t\tObject.entries(req.headers).filter(([k]) =>\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t'host',\n\t\t\t\t\t\t\t\t\t'referer',\n\t\t\t\t\t\t\t\t\t'referrer',\n\t\t\t\t\t\t\t\t\t'user-agent',\n\t\t\t\t\t\t\t\t\t'x-forwarded-proto',\n\t\t\t\t\t\t\t\t\t'x-forwarded-host',\n\t\t\t\t\t\t\t\t\t'x-forwarded-for',\n\t\t\t\t\t\t\t\t].includes(k),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t})\n\n\t\tres.once('finish', end).once('close', end).once('error', end)\n\n\t\treturn next()\n\n\t\tfunction end() {\n\t\t\tres.off('finish', end).off('close', end).off('error', end)\n\t\t\tconst durationNs = hrtime.bigint() - start\n\t\t\tlog({\n\t\t\t\tlevel: 'info',\n\t\t\t\tid: logId,\n\t\t\t\tduration: Number(durationNs) / 1e6, // ms\n\t\t\t\theaders: res.getHeaders(),\n\t\t\t})\n\t\t}\n\t}\n}\n"]}
|
package/lib/router.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Chainable } from './dx.js';
|
|
2
|
+
interface URLPatternOptions {
|
|
3
|
+
}
|
|
4
|
+
interface RouteContext {
|
|
5
|
+
matched: URLPatternResult;
|
|
6
|
+
next(): any;
|
|
7
|
+
}
|
|
8
|
+
interface Route {
|
|
9
|
+
(context: RouteContext): any;
|
|
10
|
+
}
|
|
11
|
+
interface Routes {
|
|
12
|
+
[k: string]: Route;
|
|
13
|
+
}
|
|
14
|
+
interface RouterOptions extends URLPatternOptions {
|
|
15
|
+
prefix?: string;
|
|
16
|
+
}
|
|
17
|
+
type Router = {
|
|
18
|
+
patch(routes: Routes, options?: RouterOptions): Chainable;
|
|
19
|
+
patch(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
20
|
+
trace(routes: Routes, options?: RouterOptions): Chainable;
|
|
21
|
+
trace(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
22
|
+
options(routes: Routes, options?: RouterOptions): Chainable;
|
|
23
|
+
options(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
24
|
+
connect(routes: Routes, options?: RouterOptions): Chainable;
|
|
25
|
+
connect(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
26
|
+
delete(routes: Routes, options?: RouterOptions): Chainable;
|
|
27
|
+
delete(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
28
|
+
put(routes: Routes, options?: RouterOptions): Chainable;
|
|
29
|
+
put(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
30
|
+
post(routes: Routes, options?: RouterOptions): Chainable;
|
|
31
|
+
post(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
32
|
+
head(routes: Routes, options?: RouterOptions): Chainable;
|
|
33
|
+
head(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
34
|
+
get(routes: Routes, options?: RouterOptions): Chainable;
|
|
35
|
+
get(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
36
|
+
all(routes: Routes, options?: RouterOptions): Chainable;
|
|
37
|
+
all(pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
38
|
+
method(method: string, routes: Routes, options?: RouterOptions): Chainable;
|
|
39
|
+
method(method: string, pattern: string, route: Route, options?: RouterOptions): Chainable;
|
|
40
|
+
};
|
|
41
|
+
export declare const router: Router;
|
|
42
|
+
export {};
|
package/lib/router.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getReq } from './dx.js';
|
|
2
|
+
import { urlFromReq } from './bodyHelpers.js';
|
|
3
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
|
4
|
+
const allMethods = ['get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch'];
|
|
5
|
+
function makeRouter(method, // undefined means any method
|
|
6
|
+
routes, { prefix = '' } = {}) {
|
|
7
|
+
const routeWithUrlPatterns = routes.map(([pattern, handler]) => [new URLPattern({ pathname: `${prefix}${pattern}` }), handler]);
|
|
8
|
+
return next => {
|
|
9
|
+
const req = getReq();
|
|
10
|
+
const want = method?.toUpperCase();
|
|
11
|
+
// a GET route also answers HEAD: HEAD must behave like GET, and writeRes strips the body
|
|
12
|
+
if (want !== undefined && req.method !== want && (want !== 'GET' || req.method !== 'HEAD'))
|
|
13
|
+
return next();
|
|
14
|
+
for (const [urlPattern, handler] of routeWithUrlPatterns) {
|
|
15
|
+
// '' matches nothing
|
|
16
|
+
// '/' matches both https://example.com and https://example.com/
|
|
17
|
+
// '/foo' matches https://example.com/foo but not https://example.com/foo/
|
|
18
|
+
// '/foo/' matches https://example.com/foo/ but not https://example.com/foo
|
|
19
|
+
// path can be '*' for OPTIONS request
|
|
20
|
+
// to test: curl -X OPTIONS --request-target '*' http://localhost:3000 -D -
|
|
21
|
+
// req.url === '*'
|
|
22
|
+
// new URL('*', 'https://example.com').pathname === '/*'
|
|
23
|
+
const matched = urlPattern.exec({ pathname: urlFromReq(req).pathname });
|
|
24
|
+
if (matched)
|
|
25
|
+
return handler({ matched, next });
|
|
26
|
+
}
|
|
27
|
+
return next();
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export const router = {
|
|
31
|
+
method(method, ...params) {
|
|
32
|
+
return typeof params[0] === 'string'
|
|
33
|
+
? makeRouter(method, [[params[0], params[1]]], params[2])
|
|
34
|
+
: makeRouter(method, Object.entries(params[0]), params[1]);
|
|
35
|
+
},
|
|
36
|
+
all(...params) {
|
|
37
|
+
return typeof params[0] === 'string'
|
|
38
|
+
? makeRouter(undefined, [[params[0], params[1]]], params[2])
|
|
39
|
+
: makeRouter(undefined, Object.entries(params[0]), params[1]);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
for (const method of allMethods)
|
|
43
|
+
router[method] = router.method.bind(router, method);
|
|
@@ -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
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getReq, getRes } from './dx.js';
|
|
2
|
+
import { sendFileTrusted } from './staticHelpers.js';
|
|
3
|
+
import { urlFromReq } from './bodyHelpers.js';
|
|
4
|
+
export function chainStatic(pattern, { getPathname, ...options }) {
|
|
5
|
+
const urlPattern = new URLPattern({ pathname: pattern });
|
|
6
|
+
return async (next) => {
|
|
7
|
+
const req = getReq();
|
|
8
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
9
|
+
return next();
|
|
10
|
+
const { pathname } = urlFromReq(req);
|
|
11
|
+
const matched = urlPattern.exec({ pathname });
|
|
12
|
+
if (!matched)
|
|
13
|
+
return next();
|
|
14
|
+
const res = getRes();
|
|
15
|
+
try {
|
|
16
|
+
await sendFileTrusted(req, res, getPathname?.(matched) ?? decodeURIComponent(pathname), options);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
if (!res.headersSent)
|
|
20
|
+
return next(e); // pre-stream error: user error middleware can still respond
|
|
21
|
+
}
|
|
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,6 +1,7 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
1
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
-
|
|
2
|
+
export type HttpError = Error & {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
};
|
|
4
5
|
export interface SendFileOptions {
|
|
5
6
|
allowDotfiles?: boolean;
|
|
6
7
|
root?: string;
|
|
@@ -10,9 +11,10 @@ export interface SendFileOptions {
|
|
|
10
11
|
disableCacheControl?: boolean;
|
|
11
12
|
maxAge?: number;
|
|
12
13
|
immutable?: boolean;
|
|
14
|
+
disableFollowSymlinks?: boolean;
|
|
13
15
|
end?: number;
|
|
14
16
|
start?: number;
|
|
15
17
|
}
|
|
16
18
|
export declare function sendFileTrusted(req: IncomingMessage, res: ServerResponse, pathname: string, // plain path, not URI-encoded
|
|
17
19
|
{ root, allowDotfiles, start, end, disableAcceptRanges, disableLastModified, etag, disableCacheControl, maxAge, // 1 year
|
|
18
|
-
immutable, }?: SendFileOptions): Promise<
|
|
20
|
+
immutable, disableFollowSymlinks, }?: SendFileOptions): Promise<undefined>;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { open, realpath } from 'node:fs/promises';
|
|
3
|
+
import { entityTagPath, statTag } from './vendors/etag.js';
|
|
4
|
+
import { contentTypeForExtension } from './vendors/mime.js';
|
|
5
|
+
import { fresh, parseHttpDate, parseTokenList } from './vendors/fresh.js';
|
|
6
|
+
import { parseRange } from './vendors/rangeParser.js';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
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 = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
16
|
+
export async function sendFileTrusted(req, res, pathname, // plain path, not URI-encoded
|
|
17
|
+
{ root, allowDotfiles, start = 0, end, disableAcceptRanges, disableLastModified, etag = 'weak', disableCacheControl, maxAge = 60 * 60 * 24 * 365 * 1000, // 1 year
|
|
18
|
+
immutable, disableFollowSymlinks, } = {}) {
|
|
19
|
+
// null byte(s)
|
|
20
|
+
if (pathname.includes('\0'))
|
|
21
|
+
throw httpError('Forbidden', 403);
|
|
22
|
+
let parts;
|
|
23
|
+
if (root) {
|
|
24
|
+
// normalize
|
|
25
|
+
pathname = path.normalize(`.${path.sep}${pathname}`);
|
|
26
|
+
// malicious path
|
|
27
|
+
if (upPathRegexp.test(pathname))
|
|
28
|
+
throw httpError('Forbidden', 403);
|
|
29
|
+
// explode path parts
|
|
30
|
+
parts = pathname.split(path.sep);
|
|
31
|
+
// join / normalize from optional root dir
|
|
32
|
+
pathname = path.normalize(path.join(root, pathname));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// malicious path
|
|
36
|
+
if (upPathRegexp.test(pathname))
|
|
37
|
+
throw httpError('Forbidden', 403);
|
|
38
|
+
// explode path parts
|
|
39
|
+
parts = path.normalize(pathname).split(path.sep);
|
|
40
|
+
// join / normalize from optional root dir
|
|
41
|
+
pathname = path.resolve(pathname);
|
|
42
|
+
}
|
|
43
|
+
// dotfile handling
|
|
44
|
+
if (parts.some(part => part.length > 1 && part[0] === '.') && !allowDotfiles)
|
|
45
|
+
throw httpError('Forbidden: dotfiles are not allowed', 403);
|
|
46
|
+
// pathEndsWithSep
|
|
47
|
+
if (pathname[pathname.length - 1] === path.sep)
|
|
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);
|
|
77
|
+
}
|
|
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))());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
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;
|
|
143
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await handle.close().catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function isRangeFresh(req, res) {
|
|
187
|
+
const ifRange = req.headers['if-range'];
|
|
188
|
+
if (!ifRange)
|
|
189
|
+
return true;
|
|
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")
|
|
192
|
+
if (ifRange.indexOf('"') !== -1) {
|
|
193
|
+
const etag = res.getHeader('ETag');
|
|
194
|
+
return etag !== undefined && ifRange === etag;
|
|
195
|
+
}
|
|
196
|
+
// if-range as modified date
|
|
197
|
+
const lastModified = res.getHeader('Last-Modified');
|
|
198
|
+
return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
|
|
199
|
+
}
|
|
200
|
+
function contentRange(type, size, range) {
|
|
201
|
+
return `${type} ${range ? range.start + '-' + range.end : '*'}/${size}`;
|
|
202
|
+
}
|
|
@@ -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/{cjs → lib}/stream.d.ts
RENAMED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
4
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
5
1
|
import type { IncomingMessage } from 'node:http';
|
|
6
2
|
import type { Readable } from 'node:stream';
|
|
7
|
-
import
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function readStream(stream: Readable, { length, limit }: {
|
|
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, }: {
|
|
10
5
|
length?: number;
|
|
11
6
|
limit?: number;
|
|
12
|
-
}): Promise<Buffer
|
|
7
|
+
}): Promise<Buffer<ArrayBufferLike>>;
|
package/lib/stream.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|
|
8
|
+
// note: there might be multiple encodings applied to the stream
|
|
9
|
+
// we only support one encoding
|
|
10
|
+
export function getContentStream(req, encoding, disableInflate) {
|
|
11
|
+
if (disableInflate && encoding !== 'identity')
|
|
12
|
+
throw new Error(`content-encoding ${encoding} is not supported`);
|
|
13
|
+
switch (encoding) {
|
|
14
|
+
case 'deflate': {
|
|
15
|
+
const stream = createInflate();
|
|
16
|
+
req.pipe(stream);
|
|
17
|
+
return stream;
|
|
18
|
+
}
|
|
19
|
+
case 'gzip': {
|
|
20
|
+
const stream = createGunzip();
|
|
21
|
+
req.pipe(stream);
|
|
22
|
+
return stream;
|
|
23
|
+
}
|
|
24
|
+
case 'br': {
|
|
25
|
+
const stream = createBrotliDecompress();
|
|
26
|
+
req.pipe(stream);
|
|
27
|
+
return stream;
|
|
28
|
+
}
|
|
29
|
+
case 'identity':
|
|
30
|
+
return req;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`unsupported content-encoding ${encoding}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// https://github.com/stream-utils/raw-body/blob/191e4b6506dcf77198eed01c8feb4b6817008342/index.js#L155
|
|
36
|
+
export async function readStream(stream, { length, limit, }) {
|
|
37
|
+
let completed = false;
|
|
38
|
+
// check the length and limit options.
|
|
39
|
+
// note: we intentionally leave the stream paused,
|
|
40
|
+
// so users should handle the stream themselves.
|
|
41
|
+
if (limit !== undefined && length !== undefined && length > limit)
|
|
42
|
+
throw bodyError('request entity too large', 413);
|
|
43
|
+
let received = 0;
|
|
44
|
+
const buffers = [];
|
|
45
|
+
const defer = Promise.withResolvers();
|
|
46
|
+
// attach listeners
|
|
47
|
+
stream.on('aborted', onAborted);
|
|
48
|
+
stream.on('close', onClose);
|
|
49
|
+
stream.on('data', onData);
|
|
50
|
+
stream.on('end', onEnd);
|
|
51
|
+
stream.on('error', onError);
|
|
52
|
+
function done(err, result) {
|
|
53
|
+
if (completed)
|
|
54
|
+
return;
|
|
55
|
+
completed = true;
|
|
56
|
+
onClose();
|
|
57
|
+
if (err) {
|
|
58
|
+
stream.unpipe?.();
|
|
59
|
+
stream.pause?.();
|
|
60
|
+
defer.reject(err);
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
defer.resolve(result);
|
|
64
|
+
}
|
|
65
|
+
function onData(chunk) {
|
|
66
|
+
if (completed)
|
|
67
|
+
return;
|
|
68
|
+
received += chunk.length;
|
|
69
|
+
if (limit !== undefined && received > limit) {
|
|
70
|
+
done(bodyError('request entity too large', 413));
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
buffers.push(chunk);
|
|
74
|
+
}
|
|
75
|
+
function onError(err) {
|
|
76
|
+
done(err);
|
|
77
|
+
}
|
|
78
|
+
function onEnd() {
|
|
79
|
+
if (length !== undefined && received !== length)
|
|
80
|
+
done(bodyError('request size did not match content length', 400));
|
|
81
|
+
else
|
|
82
|
+
done(undefined, Buffer.concat(buffers));
|
|
83
|
+
}
|
|
84
|
+
function onAborted() {
|
|
85
|
+
done(bodyError('request aborted', 400));
|
|
86
|
+
}
|
|
87
|
+
function onClose() {
|
|
88
|
+
buffers.splice(0, buffers.length);
|
|
89
|
+
stream.off('aborted', onAborted);
|
|
90
|
+
stream.off('data', onData);
|
|
91
|
+
stream.off('end', onEnd);
|
|
92
|
+
stream.off('error', onError);
|
|
93
|
+
stream.off('close', onClose);
|
|
94
|
+
}
|
|
95
|
+
return await defer.promise;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,sBAAsB,EAAE,YAAY,EAAE,aAAa,EAAC,MAAM,WAAW,CAAA;AAE7E,gEAAgE;AAChE,+BAA+B;AAC/B,MAAM,UAAU,gBAAgB,CAAC,GAAoB,EAAE,QAAgB,EAAE,cAAwB;IAChG,IAAI,cAAc,IAAI,QAAQ,KAAK,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,QAAQ,mBAAmB,CAAC,CAAA;IAE/G,QAAQ,QAAQ,EAAE,CAAC;QAClB,KAAK,SAAS,CAAC,CAAC,CAAC;YAChB,MAAM,MAAM,GAAG,aAAa,EAAE,CAAA;YAC9B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAChB,OAAO,MAAM,CAAA;QACd,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACb,MAAM,MAAM,GAAG,YAAY,EAAE,CAAA;YAC7B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAChB,OAAO,MAAM,CAAA;QACd,CAAC;QACD,KAAK,IAAI,CAAC,CAAC,CAAC;YACX,MAAM,MAAM,GAAG,sBAAsB,EAAE,CAAA;YACvC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAChB,OAAO,MAAM,CAAA;QACd,CAAC;QACD,KAAK,UAAU;YACd,OAAO,GAAG,CAAA;QACX;YACC,MAAM,IAAI,KAAK,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAA;IAC7D,CAAC;AACF,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,MAAgB,EAChB,EACC,MAAM,EACN,KAAK,GAIL;IAED,IAAI,SAAS,GAAG,KAAK,CAAA;IAErB,sCAAsC;IACtC,kDAAkD;IAClD,gDAAgD;IAChD,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,GAAG,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAA;IAE9G,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,EAAU,CAAA;IAE7C,mBAAmB;IACnB,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IAC/B,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAC3B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACzB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IACvB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAE3B,SAAS,IAAI,CAAC,GAAW,EAAE,MAAe;QACzC,IAAI,SAAS;YAAE,OAAM;QACrB,SAAS,GAAG,IAAI,CAAA;QAChB,OAAO,EAAE,CAAA;QACT,IAAI,GAAG,EAAE,CAAC;YACT,MAAM,CAAC,MAAM,EAAE,EAAE,CAAA;YACjB,MAAM,CAAC,KAAK,EAAE,EAAE,CAAA;YAChB,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAClB,CAAC;;YAAM,KAAK,CAAC,OAAO,CAAC,MAAO,CAAC,CAAA;IAC9B,CAAC;IAED,SAAS,MAAM,CAAC,KAAa;QAC5B,IAAI,SAAS;YAAE,OAAM;QACrB,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAA;QACxB,IAAI,KAAK,KAAK,SAAS,IAAI,QAAQ,GAAG,KAAK,EAAE,CAAC;YAC7C,IAAI,CAAC,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC,CAAA;QAC5C,CAAC;;YAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3B,CAAC;IAED,SAAS,OAAO,CAAC,GAAU;QAC1B,IAAI,CAAC,GAAG,CAAC,CAAA;IACV,CAAC;IACD,SAAS,KAAK;QACb,IAAI,MAAM,KAAK,SAAS,IAAI,QAAQ,KAAK,MAAM;YAAE,IAAI,CAAC,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC,CAAA;;YACxG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;IAC7C,CAAC;IACD,SAAS,SAAS;QACjB,IAAI,CAAC,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAA;IACnC,CAAC;IACD,SAAS,OAAO;QACf,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;QACjC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QAChC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC1B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QACxB,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAC5B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAC7B,CAAC;IAED,OAAO,MAAM,KAAK,CAAC,OAAO,CAAA;AAC3B,CAAC","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"]}
|