Serenity Operating System
at master 273 lines 8.7 kB view raw
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}