Line data Source code
1 : #include "multipart_parser.hpp"
2 : #include "../util/utf8.hpp"
3 : #include "../../util/ascii.hpp"
4 : #include <optional>
5 :
6 : namespace thinger::http {
7 :
8 : namespace {
9 :
10 5561778 : bool is_tchar(char c) {
11 5561778 : auto u = static_cast<unsigned char>(c);
12 5561778 : if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z')) return true;
13 686544 : return std::string_view("!#$%&'*+-.^_`|~").find(c) != std::string_view::npos;
14 : }
15 :
16 2770323 : bool is_ows(char c) {
17 2770323 : return c == ' ' || c == '\t';
18 : }
19 :
20 : // RFC 9110, 5.5: a field value has no control characters but HTAB (obs-text is allowed:
21 : // browsers send UTF-8 filenames as they are)
22 4053525 : bool is_field_char(char c) {
23 4053525 : auto u = static_cast<unsigned char>(c);
24 4053525 : return u == '\t' || (u >= 0x20 && u != 0x7F);
25 : }
26 :
27 1060737 : std::string_view trim(std::string_view value) {
28 1412580 : while (!value.empty() && is_ows(value.front())) value.remove_prefix(1);
29 1062987 : while (!value.empty() && is_ows(value.back())) value.remove_suffix(1);
30 1060737 : return value;
31 : }
32 :
33 346404 : std::string_view take_token(std::string_view& input) {
34 346404 : size_t size = 0;
35 2759559 : while (size < input.size() && is_tchar(input[size])) ++size;
36 346404 : auto token = input.substr(0, size);
37 346404 : input.remove_prefix(size);
38 346404 : return token;
39 : }
40 :
41 : // Characters a backslash escapes in a quoted string (RFC 2045 tspecials): elsewhere a
42 : // backslash is kept, as old browsers send Windows paths unescaped ("C:\dev\a.txt"), the
43 : // way Go's mime.ParseMediaType reads them
44 3480 : bool is_tspecial(char c) {
45 3480 : return std::string_view("()<>@,;:\\\"/[]?=").find(c) != std::string_view::npos;
46 : }
47 :
48 : // A token or a quoted string; nullopt if there is neither
49 192360 : std::optional<std::string> take_value(std::string_view& input) {
50 192360 : if (input.empty() || input.front() != '"') {
51 13866 : auto token = take_token(input);
52 13866 : if (token.empty()) return std::nullopt;
53 26808 : return std::string(token);
54 : }
55 178494 : std::string value;
56 675669 : for (size_t i = 1; i < input.size(); ++i) {
57 675195 : char c = input[i];
58 675195 : if (c == '"') {
59 178020 : input.remove_prefix(i + 1);
60 178020 : return value;
61 : }
62 497175 : if (c == '\\' && i + 1 < input.size() && is_tspecial(input[i + 1])) c = input[++i];
63 497175 : value += c;
64 : }
65 474 : return std::nullopt; // unterminated
66 178494 : }
67 :
68 22812 : int hex_value(char c) {
69 22812 : if (c >= '0' && c <= '9') return c - '0';
70 10758 : if (c >= 'a' && c <= 'f') return c - 'a' + 10;
71 10758 : if (c >= 'A' && c <= 'F') return c - 'A' + 10;
72 1350 : return -1;
73 : }
74 :
75 : // RFC 5987 ext-value (charset'language'percent-encoded) in UTF-8; nullopt if it is
76 : // malformed or its charset is not UTF-8, ISO-8859-1 or US-ASCII
77 12360 : std::optional<std::string> decode_ext_value(std::string_view value) {
78 12360 : auto first = value.find('\'');
79 12360 : if (first == std::string_view::npos) return std::nullopt;
80 11022 : auto second = value.find('\'', first + 1);
81 11022 : if (second == std::string_view::npos) return std::nullopt;
82 9684 : auto charset = value.substr(0, first);
83 9684 : auto encoded = value.substr(second + 1);
84 :
85 9684 : std::string bytes;
86 43680 : for (size_t i = 0; i < encoded.size(); ++i) {
87 36684 : if (encoded[i] != '%') {
88 23940 : bytes += encoded[i];
89 23940 : continue;
90 : }
91 12744 : if (i + 2 >= encoded.size()) return std::nullopt;
92 11406 : int high = hex_value(encoded[i + 1]);
93 11406 : int low = hex_value(encoded[i + 2]);
94 11406 : if (high < 0 || low < 0) return std::nullopt;
95 10056 : bytes += static_cast<char>(high * 16 + low);
96 10056 : i += 2;
97 : }
98 :
99 : using ::thinger::util::ascii::iequals;
100 6996 : if (iequals(charset, "utf-8")) {
101 2868 : if (!utf8::is_valid(reinterpret_cast<const uint8_t*>(bytes.data()), bytes.size())) return std::nullopt;
102 1518 : return bytes;
103 : }
104 4128 : if (iequals(charset, "iso-8859-1")) {
105 702 : std::string result;
106 5616 : for (char c : bytes) {
107 4914 : auto u = static_cast<unsigned char>(c);
108 4914 : if (u < 0x80) {
109 3510 : result += c;
110 : } else {
111 1404 : result += static_cast<char>(0xC0 | (u >> 6));
112 1404 : result += static_cast<char>(0x80 | (u & 0x3F));
113 : }
114 : }
115 702 : return result;
116 702 : }
117 3426 : if (iequals(charset, "us-ascii")) {
118 8166 : for (char c : bytes) {
119 7488 : if (static_cast<unsigned char>(c) >= 0x80) return std::nullopt;
120 : }
121 678 : return bytes;
122 : }
123 1362 : return std::nullopt;
124 9684 : }
125 :
126 : // Content-Disposition (RFC 6266, RFC 7578 4.2): a disposition type and parameters, with
127 : // whitespace around ';' and '=' and a trailing ';', as Go reads it. A repeated parameter
128 : // is ambiguous: malformed.
129 139278 : bool parse_content_disposition(std::string_view value, multipart_part& part) {
130 139278 : auto type = take_token(value);
131 139278 : if (type.empty()) return false;
132 138864 : part.disposition = std::string(type);
133 138864 : ::thinger::util::ascii::to_lower(part.disposition);
134 :
135 138864 : std::vector<std::string> seen;
136 138864 : std::optional<std::string> filename;
137 138864 : std::optional<std::string> extended_filename;
138 : for (;;) {
139 329160 : value = trim(value);
140 329160 : if (value.empty()) break;
141 198408 : if (value.front() != ';') return false;
142 194010 : value.remove_prefix(1);
143 194010 : value = trim(value);
144 194010 : if (value.empty()) break; // trailing ';'
145 :
146 193260 : std::string name(take_token(value));
147 193260 : if (name.empty()) return false;
148 192816 : ::thinger::util::ascii::to_lower(name);
149 192816 : value = trim(value);
150 192816 : if (value.empty() || value.front() != '=') return false;
151 192360 : value.remove_prefix(1);
152 192360 : value = trim(value);
153 192360 : auto parameter = take_value(value);
154 192360 : if (!parameter) return false;
155 :
156 251808 : for (const auto& other : seen) {
157 61512 : if (other == name) return false;
158 : }
159 190296 : seen.push_back(name);
160 :
161 190296 : if (name == "name") {
162 134829 : part.name = std::move(*parameter);
163 55467 : } else if (name == "filename") {
164 43107 : filename = std::move(*parameter);
165 12360 : } else if (name == "filename*") {
166 : // An unsupported or malformed one is ignored, falling back to filename
167 12360 : extended_filename = decode_ext_value(*parameter);
168 : }
169 385620 : }
170 :
171 : // filename* first (RFC 6266, 4.3), as busboy does
172 134466 : if (extended_filename) {
173 2898 : filename = std::move(extended_filename);
174 : }
175 134466 : if (filename) {
176 44547 : part.filename = std::move(*filename);
177 44547 : part.has_filename = true;
178 : }
179 134466 : return true;
180 138864 : }
181 :
182 : }
183 :
184 99 : http_response::status multipart_error_status(multipart_error error) {
185 : using status = http_response::status;
186 99 : switch (error) {
187 0 : case multipart_error::none: return status::ok;
188 18 : case multipart_error::not_multipart: return status::unsupported_media_type;
189 48 : case multipart_error::invalid_boundary:
190 : case multipart_error::malformed:
191 : case multipart_error::truncated:
192 48 : case multipart_error::invalid_body: return status::bad_request;
193 30 : case multipart_error::too_many_parts:
194 : case multipart_error::too_many_files:
195 : case multipart_error::header_too_large:
196 : case multipart_error::filename_too_large:
197 : case multipart_error::field_too_large:
198 : case multipart_error::fields_too_large:
199 : case multipart_error::file_too_large:
200 30 : case multipart_error::body_too_large: return status::payload_too_large;
201 3 : case multipart_error::file_failed: return status::internal_server_error;
202 : }
203 0 : return status::bad_request;
204 : }
205 :
206 78 : const char* multipart_error_message(multipart_error error) {
207 78 : switch (error) {
208 0 : case multipart_error::none: return "";
209 15 : case multipart_error::not_multipart: return "Unsupported Media Type: expected multipart/form-data";
210 15 : case multipart_error::invalid_boundary: return "Invalid multipart boundary";
211 12 : case multipart_error::malformed: return "Malformed multipart body";
212 12 : case multipart_error::truncated: return "Incomplete multipart body";
213 3 : case multipart_error::invalid_body: return "Invalid request body";
214 6 : case multipart_error::too_many_parts: return "Too many parts";
215 0 : case multipart_error::too_many_files: return "Too many files";
216 0 : case multipart_error::header_too_large: return "Part header too large";
217 0 : case multipart_error::filename_too_large: return "Filename too long";
218 3 : case multipart_error::field_too_large: return "Field too large";
219 0 : case multipart_error::fields_too_large: return "Fields too large";
220 6 : case multipart_error::file_too_large: return "File too large";
221 6 : case multipart_error::body_too_large: return "Payload Too Large";
222 0 : case multipart_error::file_failed: return "Could not store the file";
223 : }
224 0 : return "Invalid multipart body";
225 : }
226 :
227 6 : std::string multipart_part::header(std::string_view name) const {
228 12 : for (const auto& [key, value] : headers) {
229 12 : if (::thinger::util::ascii::iequals(key, name)) return value;
230 : }
231 0 : return {};
232 : }
233 :
234 81 : std::string multipart_part::sanitize_filename(std::string_view filename) {
235 81 : auto separator = filename.find_last_of("/\\");
236 81 : if (separator != std::string_view::npos) filename.remove_prefix(separator + 1);
237 81 : std::string result;
238 585 : for (char c : filename) {
239 504 : auto u = static_cast<unsigned char>(c);
240 504 : if (u < 0x20 || u == 0x7F) continue;
241 483 : result += c;
242 : }
243 81 : if (result == "." || result == "..") result.clear();
244 81 : return result;
245 0 : }
246 :
247 93498 : bool multipart_parser::valid_boundary(std::string_view boundary) {
248 93498 : if (boundary.empty() || boundary.size() > 70 || boundary.back() == ' ') return false;
249 928323 : for (char c : boundary) {
250 834876 : auto u = static_cast<unsigned char>(c);
251 905385 : bool valid = (u >= '0' && u <= '9') || (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') ||
252 70509 : std::string_view("'()+_,-./:=? ").find(c) != std::string_view::npos;
253 834876 : if (!valid) return false;
254 : }
255 93447 : return true;
256 : }
257 :
258 93429 : multipart_parser::multipart_parser(std::string_view boundary, multipart_limits limits)
259 93429 : : limits_(limits) {
260 93429 : if (!valid_boundary(boundary)) {
261 12 : fail(multipart_error::invalid_boundary);
262 12 : return;
263 : }
264 93417 : delimiter_ = "\r\n--";
265 93417 : delimiter_ += boundary;
266 93417 : boundary_end_ = delimiter_.size();
267 93417 : delimiter_ += "--";
268 : // The body may start with its first delimiter, without the CRLF before it
269 93417 : start_matching();
270 0 : }
271 :
272 29034 : multipart_parser::event multipart_parser::fail(multipart_error error) {
273 29034 : if (state_ != state::error) {
274 29034 : state_ = state::error;
275 29034 : error_ = error;
276 : }
277 29034 : data_ = {};
278 29034 : return event::error;
279 : }
280 :
281 228246 : void multipart_parser::start_matching() {
282 : // As if the CRLF of a delimiter were already matched: it is not in the input
283 228246 : matched_ = 2;
284 228246 : held_from_ = 2;
285 228246 : }
286 :
287 7180227 : multipart_parser::match multipart_parser::match_delimiter(size_t& matched, std::string_view input, size_t& used) const {
288 7180227 : size_t i = 0;
289 19817187 : while (matched < boundary_end_) {
290 19463214 : if (i == input.size()) {
291 82143 : used = i;
292 82143 : return match::partial;
293 : }
294 19381071 : if (input[i] != delimiter_[matched]) return match::mismatch;
295 12636960 : ++matched;
296 12636960 : ++i;
297 : }
298 353973 : if (i == input.size()) {
299 6885 : used = i;
300 6885 : return match::partial;
301 : }
302 347088 : if (matched == boundary_end_) {
303 346074 : char c = input[i];
304 : // Transport padding or CRLF: a delimiter (the line is checked after it)
305 346074 : if (is_ows(c) || c == '\r') {
306 202209 : used = i;
307 202209 : return match::delimiter;
308 : }
309 143865 : if (c != '-') return match::mismatch;
310 137571 : ++matched;
311 137571 : if (++i == input.size()) {
312 1512 : used = i;
313 1512 : return match::partial;
314 : }
315 : }
316 : // After the boundary and a dash: another one closes the body
317 137073 : if (input[i] != '-') return match::mismatch;
318 131673 : ++matched;
319 131673 : used = i + 1;
320 131673 : return match::close;
321 : }
322 :
323 337070 : multipart_parser::event multipart_parser::content(std::string_view data) {
324 337070 : if (data.empty() || state_ != state::content) return event::need_more; // (the preamble is dropped)
325 :
326 : // Over a limit: the one crossed first, as if the content came byte by byte
327 196760 : std::uint64_t limit = part_.has_filename ? limits_.max_file_size : limits_.max_field_size;
328 196760 : std::uint64_t allowed = limit - part_size_;
329 196760 : std::uint64_t fields_allowed = part_.has_filename ? allowed : limits_.max_fields_size - fields_size_;
330 196760 : if (data.size() > allowed || data.size() > fields_allowed) {
331 11721 : if (allowed <= fields_allowed) {
332 9999 : return fail(part_.has_filename ? multipart_error::file_too_large : multipart_error::field_too_large);
333 : }
334 1722 : return fail(multipart_error::fields_too_large);
335 : }
336 185039 : part_size_ += data.size();
337 185039 : if (!part_.has_filename) fields_size_ += data.size();
338 185039 : data_ = data;
339 185039 : return event::data;
340 : }
341 :
342 215877 : multipart_parser::event multipart_parser::on_delimiter(match found) {
343 215877 : bool in_part = state_ == state::content;
344 215877 : state_ = found == match::close ? state::close_line : state::delimiter_line;
345 215877 : return in_part ? event::part_end : event::need_more;
346 : }
347 :
348 560089 : multipart_parser::event multipart_parser::scan(std::string_view& input) {
349 : for (;;) {
350 : // A delimiter matched in part by the input consumed before: go on with it
351 698563 : if (matched_ > 0) {
352 306996 : size_t matched = matched_;
353 306996 : size_t used = 0;
354 306996 : auto found = match_delimiter(matched, input, used);
355 306996 : if (found == match::partial) {
356 62607 : matched_ = matched;
357 62607 : input.remove_prefix(used);
358 170379 : return event::need_more;
359 : }
360 244389 : if (found == match::mismatch) {
361 : // Not a delimiter: the bytes held back are content, and the input is
362 : // scanned from its start (it has no CR within those bytes)
363 138822 : auto held = std::string_view(delimiter_).substr(held_from_, matched_ - held_from_);
364 138822 : matched_ = 0;
365 138822 : auto result = content(held);
366 138822 : if (result != event::need_more) return result;
367 136617 : continue;
368 136617 : }
369 105567 : matched_ = 0;
370 105567 : input.remove_prefix(used);
371 105567 : return on_delimiter(found);
372 : }
373 :
374 391567 : if (input.empty()) return event::need_more;
375 :
376 : // Content up to the first CR that starts a delimiter, or a part of one at the end of
377 : // the input. A delimiter has no CR but its first byte, so a mismatch at a CR moves on
378 : // to the next one.
379 324881 : size_t position = 0;
380 : for (;;) {
381 6941864 : auto cr = input.find('\r', position);
382 6941864 : if (cr == std::string_view::npos) {
383 68633 : auto data = input;
384 68633 : input = {};
385 68633 : return content(data);
386 : }
387 6873231 : size_t matched = 0;
388 6873231 : size_t used = 0;
389 6873231 : auto found = match_delimiter(matched, input.substr(cr), used);
390 6873231 : if (found == match::mismatch) {
391 6616983 : position = cr + 1;
392 6616983 : continue;
393 : }
394 256248 : if (cr > 0) {
395 129615 : auto data = input.substr(0, cr);
396 129615 : input.remove_prefix(cr);
397 129615 : auto result = content(data);
398 129615 : if (result != event::need_more) return result;
399 1857 : break;
400 : }
401 126633 : input.remove_prefix(used);
402 126633 : if (found == match::partial) {
403 16323 : matched_ = matched;
404 16323 : held_from_ = 0;
405 16323 : return event::need_more;
406 : }
407 110310 : return on_delimiter(found);
408 6616983 : }
409 138474 : }
410 : }
411 :
412 155910 : multipart_parser::event multipart_parser::header_line(std::string_view line) {
413 : // A field line: "name: value" CRLF, with a token as its name and no folding
414 155910 : if (line.size() < 2 || line[line.size() - 2] != '\r') return fail(multipart_error::malformed);
415 155034 : line.remove_suffix(2);
416 155034 : auto colon = line.find(':');
417 155034 : if (colon == std::string_view::npos || colon == 0) return fail(multipart_error::malformed);
418 153357 : auto name = line.substr(0, colon);
419 2967822 : for (char c : name) {
420 2815431 : if (!is_tchar(c)) return fail(multipart_error::malformed);
421 : }
422 152391 : auto value = trim(line.substr(colon + 1));
423 4204818 : for (char c : value) {
424 4053525 : if (!is_field_char(c)) return fail(multipart_error::malformed);
425 : }
426 605172 : headers_.emplace_back(std::string(name), std::string(value));
427 151293 : return event::need_more;
428 : }
429 :
430 142365 : multipart_parser::event multipart_parser::header_section_end() {
431 142365 : multipart_part part;
432 142365 : part.headers = std::move(headers_);
433 142365 : headers_.clear();
434 :
435 142365 : bool disposition = false;
436 142365 : bool content_type = false;
437 285825 : for (const auto& [name, value] : part.headers) {
438 149418 : if (::thinger::util::ascii::iequals(name, "content-disposition")) {
439 140424 : if (disposition || !parse_content_disposition(value, part)) return fail(multipart_error::malformed);
440 134466 : disposition = true;
441 9528 : } else if (::thinger::util::ascii::iequals(name, "content-type")) {
442 8778 : if (content_type) return fail(multipart_error::malformed);
443 8244 : content_type = true;
444 8244 : part.content_type = value;
445 : }
446 : }
447 :
448 136407 : if (part.has_filename) {
449 44547 : if (part.filename.size() > limits_.max_filename_size) return fail(multipart_error::filename_too_large);
450 43983 : if (++files_ > limits_.max_files) return fail(multipart_error::too_many_files);
451 : }
452 :
453 134829 : part_ = std::move(part);
454 134829 : part_size_ = 0;
455 134829 : state_ = state::content;
456 : // The content may start with the delimiter (an empty part): the CRLF before it ended
457 : // the header section
458 134829 : start_matching();
459 134829 : return event::part;
460 142365 : }
461 :
462 952048 : multipart_parser::event multipart_parser::parse(std::string_view& input) {
463 952048 : data_ = {};
464 : for (;;) {
465 1542838 : event result = event::need_more;
466 1542838 : switch (state_) {
467 560089 : case state::preamble:
468 : case state::content:
469 560089 : result = scan(input);
470 560089 : break;
471 :
472 216771 : case state::delimiter_line:
473 : case state::close_line:
474 : // Transport padding, then CRLF
475 221871 : while (!input.empty() && is_ows(input.front())) input.remove_prefix(1);
476 216771 : if (input.empty()) break;
477 214350 : if (input.front() != '\r') return fail(multipart_error::malformed);
478 213456 : input.remove_prefix(1);
479 213456 : state_ = state_ == state::delimiter_line ? state::delimiter_lf : state::close_lf;
480 213456 : break;
481 :
482 150399 : case state::delimiter_lf:
483 150399 : if (input.empty()) break;
484 150399 : if (input.front() != '\n') return fail(multipart_error::malformed);
485 150195 : input.remove_prefix(1);
486 150195 : if (++parts_ > limits_.max_parts) return fail(multipart_error::too_many_parts);
487 148572 : state_ = state::headers;
488 148572 : header_size_ = 0;
489 148572 : line_.clear();
490 148572 : break;
491 :
492 523503 : case state::headers: {
493 523503 : if (input.empty()) break;
494 : // Up to the end of the line, counted against the limit before it is checked,
495 : // so the error is the same however the section is split
496 523503 : auto lf = input.find('\n');
497 523503 : size_t size = lf == std::string_view::npos ? input.size() : lf + 1;
498 523503 : if (size > limits_.max_header_size - header_size_) return fail(multipart_error::header_too_large);
499 522231 : header_size_ += size;
500 522231 : line_.append(input.data(), size);
501 522231 : input.remove_prefix(size);
502 522231 : if (lf == std::string_view::npos) break;
503 298275 : if (line_ == "\r\n") {
504 142365 : line_.clear();
505 142365 : return header_section_end();
506 : }
507 155910 : if (header_line(line_) == event::error) return event::error;
508 151293 : line_.clear();
509 151293 : break;
510 : }
511 :
512 63048 : case state::close_lf:
513 63048 : if (input.empty()) break;
514 63048 : if (input.front() != '\n') return fail(multipart_error::malformed);
515 62856 : input.remove_prefix(1);
516 62856 : state_ = state::done;
517 62856 : return event::end;
518 :
519 39 : case state::done:
520 : // The epilogue
521 39 : input = {};
522 39 : return event::end;
523 :
524 28989 : case state::error:
525 28989 : return event::error;
526 : }
527 1299787 : if (result != event::need_more) return result;
528 980072 : if (input.empty() && state_ != state::done) return event::need_more;
529 590790 : }
530 : }
531 :
532 31473 : multipart_parser::event multipart_parser::finish() {
533 31473 : data_ = {};
534 31473 : switch (state_) {
535 0 : case state::done:
536 0 : return event::end;
537 1524 : case state::close_line:
538 : // The closing delimiter may end the body, without CRLF
539 1524 : state_ = state::done;
540 1524 : return event::end;
541 28986 : case state::error:
542 28986 : return event::error;
543 963 : default:
544 963 : return fail(multipart_error::truncated);
545 : }
546 : }
547 :
548 : }
|