1 | function read_only_form_data() {
|
2 |
|
3 | const map = new Map();
|
4 |
|
5 | return {
|
6 | |
7 |
|
8 |
|
9 |
|
10 | append(key, value) {
|
11 | if (map.has(key)) {
|
12 | map.get(key).push(value);
|
13 | } else {
|
14 | map.set(key, [value]);
|
15 | }
|
16 | },
|
17 |
|
18 | data: new ReadOnlyFormData(map)
|
19 | };
|
20 | }
|
21 |
|
22 | class ReadOnlyFormData {
|
23 |
|
24 | #map;
|
25 |
|
26 |
|
27 | constructor(map) {
|
28 | this.#map = map;
|
29 | }
|
30 |
|
31 |
|
32 | get(key) {
|
33 | const value = this.#map.get(key);
|
34 | return value && value[0];
|
35 | }
|
36 |
|
37 |
|
38 | getAll(key) {
|
39 | return this.#map.get(key);
|
40 | }
|
41 |
|
42 |
|
43 | has(key) {
|
44 | return this.#map.has(key);
|
45 | }
|
46 |
|
47 | *[Symbol.iterator]() {
|
48 | for (const [key, value] of this.#map) {
|
49 | for (let i = 0; i < value.length; i += 1) {
|
50 | yield [key, value[i]];
|
51 | }
|
52 | }
|
53 | }
|
54 |
|
55 | *entries() {
|
56 | for (const [key, value] of this.#map) {
|
57 | for (let i = 0; i < value.length; i += 1) {
|
58 | yield [key, value[i]];
|
59 | }
|
60 | }
|
61 | }
|
62 |
|
63 | *keys() {
|
64 | for (const [key, value] of this.#map) {
|
65 | for (let i = 0; i < value.length; i += 1) {
|
66 | yield key;
|
67 | }
|
68 | }
|
69 | }
|
70 |
|
71 | *values() {
|
72 | for (const [, value] of this.#map) {
|
73 | for (let i = 0; i < value.length; i += 1) {
|
74 | yield value;
|
75 | }
|
76 | }
|
77 | }
|
78 | }
|
79 |
|
80 |
|
81 | function get_body(req) {
|
82 | const headers = req.headers;
|
83 | const has_body =
|
84 | headers['content-type'] !== undefined &&
|
85 |
|
86 | (headers['transfer-encoding'] !== undefined || !isNaN(Number(headers['content-length'])));
|
87 |
|
88 | if (!has_body) return Promise.resolve(undefined);
|
89 |
|
90 | const [type, ...directives] = headers['content-type'].split(/;\s*/);
|
91 |
|
92 | switch (type) {
|
93 | case 'application/octet-stream':
|
94 | return get_buffer(req);
|
95 |
|
96 | case 'text/plain':
|
97 | return get_text(req);
|
98 |
|
99 | case 'application/json':
|
100 | return get_json(req);
|
101 |
|
102 | case 'application/x-www-form-urlencoded':
|
103 | return get_urlencoded(req);
|
104 |
|
105 | case 'multipart/form-data': {
|
106 | const boundary = directives.find((directive) => directive.startsWith('boundary='));
|
107 | if (!boundary) throw new Error('Missing boundary');
|
108 | return get_multipart(req, boundary.slice('boundary='.length));
|
109 | }
|
110 | default:
|
111 | throw new Error(`Invalid Content-Type ${type}`);
|
112 | }
|
113 | }
|
114 |
|
115 |
|
116 | async function get_json(req) {
|
117 | return JSON.parse(await get_text(req));
|
118 | }
|
119 |
|
120 |
|
121 | async function get_urlencoded(req) {
|
122 | const text = await get_text(req);
|
123 |
|
124 | const { data, append } = read_only_form_data();
|
125 |
|
126 | text
|
127 | .replace(/\+/g, ' ')
|
128 | .split('&')
|
129 | .forEach((str) => {
|
130 | const [key, value] = str.split('=');
|
131 | append(decodeURIComponent(key), decodeURIComponent(value));
|
132 | });
|
133 |
|
134 | return data;
|
135 | }
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | async function get_multipart(req, boundary) {
|
142 | const text = await get_text(req);
|
143 | const parts = text.split(`--${boundary}`);
|
144 |
|
145 | const nope = () => {
|
146 | throw new Error('Malformed form data');
|
147 | };
|
148 |
|
149 | if (parts[0] !== '' || parts[parts.length - 1].trim() !== '--') {
|
150 | nope();
|
151 | }
|
152 |
|
153 | const { data, append } = read_only_form_data();
|
154 |
|
155 | parts.slice(1, -1).forEach((part) => {
|
156 | const match = /\s*([\s\S]+?)\r\n\r\n([\s\S]*)\s*/.exec(part);
|
157 | const raw_headers = match[1];
|
158 | const body = match[2].trim();
|
159 |
|
160 | let key;
|
161 | raw_headers.split('\r\n').forEach((str) => {
|
162 | const [raw_header, ...raw_directives] = str.split('; ');
|
163 | let [name, value] = raw_header.split(': ');
|
164 |
|
165 | name = name.toLowerCase();
|
166 |
|
167 |
|
168 | const directives = {};
|
169 | raw_directives.forEach((raw_directive) => {
|
170 | const [name, value] = raw_directive.split('=');
|
171 | directives[name] = JSON.parse(value);
|
172 | });
|
173 |
|
174 | if (name === 'content-disposition') {
|
175 | if (value !== 'form-data') nope();
|
176 |
|
177 | if (directives.filename) {
|
178 |
|
179 | throw new Error('File upload is not yet implemented');
|
180 | }
|
181 |
|
182 | if (directives.name) {
|
183 | key = directives.name;
|
184 | }
|
185 | }
|
186 | });
|
187 |
|
188 | if (!key) nope();
|
189 |
|
190 | append(key, body);
|
191 | });
|
192 |
|
193 | return data;
|
194 | }
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | function get_text(req) {
|
201 | return new Promise((fulfil, reject) => {
|
202 | let data = '';
|
203 |
|
204 | req.on('error', reject);
|
205 |
|
206 | req.on('data', (chunk) => {
|
207 | data += chunk;
|
208 | });
|
209 |
|
210 | req.on('end', () => {
|
211 | fulfil(data);
|
212 | });
|
213 | });
|
214 | }
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | function get_buffer(req) {
|
221 | return new Promise((fulfil, reject) => {
|
222 | let data = new Uint8Array(0);
|
223 |
|
224 | req.on('error', reject);
|
225 |
|
226 | req.on('data', (chunk) => {
|
227 | const new_data = new Uint8Array(data.length + chunk.length);
|
228 |
|
229 | for (let i = 0; i < data.length; i += 1) {
|
230 | new_data[i] = data[i];
|
231 | }
|
232 |
|
233 | for (let i = 0; i < chunk.length; i += 1) {
|
234 | new_data[i + data.length] = chunk[i];
|
235 | }
|
236 |
|
237 | data = new_data;
|
238 | });
|
239 |
|
240 | req.on('end', () => {
|
241 | fulfil(data.buffer);
|
242 | });
|
243 | });
|
244 | }
|
245 |
|
246 | export { get_body as g };
|