Serenity Operating System
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
13namespace HTTP {
14
15DeprecatedString to_deprecated_string(HttpRequest::Method method)
16{
17 switch (method) {
18 case HttpRequest::Method::GET:
19 return "GET";
20 case HttpRequest::Method::HEAD:
21 return "HEAD";
22 case HttpRequest::Method::POST:
23 return "POST";
24 case HttpRequest::Method::DELETE:
25 return "DELETE";
26 case HttpRequest::Method::PATCH:
27 return "PATCH";
28 case HttpRequest::Method::OPTIONS:
29 return "OPTIONS";
30 case HttpRequest::Method::TRACE:
31 return "TRACE";
32 case HttpRequest::Method::CONNECT:
33 return "CONNECT";
34 case HttpRequest::Method::PUT:
35 return "PUT";
36 default:
37 VERIFY_NOT_REACHED();
38 }
39}
40
41DeprecatedString HttpRequest::method_name() const
42{
43 return to_deprecated_string(m_method);
44}
45
46ErrorOr<ByteBuffer> HttpRequest::to_raw_request() const
47{
48 StringBuilder builder;
49 TRY(builder.try_append(method_name()));
50 TRY(builder.try_append(' '));
51 // NOTE: The percent_encode is so that e.g. spaces are properly encoded.
52 auto path = m_url.path();
53 VERIFY(!path.is_empty());
54 TRY(builder.try_append(URL::percent_encode(m_url.path(), URL::PercentEncodeSet::EncodeURI)));
55 if (!m_url.query().is_empty()) {
56 TRY(builder.try_append('?'));
57 TRY(builder.try_append(m_url.query()));
58 }
59 TRY(builder.try_append(" HTTP/1.1\r\nHost: "sv));
60 TRY(builder.try_append(m_url.host()));
61 if (m_url.port().has_value())
62 TRY(builder.try_appendff(":{}", *m_url.port()));
63 TRY(builder.try_append("\r\n"sv));
64 for (auto& header : m_headers) {
65 TRY(builder.try_append(header.name));
66 TRY(builder.try_append(": "sv));
67 TRY(builder.try_append(header.value));
68 TRY(builder.try_append("\r\n"sv));
69 }
70 if (!m_body.is_empty() || method() == Method::POST) {
71 TRY(builder.try_appendff("Content-Length: {}\r\n\r\n", m_body.size()));
72 TRY(builder.try_append((char const*)m_body.data(), m_body.size()));
73 }
74 TRY(builder.try_append("\r\n"sv));
75 return builder.to_byte_buffer();
76}
77
78Optional<HttpRequest> HttpRequest::from_raw_request(ReadonlyBytes raw_request)
79{
80 enum class State {
81 InMethod,
82 InResource,
83 InProtocol,
84 InHeaderName,
85 InHeaderValue,
86 InBody,
87 };
88
89 State state { State::InMethod };
90 size_t index = 0;
91
92 auto peek = [&](int offset = 0) -> u8 {
93 if (index + offset >= raw_request.size())
94 return 0;
95 return raw_request[index + offset];
96 };
97
98 auto consume = [&]() -> u8 {
99 VERIFY(index < raw_request.size());
100 return raw_request[index++];
101 };
102
103 Vector<u8, 256> buffer;
104
105 DeprecatedString method;
106 DeprecatedString resource;
107 DeprecatedString protocol;
108 Vector<Header> headers;
109 Header current_header;
110 ByteBuffer body;
111
112 auto commit_and_advance_to = [&](auto& output, State new_state) {
113 output = DeprecatedString::copy(buffer);
114 buffer.clear();
115 state = new_state;
116 };
117
118 while (index < raw_request.size()) {
119 // FIXME: Figure out what the appropriate limitations should be.
120 if (buffer.size() > 65536)
121 return {};
122 switch (state) {
123 case State::InMethod:
124 if (peek() == ' ') {
125 consume();
126 commit_and_advance_to(method, State::InResource);
127 break;
128 }
129 buffer.append(consume());
130 break;
131 case State::InResource:
132 if (peek() == ' ') {
133 consume();
134 commit_and_advance_to(resource, State::InProtocol);
135 break;
136 }
137 buffer.append(consume());
138 break;
139 case State::InProtocol:
140 if (peek(0) == '\r' && peek(1) == '\n') {
141 consume();
142 consume();
143 commit_and_advance_to(protocol, State::InHeaderName);
144 break;
145 }
146 buffer.append(consume());
147 break;
148 case State::InHeaderName:
149 if (peek(0) == ':' && peek(1) == ' ') {
150 consume();
151 consume();
152 commit_and_advance_to(current_header.name, State::InHeaderValue);
153 break;
154 }
155 buffer.append(consume());
156 break;
157 case State::InHeaderValue:
158 if (peek(0) == '\r' && peek(1) == '\n') {
159 consume();
160 consume();
161
162 // Detect end of headers
163 auto next_state = State::InHeaderName;
164 if (peek(0) == '\r' && peek(1) == '\n') {
165 consume();
166 consume();
167 next_state = State::InBody;
168 }
169
170 commit_and_advance_to(current_header.value, next_state);
171 headers.append(move(current_header));
172 break;
173 }
174 buffer.append(consume());
175 break;
176 case State::InBody:
177 buffer.append(consume());
178 if (index == raw_request.size()) {
179 // End of data, so store the body
180 auto maybe_body = ByteBuffer::copy(buffer);
181 // FIXME: Propagate this error somehow.
182 if (maybe_body.is_error())
183 return {};
184 body = maybe_body.release_value();
185 buffer.clear();
186 }
187 break;
188 }
189 }
190
191 HttpRequest request;
192 if (method == "GET")
193 request.m_method = Method::GET;
194 else if (method == "HEAD")
195 request.m_method = Method::HEAD;
196 else if (method == "POST")
197 request.m_method = Method::POST;
198 else if (method == "DELETE")
199 request.set_method(HTTP::HttpRequest::Method::DELETE);
200 else if (method == "PATCH")
201 request.set_method(HTTP::HttpRequest::Method::PATCH);
202 else if (method == "OPTIONS")
203 request.set_method(HTTP::HttpRequest::Method::OPTIONS);
204 else if (method == "TRACE")
205 request.set_method(HTTP::HttpRequest::Method::TRACE);
206 else if (method == "CONNECT")
207 request.set_method(HTTP::HttpRequest::Method::CONNECT);
208 else if (method == "PUT")
209 request.set_method(HTTP::HttpRequest::Method::PUT);
210 else
211 return {};
212
213 request.m_headers = move(headers);
214 auto url_parts = resource.split_limit('?', 2, SplitBehavior::KeepEmpty);
215
216 request.m_url.set_cannot_be_a_base_url(true);
217 if (url_parts.size() == 2) {
218 request.m_resource = url_parts[0];
219 request.m_url.set_paths({ url_parts[0] });
220 request.m_url.set_query(url_parts[1]);
221 } else {
222 request.m_resource = resource;
223 request.m_url.set_paths({ resource });
224 }
225
226 request.set_body(move(body));
227
228 return request;
229}
230
231void HttpRequest::set_headers(HashMap<DeprecatedString, DeprecatedString> const& headers)
232{
233 for (auto& it : headers)
234 m_headers.append({ it.key, it.value });
235}
236
237Optional<HttpRequest::Header> HttpRequest::get_http_basic_authentication_header(URL const& url)
238{
239 if (!url.includes_credentials())
240 return {};
241 StringBuilder builder;
242 builder.append(url.username());
243 builder.append(':');
244 builder.append(url.password());
245
246 // FIXME: change to TRY() and make method fallible
247 auto token = MUST(encode_base64(MUST(builder.to_string()).bytes()));
248 builder.clear();
249 builder.append("Basic "sv);
250 builder.append(token);
251 return Header { "Authorization", builder.to_deprecated_string() };
252}
253
254Optional<HttpRequest::BasicAuthenticationCredentials> HttpRequest::parse_http_basic_authentication_header(DeprecatedString const& value)
255{
256 if (!value.starts_with("Basic "sv, AK::CaseSensitivity::CaseInsensitive))
257 return {};
258 auto token = value.substring_view(6);
259 if (token.is_empty())
260 return {};
261 auto decoded_token_bb = decode_base64(token);
262 if (decoded_token_bb.is_error())
263 return {};
264 auto decoded_token = DeprecatedString::copy(decoded_token_bb.value());
265 auto colon_index = decoded_token.find(':');
266 if (!colon_index.has_value())
267 return {};
268 auto username = decoded_token.substring_view(0, colon_index.value());
269 auto password = decoded_token.substring_view(colon_index.value() + 1);
270 return BasicAuthenticationCredentials { username, password };
271}
272
273}