Coverage Report

Created: 2025-11-16 07:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}