/src/serenity/Userland/Libraries/LibHTTP/HttpRequest.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2022, the SerenityOS developers. |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <AK/Base64.h> |
9 | | #include <AK/StringBuilder.h> |
10 | | #include <LibHTTP/HttpRequest.h> |
11 | | #include <LibHTTP/Job.h> |
12 | | #include <LibURL/Parser.h> |
13 | | |
14 | | namespace HTTP { |
15 | | |
16 | | StringView to_string_view(HttpRequest::Method method) |
17 | 0 | { |
18 | 0 | switch (method) { |
19 | 0 | case HttpRequest::Method::GET: |
20 | 0 | return "GET"sv; |
21 | 0 | case HttpRequest::Method::HEAD: |
22 | 0 | return "HEAD"sv; |
23 | 0 | case HttpRequest::Method::POST: |
24 | 0 | return "POST"sv; |
25 | 0 | case HttpRequest::Method::DELETE: |
26 | 0 | return "DELETE"sv; |
27 | 0 | case HttpRequest::Method::PATCH: |
28 | 0 | return "PATCH"sv; |
29 | 0 | case HttpRequest::Method::OPTIONS: |
30 | 0 | return "OPTIONS"sv; |
31 | 0 | case HttpRequest::Method::TRACE: |
32 | 0 | return "TRACE"sv; |
33 | 0 | case HttpRequest::Method::CONNECT: |
34 | 0 | return "CONNECT"sv; |
35 | 0 | case HttpRequest::Method::PUT: |
36 | 0 | return "PUT"sv; |
37 | 0 | default: |
38 | 0 | VERIFY_NOT_REACHED(); |
39 | 0 | } |
40 | 0 | } |
41 | | |
42 | | StringView HttpRequest::method_name() const |
43 | 0 | { |
44 | 0 | return to_string_view(m_method); |
45 | 0 | } |
46 | | |
47 | | ErrorOr<ByteBuffer> HttpRequest::to_raw_request() const |
48 | 0 | { |
49 | 0 | StringBuilder builder; |
50 | 0 | TRY(builder.try_append(method_name())); |
51 | 0 | TRY(builder.try_append(' ')); |
52 | 0 | auto path = m_url.serialize_path(); |
53 | 0 | VERIFY(!path.is_empty()); |
54 | 0 | TRY(builder.try_append(path)); |
55 | 0 | if (m_url.query().has_value()) { |
56 | 0 | TRY(builder.try_append('?')); |
57 | 0 | TRY(builder.try_append(*m_url.query())); |
58 | 0 | } |
59 | 0 | TRY(builder.try_append(" HTTP/1.1\r\nHost: "sv)); |
60 | 0 | TRY(builder.try_append(TRY(m_url.serialized_host()))); |
61 | 0 | if (m_url.port().has_value()) |
62 | 0 | TRY(builder.try_appendff(":{}", *m_url.port())); |
63 | 0 | TRY(builder.try_append("\r\n"sv)); |
64 | | // Start headers. |
65 | 0 | bool has_content_length = m_headers.contains("Content-Length"sv); |
66 | 0 | for (auto const& [name, value] : m_headers.headers()) { |
67 | 0 | TRY(builder.try_append(name)); |
68 | 0 | TRY(builder.try_append(": "sv)); |
69 | 0 | TRY(builder.try_append(value)); |
70 | 0 | TRY(builder.try_append("\r\n"sv)); |
71 | 0 | } |
72 | 0 | if (!m_body.is_empty() || method() == Method::POST) { |
73 | | // Add Content-Length header if it's not already present. |
74 | 0 | if (!has_content_length) { |
75 | 0 | TRY(builder.try_appendff("Content-Length: {}\r\n", m_body.size())); |
76 | 0 | } |
77 | | // Finish headers. |
78 | 0 | TRY(builder.try_append("\r\n"sv)); |
79 | 0 | TRY(builder.try_append((char const*)m_body.data(), m_body.size())); |
80 | 0 | } else { |
81 | | // Finish headers. |
82 | 0 | TRY(builder.try_append("\r\n"sv)); |
83 | 0 | } |
84 | 0 | return builder.to_byte_buffer(); |
85 | 0 | } |
86 | | |
87 | | ErrorOr<HttpRequest, HttpRequest::ParseError> HttpRequest::from_raw_request(ReadonlyBytes raw_request) |
88 | 2.46k | { |
89 | 2.46k | enum class State { |
90 | 2.46k | InMethod, |
91 | 2.46k | InResource, |
92 | 2.46k | InProtocol, |
93 | 2.46k | InHeaderName, |
94 | 2.46k | InHeaderValue, |
95 | 2.46k | InBody, |
96 | 2.46k | }; |
97 | | |
98 | 2.46k | State state { State::InMethod }; |
99 | 2.46k | size_t index = 0; |
100 | | |
101 | 71.6M | auto peek = [&](int offset = 0) -> u8 { |
102 | 71.6M | if (index + offset >= raw_request.size()) |
103 | 652 | return 0; |
104 | 71.6M | return raw_request[index + offset]; |
105 | 71.6M | }; |
106 | | |
107 | 59.6M | auto consume = [&]() -> u8 { |
108 | 59.6M | VERIFY(index < raw_request.size()); |
109 | 59.6M | return raw_request[index++]; |
110 | 59.6M | }; |
111 | | |
112 | 2.46k | Vector<u8, 256> buffer; |
113 | | |
114 | 2.46k | Optional<unsigned> content_length; |
115 | 2.46k | ByteString method; |
116 | 2.46k | ByteString resource; |
117 | 2.46k | ByteString protocol; |
118 | 2.46k | HeaderMap headers; |
119 | 2.46k | Header current_header; |
120 | 2.46k | ByteBuffer body; |
121 | | |
122 | 25.2M | auto commit_and_advance_to = [&](auto& output, State new_state) { |
123 | 25.2M | output = ByteString::copy(buffer); |
124 | 25.2M | buffer.clear(); |
125 | 25.2M | state = new_state; |
126 | 25.2M | }; |
127 | | |
128 | 34.4M | while (index < raw_request.size()) { |
129 | | // FIXME: Figure out what the appropriate limitations should be. |
130 | 34.4M | if (buffer.size() > 65536) |
131 | 7 | return ParseError::RequestTooLarge; |
132 | 34.4M | switch (state) { |
133 | 1.66M | case State::InMethod: |
134 | 1.66M | if (peek() == ' ') { |
135 | 2.39k | consume(); |
136 | 2.39k | commit_and_advance_to(method, State::InResource); |
137 | 2.39k | break; |
138 | 2.39k | } |
139 | 1.66M | buffer.append(consume()); |
140 | 1.66M | break; |
141 | 2.72M | case State::InResource: |
142 | 2.72M | if (peek() == ' ') { |
143 | 2.31k | consume(); |
144 | 2.31k | commit_and_advance_to(resource, State::InProtocol); |
145 | 2.31k | break; |
146 | 2.31k | } |
147 | 2.71M | buffer.append(consume()); |
148 | 2.71M | break; |
149 | 632k | case State::InProtocol: |
150 | 632k | if (peek(0) == '\r' && peek(1) == '\n') { |
151 | 2.21k | consume(); |
152 | 2.21k | consume(); |
153 | 2.21k | commit_and_advance_to(protocol, State::InHeaderName); |
154 | 2.21k | break; |
155 | 2.21k | } |
156 | 629k | buffer.append(consume()); |
157 | 629k | break; |
158 | 14.7M | case State::InHeaderName: |
159 | 14.7M | if (peek(0) == ':' && peek(1) == ' ') { |
160 | 12.6M | consume(); |
161 | 12.6M | consume(); |
162 | 12.6M | commit_and_advance_to(current_header.name, State::InHeaderValue); |
163 | 12.6M | break; |
164 | 12.6M | } |
165 | 2.05M | buffer.append(consume()); |
166 | 2.05M | break; |
167 | 13.5M | case State::InHeaderValue: |
168 | 13.5M | if (peek(0) == '\r' && peek(1) == '\n') { |
169 | 12.6M | consume(); |
170 | 12.6M | consume(); |
171 | | |
172 | | // Detect end of headers |
173 | 12.6M | auto next_state = State::InHeaderName; |
174 | 12.6M | if (peek(0) == '\r' && peek(1) == '\n') { |
175 | 1.31k | consume(); |
176 | 1.31k | consume(); |
177 | 1.31k | next_state = State::InBody; |
178 | 1.31k | } |
179 | | |
180 | 12.6M | commit_and_advance_to(current_header.value, next_state); |
181 | | |
182 | 12.6M | if (current_header.name.equals_ignoring_ascii_case("Content-Length"sv)) |
183 | 4.24k | content_length = current_header.value.to_number<unsigned>(); |
184 | | |
185 | 12.6M | headers.set(move(current_header.name), move(current_header.value)); |
186 | 12.6M | break; |
187 | 12.6M | } |
188 | 883k | buffer.append(consume()); |
189 | 883k | break; |
190 | 1.15M | case State::InBody: |
191 | 1.15M | buffer.append(consume()); |
192 | 1.15M | if (index == raw_request.size()) { |
193 | | // End of data, so store the body |
194 | 285 | auto maybe_body = ByteBuffer::copy(buffer); |
195 | 285 | if (maybe_body.is_error()) { |
196 | 0 | VERIFY(maybe_body.error().code() == ENOMEM); |
197 | 0 | return ParseError::OutOfMemory; |
198 | 0 | } |
199 | 285 | body = maybe_body.release_value(); |
200 | 285 | buffer.clear(); |
201 | 285 | } |
202 | 1.15M | break; |
203 | 34.4M | } |
204 | 34.4M | } |
205 | | |
206 | 2.45k | if (state != State::InBody) |
207 | 1.14k | return ParseError::RequestIncomplete; |
208 | | |
209 | 1.30k | if (content_length.has_value() && content_length.value() != body.size()) |
210 | 56 | return ParseError::RequestIncomplete; |
211 | | |
212 | 1.25k | HttpRequest request; |
213 | 1.25k | if (method == "GET") |
214 | 1.03k | request.m_method = Method::GET; |
215 | 214 | else if (method == "HEAD") |
216 | 55 | request.m_method = Method::HEAD; |
217 | 159 | else if (method == "POST") |
218 | 1 | request.m_method = Method::POST; |
219 | 158 | else if (method == "DELETE") |
220 | 0 | request.set_method(HTTP::HttpRequest::Method::DELETE); |
221 | 158 | else if (method == "PATCH") |
222 | 0 | request.set_method(HTTP::HttpRequest::Method::PATCH); |
223 | 158 | else if (method == "OPTIONS") |
224 | 0 | request.set_method(HTTP::HttpRequest::Method::OPTIONS); |
225 | 158 | else if (method == "TRACE") |
226 | 1 | request.set_method(HTTP::HttpRequest::Method::TRACE); |
227 | 157 | else if (method == "CONNECT") |
228 | 15 | request.set_method(HTTP::HttpRequest::Method::CONNECT); |
229 | 142 | else if (method == "PUT") |
230 | 15 | request.set_method(HTTP::HttpRequest::Method::PUT); |
231 | 127 | else |
232 | 127 | return ParseError::UnsupportedMethod; |
233 | | |
234 | 1.12k | request.m_headers = move(headers); |
235 | 1.12k | auto url_parts = resource.split_limit('?', 2, SplitBehavior::KeepEmpty); |
236 | | |
237 | 1.12k | auto url_part_to_string = [](ByteString const& url_part) -> ErrorOr<String, ParseError> { |
238 | 338 | auto query_string_or_error = String::from_byte_string(url_part); |
239 | 338 | if (!query_string_or_error.is_error()) |
240 | 239 | return query_string_or_error.release_value(); |
241 | | |
242 | 99 | if (query_string_or_error.error().code() == ENOMEM) |
243 | 0 | return ParseError::OutOfMemory; |
244 | | |
245 | 99 | return ParseError::InvalidURL; |
246 | 99 | }; |
247 | | |
248 | 1.12k | request.m_url.set_cannot_be_a_base_url(true); |
249 | 1.12k | if (url_parts.size() == 2) { |
250 | 338 | request.m_resource = url_parts[0]; |
251 | 338 | request.m_url.set_paths({ url_parts[0] }); |
252 | 338 | request.m_url.set_query(TRY(url_part_to_string(url_parts[1]))); |
253 | 786 | } else { |
254 | 786 | request.m_resource = resource; |
255 | 786 | request.m_url.set_paths({ resource }); |
256 | 786 | } |
257 | | |
258 | 1.12k | request.set_body(move(body)); |
259 | | |
260 | 1.02k | return request; |
261 | 1.12k | } |
262 | | |
263 | | void HttpRequest::set_headers(HTTP::HeaderMap headers) |
264 | 0 | { |
265 | 0 | m_headers = move(headers); |
266 | 0 | } |
267 | | |
268 | | Optional<Header> HttpRequest::get_http_basic_authentication_header(URL::URL const& url) |
269 | 0 | { |
270 | 0 | if (!url.includes_credentials()) |
271 | 0 | return {}; |
272 | 0 | StringBuilder builder; |
273 | 0 | builder.append(URL::percent_decode(url.username())); |
274 | 0 | builder.append(':'); |
275 | 0 | builder.append(URL::percent_decode(url.password())); |
276 | | |
277 | | // FIXME: change to TRY() and make method fallible |
278 | 0 | auto token = MUST(encode_base64(builder.string_view().bytes())); |
279 | 0 | builder.clear(); |
280 | 0 | builder.append("Basic "sv); |
281 | 0 | builder.append(token); |
282 | 0 | return Header { "Authorization", builder.to_byte_string() }; |
283 | 0 | } |
284 | | |
285 | | Optional<HttpRequest::BasicAuthenticationCredentials> HttpRequest::parse_http_basic_authentication_header(ByteString const& value) |
286 | 0 | { |
287 | 0 | if (!value.starts_with("Basic "sv, AK::CaseSensitivity::CaseInsensitive)) |
288 | 0 | return {}; |
289 | 0 | auto token = value.substring_view(6); |
290 | 0 | if (token.is_empty()) |
291 | 0 | return {}; |
292 | 0 | auto decoded_token_bb = decode_base64(token); |
293 | 0 | if (decoded_token_bb.is_error()) |
294 | 0 | return {}; |
295 | 0 | auto decoded_token = ByteString::copy(decoded_token_bb.value()); |
296 | 0 | auto colon_index = decoded_token.find(':'); |
297 | 0 | if (!colon_index.has_value()) |
298 | 0 | return {}; |
299 | 0 | auto username = decoded_token.substring_view(0, colon_index.value()); |
300 | 0 | auto password = decoded_token.substring_view(colon_index.value() + 1); |
301 | 0 | return BasicAuthenticationCredentials { username, password }; |
302 | 0 | } |
303 | | |
304 | | } |