@wooksjs/http-body 0.5.25 → 0.6.0

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/dist/index.cjs CHANGED
@@ -24,19 +24,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  //#endregion
25
25
  const __wooksjs_event_http = __toESM(require("@wooksjs/event-http"));
26
26
 
27
- //#region packages/http-body/src/utils/body-compressor.ts
28
- const compressors = { identity: {
29
- compress: (v) => v,
30
- uncompress: (v) => v
31
- } };
32
- async function uncompressBody(encodings, body) {
33
- let newBody = body;
34
- for (const e of encodings.reverse()) {
35
- const cmp = compressors[e];
36
- if (!cmp) throw new Error(`Usupported compression type "${e}".`);
37
- newBody = await cmp.uncompress(body);
38
- }
39
- return newBody;
27
+ //#region packages/http-body/src/utils/safe-json.ts
28
+ const ILLEGAL_KEYS = [
29
+ "__proto__",
30
+ "constructor",
31
+ "prototype"
32
+ ];
33
+ const illigalKeySet = new Set(ILLEGAL_KEYS);
34
+ function safeJsonParse(src) {
35
+ return JSON.parse(src, (key, value) => {
36
+ assertKey(key);
37
+ return value;
38
+ });
39
+ }
40
+ function assertKey(k) {
41
+ if (illigalKeySet.has(k)) throw new __wooksjs_event_http.HttpError(400, `Illegal key name "${k}"`);
40
42
  }
41
43
 
42
44
  //#endregion
@@ -45,7 +47,7 @@ function useBody() {
45
47
  const { store } = (0, __wooksjs_event_http.useHttpContext)();
46
48
  const { init } = store("request");
47
49
  const { rawBody } = (0, __wooksjs_event_http.useRequest)();
48
- const { "content-type": contentType, "content-encoding": contentEncoding } = (0, __wooksjs_event_http.useHeaders)();
50
+ const { "content-type": contentType } = (0, __wooksjs_event_http.useHeaders)();
49
51
  function contentIs(type) {
50
52
  return (contentType || "").includes(type);
51
53
  }
@@ -56,27 +58,18 @@ function useBody() {
56
58
  const isBinary = () => init("isBinary", () => contentIs("application/octet-stream"));
57
59
  const isFormData = () => init("isFormData", () => contentIs("multipart/form-data"));
58
60
  const isUrlencoded = () => init("isUrlencoded", () => contentIs("application/x-www-form-urlencoded"));
59
- const isCompressed = () => init("isCompressed", () => {
60
- const parts = contentEncodings();
61
- for (const p of parts) if ([
62
- "deflate",
63
- "gzip",
64
- "br"
65
- ].includes(p)) return true;
66
- return false;
67
- });
68
- const contentEncodings = () => init("contentEncodings", () => (contentEncoding || "").split(",").map((p) => p.trim()).filter((p) => !!p));
69
61
  const parseBody = () => init("parsed", async () => {
70
- const body = await uncompressBody(contentEncodings(), (await rawBody()).toString());
71
- if (isJson()) return jsonParser(body);
72
- else if (isFormData()) return formDataParser(body);
73
- else if (isUrlencoded()) return urlEncodedParser(body);
74
- else if (isBinary()) return textParser(body);
75
- else return textParser(body);
62
+ const body = await rawBody();
63
+ const sBody = body.toString();
64
+ if (isJson()) return jsonParser(sBody);
65
+ else if (isFormData()) return formDataParser(sBody);
66
+ else if (isUrlencoded()) return urlEncodedParser(sBody);
67
+ else if (isBinary()) return textParser(sBody);
68
+ else return textParser(sBody);
76
69
  });
77
70
  function jsonParser(v) {
78
71
  try {
79
- return JSON.parse(v);
72
+ return safeJsonParse(v);
80
73
  } catch (error) {
81
74
  throw new __wooksjs_event_http.HttpError(400, error.message);
82
75
  }
@@ -85,43 +78,57 @@ else return textParser(body);
85
78
  return v;
86
79
  }
87
80
  function formDataParser(v) {
81
+ const MAX_PARTS = 255;
82
+ const MAX_KEY_LENGTH = 100;
83
+ const MAX_VALUE_LENGTH = 102400;
88
84
  const boundary = `--${(/boundary=([^;]+)(?:;|$)/u.exec(contentType || "") || [, ""])[1]}`;
89
85
  if (!boundary) throw new __wooksjs_event_http.HttpError(__wooksjs_event_http.EHttpStatusCode.BadRequest, "form-data boundary not recognized");
90
86
  const parts = v.trim().split(boundary);
91
- const result = {};
87
+ const result = Object.create(null);
92
88
  let key = "";
93
89
  let partContentType = "text/plain";
90
+ let partCount = 0;
94
91
  for (const part of parts) {
95
92
  parsePart();
96
93
  key = "";
97
94
  partContentType = "text/plain";
95
+ if (!part.trim() || part.trim() === "--") continue;
96
+ partCount++;
97
+ if (partCount > MAX_PARTS) throw new __wooksjs_event_http.HttpError(413, "Too many form fields");
98
98
  let valueMode = false;
99
- const lines = part.trim().split(/\n/u).map((s) => s.trim());
100
- for (const line of lines) if (valueMode) if (result[key]) result[key] += `\n${line}`;
101
- else result[key] = line;
102
- else {
103
- if (!line || line === "--") {
99
+ const lines = part.trim().split(/\n/u).map((l) => l.trim());
100
+ for (const line of lines) {
101
+ if (valueMode) {
102
+ if (line.length + String(result[key] ?? "").length > MAX_VALUE_LENGTH) throw new __wooksjs_event_http.HttpError(413, `Field "${key}" is too large`);
103
+ result[key] = (result[key] ? `${result[key]}\n` : "") + line;
104
+ continue;
105
+ }
106
+ if (!line) {
104
107
  valueMode = !!key;
105
- if (valueMode) key = key.replace(/^["']/u, "").replace(/["']$/u, "");
106
108
  continue;
107
109
  }
108
110
  if (line.toLowerCase().startsWith("content-disposition: form-data;")) {
109
- key = (/name=([^;]+)/.exec(line) || [])[1];
110
- if (!key) throw new __wooksjs_event_http.HttpError(__wooksjs_event_http.EHttpStatusCode.BadRequest, `Could not read multipart name: ${line}`);
111
+ key = (/name=([^;]+)/.exec(line) || [])[1].replace(/^["']|["']$/g, "") ?? "";
112
+ if (!key) throw new __wooksjs_event_http.HttpError(400, `Could not read multipart name: ${line}`);
113
+ if (key.length > MAX_KEY_LENGTH) throw new __wooksjs_event_http.HttpError(413, "Field name too long");
114
+ if ([
115
+ "__proto__",
116
+ "constructor",
117
+ "prototype"
118
+ ].includes(key)) throw new __wooksjs_event_http.HttpError(400, `Illegal key name "${key}"`);
111
119
  continue;
112
120
  }
113
121
  if (line.toLowerCase().startsWith("content-type:")) {
114
- partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1];
115
- if (!partContentType) throw new __wooksjs_event_http.HttpError(__wooksjs_event_http.EHttpStatusCode.BadRequest, `Could not read content-type: ${line}`);
122
+ partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1] ?? "";
116
123
  continue;
117
124
  }
118
125
  }
119
126
  }
120
127
  parsePart();
128
+ return result;
121
129
  function parsePart() {
122
- if (key && partContentType.includes("application/json")) result[key] = JSON.parse(result[key]);
130
+ if (key && partContentType.includes("application/json") && typeof result[key] === "string") result[key] = safeJsonParse(result[key]);
123
131
  }
124
- return result;
125
132
  }
126
133
  function urlEncodedParser(v) {
127
134
  return new __wooksjs_event_http.WooksURLSearchParams(v.trim()).toJson();
@@ -134,17 +141,10 @@ else {
134
141
  isBinary,
135
142
  isFormData,
136
143
  isUrlencoded,
137
- isCompressed,
138
- contentEncodings,
139
144
  parseBody,
140
145
  rawBody
141
146
  };
142
147
  }
143
- function registerBodyCompressor(name, compressor) {
144
- if (compressors[name]) throw new Error(`Body compressor "${name}" already registered.`);
145
- compressors[name] = compressor;
146
- }
147
148
 
148
149
  //#endregion
149
- exports.registerBodyCompressor = registerBodyCompressor
150
150
  exports.useBody = useBody
package/dist/index.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- interface TBodyCompressor {
2
- compress: (data: string) => string | Promise<string>;
3
- uncompress: (data: string) => string | Promise<string>;
4
- }
5
-
6
1
  declare function useBody(): {
7
2
  isJson: () => boolean;
8
3
  isHtml: () => boolean;
@@ -11,11 +6,8 @@ declare function useBody(): {
11
6
  isBinary: () => boolean;
12
7
  isFormData: () => boolean;
13
8
  isUrlencoded: () => boolean;
14
- isCompressed: () => boolean;
15
- contentEncodings: () => string[];
16
9
  parseBody: <T>() => Promise<T>;
17
10
  rawBody: () => Promise<Buffer<ArrayBufferLike>>;
18
11
  };
19
- declare function registerBodyCompressor(name: string, compressor: TBodyCompressor): void;
20
12
 
21
- export { registerBodyCompressor, useBody };
13
+ export { useBody };
package/dist/index.mjs CHANGED
@@ -1,18 +1,20 @@
1
1
  import { EHttpStatusCode, HttpError, WooksURLSearchParams, useHeaders, useHttpContext, useRequest } from "@wooksjs/event-http";
2
2
 
3
- //#region packages/http-body/src/utils/body-compressor.ts
4
- const compressors = { identity: {
5
- compress: (v) => v,
6
- uncompress: (v) => v
7
- } };
8
- async function uncompressBody(encodings, body) {
9
- let newBody = body;
10
- for (const e of encodings.reverse()) {
11
- const cmp = compressors[e];
12
- if (!cmp) throw new Error(`Usupported compression type "${e}".`);
13
- newBody = await cmp.uncompress(body);
14
- }
15
- return newBody;
3
+ //#region packages/http-body/src/utils/safe-json.ts
4
+ const ILLEGAL_KEYS = [
5
+ "__proto__",
6
+ "constructor",
7
+ "prototype"
8
+ ];
9
+ const illigalKeySet = new Set(ILLEGAL_KEYS);
10
+ function safeJsonParse(src) {
11
+ return JSON.parse(src, (key, value) => {
12
+ assertKey(key);
13
+ return value;
14
+ });
15
+ }
16
+ function assertKey(k) {
17
+ if (illigalKeySet.has(k)) throw new HttpError(400, `Illegal key name "${k}"`);
16
18
  }
17
19
 
18
20
  //#endregion
@@ -21,7 +23,7 @@ function useBody() {
21
23
  const { store } = useHttpContext();
22
24
  const { init } = store("request");
23
25
  const { rawBody } = useRequest();
24
- const { "content-type": contentType, "content-encoding": contentEncoding } = useHeaders();
26
+ const { "content-type": contentType } = useHeaders();
25
27
  function contentIs(type) {
26
28
  return (contentType || "").includes(type);
27
29
  }
@@ -32,27 +34,18 @@ function useBody() {
32
34
  const isBinary = () => init("isBinary", () => contentIs("application/octet-stream"));
33
35
  const isFormData = () => init("isFormData", () => contentIs("multipart/form-data"));
34
36
  const isUrlencoded = () => init("isUrlencoded", () => contentIs("application/x-www-form-urlencoded"));
35
- const isCompressed = () => init("isCompressed", () => {
36
- const parts = contentEncodings();
37
- for (const p of parts) if ([
38
- "deflate",
39
- "gzip",
40
- "br"
41
- ].includes(p)) return true;
42
- return false;
43
- });
44
- const contentEncodings = () => init("contentEncodings", () => (contentEncoding || "").split(",").map((p) => p.trim()).filter((p) => !!p));
45
37
  const parseBody = () => init("parsed", async () => {
46
- const body = await uncompressBody(contentEncodings(), (await rawBody()).toString());
47
- if (isJson()) return jsonParser(body);
48
- else if (isFormData()) return formDataParser(body);
49
- else if (isUrlencoded()) return urlEncodedParser(body);
50
- else if (isBinary()) return textParser(body);
51
- else return textParser(body);
38
+ const body = await rawBody();
39
+ const sBody = body.toString();
40
+ if (isJson()) return jsonParser(sBody);
41
+ else if (isFormData()) return formDataParser(sBody);
42
+ else if (isUrlencoded()) return urlEncodedParser(sBody);
43
+ else if (isBinary()) return textParser(sBody);
44
+ else return textParser(sBody);
52
45
  });
53
46
  function jsonParser(v) {
54
47
  try {
55
- return JSON.parse(v);
48
+ return safeJsonParse(v);
56
49
  } catch (error) {
57
50
  throw new HttpError(400, error.message);
58
51
  }
@@ -61,43 +54,57 @@ else return textParser(body);
61
54
  return v;
62
55
  }
63
56
  function formDataParser(v) {
57
+ const MAX_PARTS = 255;
58
+ const MAX_KEY_LENGTH = 100;
59
+ const MAX_VALUE_LENGTH = 102400;
64
60
  const boundary = `--${(/boundary=([^;]+)(?:;|$)/u.exec(contentType || "") || [, ""])[1]}`;
65
61
  if (!boundary) throw new HttpError(EHttpStatusCode.BadRequest, "form-data boundary not recognized");
66
62
  const parts = v.trim().split(boundary);
67
- const result = {};
63
+ const result = Object.create(null);
68
64
  let key = "";
69
65
  let partContentType = "text/plain";
66
+ let partCount = 0;
70
67
  for (const part of parts) {
71
68
  parsePart();
72
69
  key = "";
73
70
  partContentType = "text/plain";
71
+ if (!part.trim() || part.trim() === "--") continue;
72
+ partCount++;
73
+ if (partCount > MAX_PARTS) throw new HttpError(413, "Too many form fields");
74
74
  let valueMode = false;
75
- const lines = part.trim().split(/\n/u).map((s) => s.trim());
76
- for (const line of lines) if (valueMode) if (result[key]) result[key] += `\n${line}`;
77
- else result[key] = line;
78
- else {
79
- if (!line || line === "--") {
75
+ const lines = part.trim().split(/\n/u).map((l) => l.trim());
76
+ for (const line of lines) {
77
+ if (valueMode) {
78
+ if (line.length + String(result[key] ?? "").length > MAX_VALUE_LENGTH) throw new HttpError(413, `Field "${key}" is too large`);
79
+ result[key] = (result[key] ? `${result[key]}\n` : "") + line;
80
+ continue;
81
+ }
82
+ if (!line) {
80
83
  valueMode = !!key;
81
- if (valueMode) key = key.replace(/^["']/u, "").replace(/["']$/u, "");
82
84
  continue;
83
85
  }
84
86
  if (line.toLowerCase().startsWith("content-disposition: form-data;")) {
85
- key = (/name=([^;]+)/.exec(line) || [])[1];
86
- if (!key) throw new HttpError(EHttpStatusCode.BadRequest, `Could not read multipart name: ${line}`);
87
+ key = (/name=([^;]+)/.exec(line) || [])[1].replace(/^["']|["']$/g, "") ?? "";
88
+ if (!key) throw new HttpError(400, `Could not read multipart name: ${line}`);
89
+ if (key.length > MAX_KEY_LENGTH) throw new HttpError(413, "Field name too long");
90
+ if ([
91
+ "__proto__",
92
+ "constructor",
93
+ "prototype"
94
+ ].includes(key)) throw new HttpError(400, `Illegal key name "${key}"`);
87
95
  continue;
88
96
  }
89
97
  if (line.toLowerCase().startsWith("content-type:")) {
90
- partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1];
91
- if (!partContentType) throw new HttpError(EHttpStatusCode.BadRequest, `Could not read content-type: ${line}`);
98
+ partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1] ?? "";
92
99
  continue;
93
100
  }
94
101
  }
95
102
  }
96
103
  parsePart();
104
+ return result;
97
105
  function parsePart() {
98
- if (key && partContentType.includes("application/json")) result[key] = JSON.parse(result[key]);
106
+ if (key && partContentType.includes("application/json") && typeof result[key] === "string") result[key] = safeJsonParse(result[key]);
99
107
  }
100
- return result;
101
108
  }
102
109
  function urlEncodedParser(v) {
103
110
  return new WooksURLSearchParams(v.trim()).toJson();
@@ -110,16 +117,10 @@ else {
110
117
  isBinary,
111
118
  isFormData,
112
119
  isUrlencoded,
113
- isCompressed,
114
- contentEncodings,
115
120
  parseBody,
116
121
  rawBody
117
122
  };
118
123
  }
119
- function registerBodyCompressor(name, compressor) {
120
- if (compressors[name]) throw new Error(`Body compressor "${name}" already registered.`);
121
- compressors[name] = compressor;
122
- }
123
124
 
124
125
  //#endregion
125
- export { registerBodyCompressor, useBody };
126
+ export { useBody };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/http-body",
3
- "version": "0.5.25",
3
+ "version": "0.6.0",
4
4
  "description": "@wooksjs/http-body",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -38,12 +38,12 @@
38
38
  "bugs": {
39
39
  "url": "https://github.com/wooksjs/wooksjs/issues"
40
40
  },
41
- "peerDependencies": {},
42
41
  "homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/http-body#readme",
43
42
  "devDependencies": {
43
+ "@types/node": "^22.10.6",
44
44
  "typescript": "^5.7.3",
45
45
  "vitest": "^2.1.8",
46
- "@wooksjs/event-http": "^0.5.25"
46
+ "@wooksjs/event-http": "^0.6.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "rolldown -c ../../rolldown.config.mjs"