Line data Source code
1 : #ifndef THINGER_HTTP_COMMON_MULTIPART_PARSER_HPP
2 : #define THINGER_HTTP_COMMON_MULTIPART_PARSER_HPP
3 :
4 : #include <cstddef>
5 : #include <cstdint>
6 : #include <limits>
7 : #include <string>
8 : #include <string_view>
9 : #include <utility>
10 : #include <vector>
11 : #include "http_response.hpp"
12 :
13 : namespace thinger::http {
14 :
15 : // Limits of a multipart body, against bodies that are small for the maximum body size
16 : // but expensive to process: many parts, large header sections, fields kept in memory.
17 : // Content sizes count the bytes of the parts, without delimiters nor headers; the whole
18 : // body is bounded by the maximum body size of the route.
19 : struct multipart_limits {
20 : // Parts in the body, fields and files (Go: 1000)
21 : size_t max_parts = 1000;
22 : // Parts with a filename (busboy and Go: unlimited)
23 : size_t max_files = std::numeric_limits<size_t>::max();
24 : // Header section of each part, line endings included (busboy: 16 KB)
25 : std::uint64_t max_header_size = 16 * 1024;
26 : // Filename of a part, in bytes, as sent (decoded from filename* if present): no
27 : // longer than a file name can be in most file systems
28 : std::uint64_t max_filename_size = 255;
29 : // Content of each part without filename (busboy: 1 MB)
30 : std::uint64_t max_field_size = 1024 * 1024;
31 : // Content of all the parts without filename, which are usually kept in memory
32 : std::uint64_t max_fields_size = 8 * 1024 * 1024;
33 : // Content of each part with a filename (busboy and Go: unlimited)
34 : std::uint64_t max_file_size = std::numeric_limits<std::uint64_t>::max();
35 : };
36 :
37 : // Why a multipart body failed
38 : enum class multipart_error {
39 : none,
40 : not_multipart, // 415: the Content-Type is not multipart (or the body has a Content-Encoding)
41 : invalid_boundary, // 400: the boundary parameter is missing or invalid
42 : malformed, // 400: the body does not follow the multipart syntax
43 : truncated, // 400: the body ended before its closing delimiter
44 : invalid_body, // 400: the request body could not be read (framing, connection closed)
45 : too_many_parts, // 413
46 : too_many_files, // 413
47 : header_too_large, // 413: the header section of a part
48 : filename_too_large, // 413
49 : field_too_large, // 413: a part without filename
50 : fields_too_large, // 413: all the parts without filename
51 : file_too_large, // 413: a part with a filename
52 : body_too_large, // 413: over the maximum body size of the route
53 : file_failed // 500: a temporary file could not be written
54 : };
55 :
56 : // Status answering an error (400, 413, 415 or 500), and a message for the client
57 : http_response::status multipart_error_status(multipart_error error);
58 : const char* multipart_error_message(multipart_error error);
59 :
60 : // A part of a multipart body: its header section, and what its Content-Disposition and
61 : // Content-Type say (RFC 7578)
62 : struct multipart_part {
63 : // Header fields as received (names keep their case; values without surrounding whitespace)
64 : std::vector<std::pair<std::string, std::string>> headers;
65 :
66 : // Disposition type, lowercase ("form-data"); empty without Content-Disposition
67 : std::string disposition;
68 : // Field name (the name parameter), empty if none
69 : std::string name;
70 : // Filename as the client sent it, from filename* (RFC 5987, if its charset is UTF-8,
71 : // ISO-8859-1 or US-ASCII) or else filename. It may be a path ("../../etc/passwd",
72 : // "C:\\Users\\me\\photo.jpg") or have control characters: never use it as a path,
73 : // see safe_filename()
74 : std::string filename;
75 : // Whether the part has a filename parameter (even an empty one, as browsers send for
76 : // a file input without a file): a file, not a field
77 : bool has_filename = false;
78 : // Content-Type of the part, "text/plain" if it has none (RFC 7578, 4.4)
79 : std::string content_type = "text/plain";
80 :
81 198 : bool is_file() const { return has_filename; }
82 :
83 : // The value of a header (the first one with that name, case-insensitive), empty if missing
84 : std::string header(std::string_view name) const;
85 :
86 : // The filename reduced to a name to store it with: what follows its last '/' or
87 : // '\', without control characters; empty for "." and ".." (and an empty filename).
88 : // Still, it can collide with existing files, start with a dot, or be long.
89 27 : std::string safe_filename() const { return sanitize_filename(filename); }
90 :
91 : static std::string sanitize_filename(std::string_view filename);
92 : };
93 :
94 : /**
95 : * Incremental parser of a multipart body (RFC 2046, section 5.1; RFC 7578 for
96 : * multipart/form-data). It is fed the body in pieces of any size and returns the events
97 : * they make, without keeping more than the header section of a part and a partial
98 : * delimiter: the content of the parts is handed over as it is found, in pieces pointing
99 : * into the input (or into the parser, for bytes held back as a possible delimiter).
100 : *
101 : * The result does not depend on how the body is split. It is strict as Go's
102 : * mime/multipart: CRLF line endings only, transport padding (spaces and tabs) after
103 : * delimiters, a closing delimiter required, preamble and epilogue ignored, no header
104 : * folding. A part can start with its delimiter right after its header section (an empty
105 : * part), as Go reads it. The boundary inside the content of a part is not a delimiter
106 : * unless it starts a line and is followed by "--", whitespace or CRLF.
107 : */
108 : class multipart_parser {
109 : public:
110 : enum class event {
111 : need_more, // all the input was consumed: feed more, or finish()
112 : part, // a part starts: its header section is in part()
113 : data, // a piece of the content of the current part, in data()
114 : part_end, // the current part ended
115 : end, // the closing delimiter was read (the rest of the body is the epilogue)
116 : error // the body is invalid, or over a limit: see error()
117 : };
118 :
119 : // RFC 2046: 1 to 70 characters, digits, letters and '()+_,-./:=? (and spaces, but not last)
120 : static bool valid_boundary(std::string_view boundary);
121 :
122 : // A parser for a body with this boundary (as the boundary parameter of the
123 : // Content-Type gives it). With an invalid boundary, it fails at once (invalid_boundary).
124 : explicit multipart_parser(std::string_view boundary, multipart_limits limits = {});
125 :
126 : /// Parse `input` up to the next event, removing what it consumed from its front.
127 : /// After `end` (and `error`) the same event is returned again, and the rest of the
128 : /// input (the epilogue) is consumed.
129 : event parse(std::string_view& input);
130 :
131 : /// The body ended: `end` if its closing delimiter was read, else `error` (truncated)
132 : event finish();
133 :
134 : /// Content of a `data` event, valid until the next call
135 345305 : std::string_view data() const { return data_; }
136 :
137 : /// The current part, from its `part` event to the next one
138 134859 : const multipart_part& part() const { return part_; }
139 :
140 29034 : multipart_error error() const { return error_; }
141 : bool done() const { return state_ == state::done; }
142 108 : bool failed() const { return state_ == state::error; }
143 :
144 : /// Parts started, and bytes of content of the current part
145 : size_t parts() const { return parts_; }
146 : std::uint64_t part_size() const { return part_size_; }
147 :
148 : private:
149 : enum class state {
150 : preamble, // before the first delimiter
151 : delimiter_line, // transport padding after a delimiter, then CRLF
152 : delimiter_lf,
153 : headers, // header section of a part
154 : content, // content of a part
155 : close_line, // transport padding after the closing delimiter, then CRLF or the end
156 : close_lf,
157 : done, // the epilogue
158 : error
159 : };
160 :
161 : enum class match { mismatch, partial, delimiter, close };
162 :
163 : multipart_limits limits_;
164 : // CRLF "--" boundary "--": the delimiter, and the two dashes of the closing one
165 : std::string delimiter_;
166 : size_t boundary_end_ = 0; // length of CRLF "--" boundary
167 : state state_ = state::preamble;
168 : multipart_error error_ = multipart_error::none;
169 :
170 : // Bytes of delimiter_ matched by the input already consumed (held back), from held_from_:
171 : // the parts and the body start as if after a CRLF, which is not content
172 : size_t matched_ = 0;
173 : size_t held_from_ = 0;
174 :
175 : // Header section of the next part: the line being read, its fields, and its size
176 : std::string line_;
177 : std::vector<std::pair<std::string, std::string>> headers_;
178 : std::uint64_t header_size_ = 0;
179 :
180 : multipart_part part_;
181 : std::string_view data_;
182 : size_t parts_ = 0;
183 : size_t files_ = 0;
184 : std::uint64_t part_size_ = 0;
185 : std::uint64_t fields_size_ = 0;
186 :
187 : event fail(multipart_error error);
188 : void start_matching();
189 : match match_delimiter(size_t& matched, std::string_view input, size_t& used) const;
190 : event scan(std::string_view& input);
191 : event on_delimiter(match found);
192 : event header_line(std::string_view line);
193 : event header_section_end();
194 : event content(std::string_view data);
195 : };
196 :
197 : }
198 :
199 : #endif
|