Files
serenity/Userland/Libraries/LibHTTP/Http11Connection.cpp
Dan Klishch 77be5254e1 LibHTTP: Implement bare-bones HTTP/1.1 client using coroutines
We don't have asynchronous TCP socket implementation, so its usefulness
is a bit limited currently but we can still test it using memory
streams. Additionally, it serves as a temporary {show,test}case for the
asynchronous streams machinery.
2024-06-13 17:40:24 +02:00

127 lines
3.5 KiB
C++

/*
* Copyright (c) 2024, Dan Klishch <danilklishch@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/AsyncStreamHelpers.h>
#include <AK/GenericLexer.h>
#include <LibHTTP/Http11Connection.h>
namespace HTTP {
namespace {
StringView method_name(Method method)
{
switch (method) {
#define CASE(x) \
case Method::x: \
return #x##sv;
ENUMERATE_METHODS(CASE)
#undef CASE
default:
VERIFY_NOT_REACHED();
}
}
ByteBuffer format_request(RequestData const& data)
{
StringBuilder builder;
builder.append(method_name(data.method));
builder.append(' ');
builder.append(data.url);
builder.append(" HTTP/1.1\r\n"sv);
for (auto const& [name, value] : data.headers) {
builder.append(name);
builder.append(": "sv);
builder.append(value);
builder.append("\r\n"sv);
}
builder.append("\r\n"sv);
return MUST(builder.to_byte_buffer());
}
struct StatusCodeAndHeaders {
u16 status_code;
Vector<Header> headers;
};
Coroutine<ErrorOr<StatusCodeAndHeaders>> receive_response_headers(AsyncStream& stream)
{
auto status_line = CO_TRY(co_await AsyncStreamHelpers::consume_until(stream, "\r\n"sv));
GenericLexer status_lexer { StringView { status_line } };
if (!status_lexer.next_is("HTTP/1.1 ")) {
stream.reset();
co_return Error::from_string_literal("HTTP-version must be 'HTTP/1.1'");
}
status_lexer.consume(9);
auto status_code = status_lexer.consume_decimal_integer<u16>();
if (status_code.is_error()) {
stream.reset();
co_return Error::from_string_literal("Invalid HTTP status code");
}
Vector<Header> headers;
while (true) {
auto header = StringView { CO_TRY(co_await AsyncStreamHelpers::consume_until(stream, "\r\n"sv)) };
if (header == "\r\n"sv)
break;
auto colon_position = header.find(':');
if (!colon_position.has_value()) {
stream.reset();
co_return Error::from_string_literal("':' must be present in a header line");
}
headers.append({
.header = header.substring_view(0, colon_position.value()),
.value = header.substring_view(colon_position.value() + 1).trim_whitespace(),
});
}
co_return StatusCodeAndHeaders {
.status_code = status_code.value(),
.headers = headers,
};
}
}
Coroutine<ErrorOr<NonnullOwnPtr<Http11Response>>> Http11Response::create(Badge<Http11Connection>, RequestData&& data, AsyncStream& stream)
{
auto header = format_request(data);
if (data.body.has<Empty>()) {
CO_TRY(co_await stream.write({ { header } }));
} else if (data.body.has<RequestData::PlainBody>()) {
auto& body = data.body.get<RequestData::PlainBody>().data;
CO_TRY(co_await stream.write({ { header, body.bytes() } }));
} else {
VERIFY_NOT_REACHED();
}
auto [status_code, headers] = CO_TRY(co_await receive_response_headers(stream));
Optional<size_t> content_length;
for (auto const& header : headers) {
if (header.header.equals_ignoring_ascii_case("Content-Length"sv)) {
content_length = header.value.to_number<size_t>();
}
}
if (!content_length.has_value()) {
stream.reset();
co_return Error::from_string_literal("'Content-Length' must be provided");
}
auto body = make<AsyncInputStreamSlice>(stream, content_length.value());
co_return adopt_own(*new (nothrow) Http11Response(move(body), status_code, move(headers)));
}
}