UNPKG

5.16 kBJavaScriptView Raw
1function read_only_form_data() {
2 /** @type {Map<string, string[]>} */
3 const map = new Map();
4
5 return {
6 /**
7 * @param {string} key
8 * @param {string} value
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
22class ReadOnlyFormData {
23 /** @type {Map<string, string[]>} */
24 #map;
25
26 /** @param {Map<string, string[]>} map */
27 constructor(map) {
28 this.#map = map;
29 }
30
31 /** @param {string} key */
32 get(key) {
33 const value = this.#map.get(key);
34 return value && value[0];
35 }
36
37 /** @param {string} key */
38 getAll(key) {
39 return this.#map.get(key);
40 }
41
42 /** @param {string} key */
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/** @param {import('http').IncomingMessage} req */
81function get_body(req) {
82 const headers = req.headers;
83 const has_body =
84 headers['content-type'] !== undefined &&
85 // https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95
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/** @param {import('http').IncomingMessage} req */
116async function get_json(req) {
117 return JSON.parse(await get_text(req));
118}
119
120/** @param {import('http').IncomingMessage} req */
121async 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 * @param {import('http').IncomingMessage} req
139 * @param {string} boundary
140 */
141async 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 /** @type {Record<string, string>} */
168 const directives = {};
169 raw_directives.forEach((raw_directive) => {
170 const [name, value] = raw_directive.split('=');
171 directives[name] = JSON.parse(value); // TODO is this right?
172 });
173
174 if (name === 'content-disposition') {
175 if (value !== 'form-data') nope();
176
177 if (directives.filename) {
178 // TODO we probably don't want to do this automatically
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 * @param {import('http').IncomingMessage} req
198 * @returns {Promise<string>}
199 */
200function 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 * @param {import('http').IncomingMessage} req
218 * @returns {Promise<ArrayBuffer>}
219 */
220function 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
246export { get_body as g };