srctree

Robin Linden parent f3fcb910 7622d744
protocol: Split success and error results into separate types

browser/gui/app.cpp added: 184, removed: 173, total 11
@@ -524,7 +524,7 @@ void App::reload() {
 
void App::on_navigation_failure(protocol::ErrorCode err) {
update_status_line();
response_headers_str_ = maybe_page_.error().response.headers.to_string();
response_headers_str_.clear();
dom_str_.clear();
stylesheet_str_.clear();
layout_str_.clear();
@@ -587,8 +587,8 @@ void App::on_page_loaded() {
 
auto icon = engine_.load(*uri).response;
sf::Image favicon;
if (icon.err != protocol::ErrorCode::Ok || !favicon.loadFromMemory(icon.body.data(), icon.body.size())) {
spdlog::warn("Error loading favicon from '{}': {}", uri->uri, to_string(icon.err));
if (!icon.has_value() || !favicon.loadFromMemory(icon->body.data(), icon->body.size())) {
spdlog::warn("Error loading favicon from '{}': {}", uri->uri, to_string(icon.error().err));
continue;
}
 
@@ -669,15 +669,15 @@ void App::scroll(int pixels) {
}
 
void App::update_status_line() {
auto const &r = [this] {
auto const &status = [this] {
if (maybe_page_) {
return page().response;
return page().response.status_line;
}
 
return maybe_page_.error().response;
return maybe_page_.error().response.status_line.value_or(protocol::StatusLine{});
}();
 
status_line_str_ = fmt::format("{} {} {}", r.status_line.version, r.status_line.status_code, r.status_line.reason);
status_line_str_ = fmt::format("{} {} {}", status.version, status.status_code, status.reason);
}
 
void App::run_overlay() {
@@ -710,8 +710,6 @@ void App::run_http_response_widget() const {
if (ImGui::CollapsingHeader("Body")) {
if (maybe_page_) {
ImGui::TextUnformatted(page().response.body.c_str());
} else {
ImGui::TextUnformatted(maybe_page_.error().response.body.c_str());
}
}
});
 
engine/BUILD added: 184, removed: 173, total 11
@@ -39,5 +39,6 @@ cc_test(
"//type",
"//type:naive",
"//uri",
"@expected",
],
)
 
engine/engine.cpp added: 184, removed: 173, total 11
@@ -32,16 +32,16 @@ namespace engine {
tl::expected<std::unique_ptr<PageState>, NavigationError> Engine::navigate(uri::Uri uri) {
auto result = load(std::move(uri));
 
if (result.response.err != protocol::ErrorCode::Ok) {
if (!result.response.has_value()) {
return tl::unexpected{NavigationError{
.uri = std::move(result.uri_after_redirects),
.response = std::move(result.response),
.response = std::move(result.response.error()),
}};
}
 
auto state = std::make_unique<PageState>();
state->uri = std::move(result.uri_after_redirects);
state->response = std::move(result.response);
state->response = std::move(result.response.value());
state->dom = html::parse(state->response.body);
state->stylesheet = css::default_style();
 
@@ -80,25 +80,25 @@ tl::expected<std::unique_ptr<PageState>, NavigationError> Engine::navigate(uri::
auto &style_data = res.response;
stylesheet_url = std::move(res.uri_after_redirects);
 
if (style_data.err != protocol::ErrorCode::Ok) {
spdlog::warn("Error {} downloading {}", static_cast<int>(style_data.err), stylesheet_url->uri);
if (!style_data.has_value()) {
spdlog::warn("Error {} downloading {}", static_cast<int>(style_data.error().err), stylesheet_url->uri);
return {};
}
 
if ((stylesheet_url->scheme == "http" || stylesheet_url->scheme == "https")
&& style_data.status_line.status_code != 200) {
&& style_data->status_line.status_code != 200) {
spdlog::warn("Error {}: {} downloading {}",
style_data.status_line.status_code,
style_data.status_line.reason,
style_data->status_line.status_code,
style_data->status_line.reason,
stylesheet_url->uri);
return {};
}
 
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives
auto encoding = style_data.headers.get("Content-Encoding");
auto encoding = style_data->headers.get("Content-Encoding");
if (encoding == "gzip" || encoding == "x-gzip" || encoding == "deflate") {
auto zlib_mode = encoding == "deflate" ? archive::ZlibMode::Zlib : archive::ZlibMode::Gzip;
auto decoded = archive::zlib_decode(style_data.body, zlib_mode);
auto decoded = archive::zlib_decode(style_data->body, zlib_mode);
if (!decoded) {
auto const &err = decoded.error();
spdlog::error("Failed {}-decoding of '{}': '{}: {}'",
@@ -109,13 +109,13 @@ tl::expected<std::unique_ptr<PageState>, NavigationError> Engine::navigate(uri::
return {};
}
 
style_data.body = *std::move(decoded);
style_data->body = *std::move(decoded);
} else if (encoding) {
spdlog::warn("Got unsupported encoding '{}', skipping stylesheet '{}'", *encoding, stylesheet_url->uri);
return {};
}
 
return css::parse(style_data.body);
return css::parse(style_data->body);
}));
}
 
@@ -146,27 +146,36 @@ Engine::LoadResult Engine::load(uri::Uri uri) {
};
 
int redirect_count = 0;
protocol::Response response = protocol_handler_->handle(uri);
while (response.err == protocol::ErrorCode::Ok && is_redirect(response.status_line.status_code)) {
auto response = protocol_handler_->handle(uri);
while (response.has_value() && is_redirect(response->status_line.status_code)) {
++redirect_count;
auto location = response.headers.get("Location");
auto location = response->headers.get("Location");
if (!location) {
response.err = protocol::ErrorCode::InvalidResponse;
return {std::move(response), std::move(uri)};
return {
.response = tl::unexpected{protocol::Error{
protocol::ErrorCode::InvalidResponse, std::move(response->status_line)}},
.uri_after_redirects = std::move(uri),
};
}
 
spdlog::info("Following {} redirect from {} to {}", response.status_line.status_code, uri.uri, *location);
spdlog::info("Following {} redirect from {} to {}", response->status_line.status_code, uri.uri, *location);
auto new_uri = uri::Uri::parse(std::string(*location), uri);
if (!new_uri) {
response.err = protocol::ErrorCode::InvalidResponse;
return {std::move(response), std::move(uri)};
return {
.response = tl::unexpected{protocol::Error{
protocol::ErrorCode::InvalidResponse, std::move(response->status_line)}},
.uri_after_redirects = std::move(uri),
};
}
 
uri = *std::move(new_uri);
response = protocol_handler_->handle(uri);
if (redirect_count > kMaxRedirects) {
response.err = protocol::ErrorCode::RedirectLimit;
return {std::move(response), std::move(uri)};
return {
.response = tl::unexpected{protocol::Error{
protocol::ErrorCode::RedirectLimit, std::move(response->status_line)}},
.uri_after_redirects = std::move(uri),
};
}
}
 
 
engine/engine.h added: 184, removed: 173, total 11
@@ -36,7 +36,7 @@ struct PageState {
 
struct NavigationError {
uri::Uri uri{};
protocol::Response response{};
protocol::Error response{};
};
 
class Engine {
@@ -52,7 +52,7 @@ public:
void relayout(PageState &, int layout_width);
 
struct [[nodiscard]] LoadResult {
protocol::Response response;
tl::expected<protocol::Response, protocol::Error> response;
uri::Uri uri_after_redirects;
};
LoadResult load(uri::Uri);
 
engine/engine_test.cpp added: 184, removed: 173, total 11
@@ -16,6 +16,8 @@
#include "type/type.h"
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
#include <algorithm>
#include <map>
#include <memory>
@@ -31,15 +33,19 @@ using etest::require;
using protocol::ErrorCode;
using protocol::Response;
 
using Responses = std::map<std::string, tl::expected<Response, protocol::Error>>;
 
namespace {
 
class FakeProtocolHandler final : public protocol::IProtocolHandler {
public:
explicit FakeProtocolHandler(std::map<std::string, Response> responses) : responses_{std::move(responses)} {}
[[nodiscard]] Response handle(uri::Uri const &uri) override { return responses_.at(uri.uri); }
explicit FakeProtocolHandler(Responses responses) : responses_{std::move(responses)} {}
[[nodiscard]] tl::expected<Response, protocol::Error> handle(uri::Uri const &uri) override {
return responses_.at(uri.uri);
}
 
private:
std::map<std::string, Response> responses_;
Responses responses_;
};
 
bool contains(std::vector<css::Rule> const &stylesheet, css::Rule const &rule) {
@@ -50,10 +56,9 @@ bool contains(std::vector<css::Rule> const &stylesheet, css::Rule const &rule) {
 
int main() {
etest::test("css in <head><style>", [] {
std::map<std::string, Response> responses{{
Responses responses{{
"hax://example.com"s,
Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><style>p { font-size: 123em; }</style></head></html>"},
},
@@ -68,8 +73,8 @@ int main() {
});
 
etest::test("navigation failure", [] {
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = ErrorCode::Unresolved}},
engine::Engine e{std::make_unique<FakeProtocolHandler>(Responses{
std::pair{"hax://example.com"s, tl::unexpected{protocol::Error{ErrorCode::Unresolved}}},
})};
 
auto page = e.navigate(uri::Uri::parse("hax://example.com").value());
@@ -77,8 +82,8 @@ int main() {
});
 
etest::test("page load", [] {
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = ErrorCode::Ok}},
engine::Engine e{std::make_unique<FakeProtocolHandler>(Responses{
std::pair{"hax://example.com"s, Response{}},
})};
 
auto page = e.navigate(uri::Uri::parse("hax://example.com").value());
@@ -86,8 +91,8 @@ int main() {
});
 
etest::test("layout update", [] {
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = ErrorCode::Ok}},
engine::Engine e{std::make_unique<FakeProtocolHandler>(Responses{
std::pair{"hax://example.com"s, Response{}},
})};
e.set_layout_width(123);
 
@@ -99,10 +104,9 @@ int main() {
});
 
etest::test("css in <head><style> takes priority over browser built-in css", [] {
std::map<std::string, Response> responses{{
Responses responses{{
"hax://example.com"s,
Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html></html>"},
},
@@ -113,10 +117,9 @@ int main() {
require(page->layout.has_value());
expect_eq(page->layout->get_property<css::PropertyId::Display>(), style::DisplayValue::Block);
 
responses = std::map<std::string, Response>{{
responses = Responses{{
"hax://example.com"s,
Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><style>html { display: inline; }</style></head></html>"},
},
@@ -132,10 +135,9 @@ int main() {
});
 
etest::test("multiple inline <head><style> elements are allowed", [] {
std::map<std::string, Response> responses{{
Responses responses{{
"hax://example.com"s,
Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><style>"
"a { color: red; } "
@@ -156,9 +158,8 @@ int main() {
});
 
etest::test("stylesheet link, parallel download", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head>"
"<link rel=stylesheet href=one.css />"
@@ -166,12 +167,10 @@ int main() {
"</head></html>"},
};
responses["hax://example.com/one.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"p { font-size: 123em; }"},
};
responses["hax://example.com/two.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"p { color: green; }"},
};
@@ -183,14 +182,12 @@ int main() {
});
 
etest::test("stylesheet link, unsupported Content-Encoding", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><link rel=stylesheet href=lol.css /></head></html>"},
};
responses["hax://example.com/lol.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.headers{{"Content-Encoding", "really-borked-content-type"}},
.body{"p { font-size: 123em; }"},
@@ -209,14 +206,12 @@ int main() {
"\x78\x5e\x2b\x50\xa8\x56\x48\xcb\xcf\x2b\xd1\x2d\xce\xac\x4a\xb5\x52\x30\x34\x32\x4e\xcd\xb5\x56\xa8\xe5\x02\x00\x63\xc3\x07\x6f"s;
 
etest::test("stylesheet link, gzip Content-Encoding", [gzipped_css]() mutable {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><link rel=stylesheet href=lol.css /></head></html>"},
};
responses["hax://example.com/lol.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.headers{{"Content-Encoding", "gzip"}},
.body{gzipped_css},
@@ -232,7 +227,6 @@ int main() {
 
// And again, but with x-gzip instead.
responses["hax://example.com/lol.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.headers{{"Content-Encoding", "x-gzip"}},
.body{std::move(gzipped_css)},
@@ -248,16 +242,14 @@ int main() {
});
 
etest::test("stylesheet link, gzip Content-Encoding, bad header", [gzipped_css]() mutable {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><link rel=stylesheet href=lol.css /></head></html>"},
};
// Ruin the gzip header.
gzipped_css[1] += 1;
responses["hax://example.com/lol.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.headers{{"Content-Encoding", "gzip"}},
.body{std::move(gzipped_css)},
@@ -273,16 +265,14 @@ int main() {
});
 
etest::test("stylesheet link, gzip Content-Encoding, crc32 mismatch", [gzipped_css]() mutable {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><link rel=stylesheet href=lol.css /></head></html>"},
};
// Ruin the content.
gzipped_css[20] += 1;
responses["hax://example.com/lol.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.headers{{"Content-Encoding", "gzip"}},
.body{std::move(gzipped_css)},
@@ -298,14 +288,12 @@ int main() {
});
 
etest::test("stylesheet link, gzip Content-Encoding, served zlib", [zlibbed_css] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><link rel=stylesheet href=lol.css /></head></html>"},
};
responses["hax://example.com/lol.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.headers{{"Content-Encoding", "gzip"}},
.body{zlibbed_css},
@@ -321,14 +309,12 @@ int main() {
});
 
etest::test("stylesheet link, deflate Content-Encoding", [zlibbed_css] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head><link rel=stylesheet href=lol.css /></head></html>"},
};
responses["hax://example.com/lol.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.headers{{"Content-Encoding", "deflate"}},
.body{zlibbed_css},
@@ -344,20 +330,17 @@ int main() {
});
 
etest::test("redirect", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 301},
.headers = {{"Location", "hax://example.com/redirected"}},
};
responses["hax://example.com/redirected"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><body>hello!</body></html>"},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
auto page = e.navigate(uri::Uri::parse("hax://example.com").value()).value();
expect_eq(page->response.err, protocol::ErrorCode::Ok);
expect_eq(page->uri.uri, "hax://example.com/redirected");
 
auto const &body = std::get<dom::Element>(page->dom.html().children.at(1));
@@ -365,9 +348,8 @@ int main() {
});
 
etest::test("redirect not providing Location header", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 301},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
@@ -376,34 +358,29 @@ int main() {
});
 
etest::test("redirect, style", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><head>"
"<link rel=stylesheet href=hello.css />"
"</head></html>"},
};
responses["hax://example.com/hello.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 301},
.headers = {{"Location", "hax://example.com/redirected.css"}},
};
responses["hax://example.com/redirected.css"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"p { color: green; }"},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
auto page = e.navigate(uri::Uri::parse("hax://example.com").value()).value();
expect_eq(page->response.err, protocol::ErrorCode::Ok);
expect(contains(page->stylesheet.rules, {.selectors{"p"}, .declarations{{css::PropertyId::Color, "green"}}}));
});
 
etest::test("redirect loop", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 301},
.headers = {{"Location", "hax://example.com"}},
};
@@ -413,14 +390,12 @@ int main() {
});
 
etest::test("load", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 301},
.headers = {{"Location", "hax://example.com/redirected"}},
};
responses["hax://example.com/redirected"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{"<html><body>hello!</body></html>"},
};
@@ -433,14 +408,13 @@ int main() {
etest::test("IType accessor, you get what you give", [] {
auto naive = std::make_unique<type::NaiveType>();
type::IType const *saved = naive.get();
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map<std::string, Response>{}), std::move(naive)};
engine::Engine e{std::make_unique<FakeProtocolHandler>(Responses{}), std::move(naive)};
expect_eq(&e.font_system(), saved);
});
 
etest::test("bad uri in redirect", [] {
std::map<std::string, Response> responses;
Responses responses;
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 301},
.headers = {{"Location", ""}},
};
@@ -450,18 +424,17 @@ int main() {
});
 
etest::test("bad uri in style href", [] {
std::map<std::string, Response> responses;
Responses responses;
std::string body = "<html><head><link rel=stylesheet href=";
body += std::string(1025, 'a');
body += " /></head></html>";
responses["hax://example.com"s] = Response{
.err = ErrorCode::Ok,
.status_line = {.status_code = 200},
.body{std::move(body)},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
auto page = e.navigate(uri::Uri::parse("hax://example.com").value()).value();
expect_eq(page->response.err, protocol::ErrorCode::Ok);
auto page = e.navigate(uri::Uri::parse("hax://example.com").value());
expect(page.has_value());
});
 
return etest::run_all_tests();
 
protocol/BUILD added: 184, removed: 173, total 11
@@ -15,6 +15,7 @@ cc_library(
"//net",
"//uri",
"//util:string",
"@expected",
"@fmt",
],
)
@@ -37,6 +38,7 @@ cc_library(
":protocol",
"//etest",
"//uri",
"@expected",
"@fmt",
],
) for src in glob(
 
protocol/file_handler.cpp added: 184, removed: 173, total 11
@@ -9,6 +9,8 @@
 
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
#include <filesystem>
#include <fstream>
#include <ios>
@@ -17,21 +19,21 @@
 
namespace protocol {
 
Response FileHandler::handle(uri::Uri const &uri) {
tl::expected<Response, Error> FileHandler::handle(uri::Uri const &uri) {
auto path = std::filesystem::path(uri.path);
if (!exists(path)) {
return {ErrorCode::Unresolved};
return tl::unexpected{protocol::Error{ErrorCode::Unresolved}};
}
 
if (!is_regular_file(path)) {
return {ErrorCode::InvalidResponse};
return tl::unexpected{protocol::Error{ErrorCode::InvalidResponse}};
}
 
auto file = std::ifstream(path, std::ios::in | std::ios::binary);
auto size = file_size(path);
auto content = std::string(size, '\0');
file.read(content.data(), size);
return {ErrorCode::Ok, {}, {}, std::move(content)};
return Response{{}, {}, std::move(content)};
}
 
} // namespace protocol
 
protocol/file_handler.h added: 184, removed: 173, total 11
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2022-2024 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
@@ -11,11 +11,13 @@
 
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
namespace protocol {
 
class FileHandler final : public IProtocolHandler {
public:
[[nodiscard]] Response handle(uri::Uri const &uri) override;
[[nodiscard]] tl::expected<Response, Error> handle(uri::Uri const &uri) override;
};
 
} // namespace protocol
 
protocol/file_handler_test.cpp added: 184, removed: 173, total 11
@@ -63,7 +63,7 @@ int main() {
etest::test("uri pointing to non-existent file", [] {
protocol::FileHandler handler;
auto res = handler.handle(uri::Uri::parse("file:///this/file/does/definitely/not/exist.hastur").value());
expect_eq(res, protocol::Response{protocol::ErrorCode::Unresolved});
expect_eq(res.error(), protocol::Error{protocol::ErrorCode::Unresolved});
});
 
etest::test("uri pointing to a folder", [] {
@@ -71,7 +71,7 @@ int main() {
 
protocol::FileHandler handler;
auto res = handler.handle(uri::Uri::parse(fmt::format("file://{}", tmp_dir.generic_string())).value());
expect_eq(res, protocol::Response{protocol::ErrorCode::InvalidResponse});
expect_eq(res.error(), protocol::Error{protocol::ErrorCode::InvalidResponse});
});
 
etest::test("uri pointing to a regular file", [] {
@@ -84,7 +84,7 @@ int main() {
 
protocol::FileHandler handler;
auto res = handler.handle(uri::Uri::parse(fmt::format("file://{}", tmp_file->path().generic_string())).value());
expect_eq(res, protocol::Response{protocol::ErrorCode::Ok, {}, {}, "hello!"});
expect_eq(res, protocol::Response{{}, {}, "hello!"});
});
 
return etest::run_all_tests();
 
protocol/http.h added: 184, removed: 173, total 11
@@ -11,6 +11,8 @@
#include "uri/uri.h"
#include "util/string.h"
 
#include <tl/expected.hpp>
 
#include <charconv>
#include <cstddef>
#include <optional>
@@ -23,39 +25,40 @@ namespace protocol {
 
class Http {
public:
static Response get(auto &&socket, uri::Uri const &uri, std::optional<std::string_view> user_agent) {
static tl::expected<Response, Error> get(
auto &&socket, uri::Uri const &uri, std::optional<std::string_view> user_agent) {
using namespace std::string_view_literals;
 
if (!socket.connect(uri.authority.host, Http::use_port(uri) ? uri.authority.port : uri.scheme)) {
return {ErrorCode::Unresolved};
return tl::unexpected{Error{ErrorCode::Unresolved}};
}
 
socket.write(Http::create_get_request(uri, std::move(user_agent)));
auto data = socket.read_until("\r\n"sv);
if (data.empty()) {
return {ErrorCode::InvalidResponse};
return tl::unexpected{Error{ErrorCode::InvalidResponse}};
}
 
auto status_line = Http::parse_status_line(data.substr(0, data.size() - 2));
if (!status_line) {
return {ErrorCode::InvalidResponse};
return tl::unexpected{Error{ErrorCode::InvalidResponse}};
}
 
data = socket.read_until("\r\n\r\n"sv);
if (data.empty()) {
return {ErrorCode::InvalidResponse, std::move(*status_line)};
return tl::unexpected{Error{ErrorCode::InvalidResponse, std::move(status_line)}};
}
 
auto headers = Http::parse_headers(data.substr(0, data.size() - 4));
if (headers.size() == 0) {
return {ErrorCode::InvalidResponse, std::move(*status_line)};
return tl::unexpected{Error{ErrorCode::InvalidResponse, std::move(status_line)}};
}
 
auto encoding = headers.get("transfer-encoding"sv);
if (encoding == "chunked"sv) {
auto body = Http::get_chunked_body(socket);
if (!body) {
return {ErrorCode::InvalidResponse, std::move(*status_line)};
return tl::unexpected{Error{ErrorCode::InvalidResponse, std::move(status_line)}};
}
 
data = *body;
@@ -63,7 +66,7 @@ public:
data = socket.read_all();
}
 
return {ErrorCode::Ok, std::move(*status_line), std::move(headers), std::move(data)};
return Response{std::move(*status_line), std::move(headers), std::move(data)};
}
 
private:
 
protocol/http_handler.cpp added: 184, removed: 173, total 11
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021-2022 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021-2024 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021 Mikael Larsson <c.mikael.larsson@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
@@ -10,9 +10,11 @@
#include "protocol/response.h"
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
namespace protocol {
 
Response HttpHandler::handle(uri::Uri const &uri) {
tl::expected<Response, Error> HttpHandler::handle(uri::Uri const &uri) {
return Http::get(net::Socket{}, uri, user_agent_);
}
 
 
protocol/http_handler.h added: 184, removed: 173, total 11
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2022-2024 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
@@ -10,6 +10,8 @@
 
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
#include <optional>
#include <string>
#include <utility>
@@ -20,7 +22,7 @@ class HttpHandler final : public IProtocolHandler {
public:
explicit HttpHandler(std::optional<std::string> user_agent) : user_agent_{std::move(user_agent)} {}
 
[[nodiscard]] Response handle(uri::Uri const &) override;
[[nodiscard]] tl::expected<Response, Error> handle(uri::Uri const &) override;
 
private:
std::optional<std::string> user_agent_;
 
protocol/http_test.cpp added: 184, removed: 173, total 11
@@ -106,7 +106,7 @@ int main() {
"</head>\n"
"</html>\n";
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).value();
 
require(response.headers.size() == 13);
expect_eq(socket.host, "example.com");
@@ -155,7 +155,7 @@ int main() {
"<A HREF=\"http://www.google.com/\">here</A>.\r\n"
"</BODY></HTML>\r\n";
 
auto response = protocol::Http::get(socket, create_uri("http://google.com"), std::nullopt);
auto response = protocol::Http::get(socket, create_uri("http://google.com"), std::nullopt).value();
 
require(response.headers.size() == 7);
expect_eq(socket.host, "google.com");
@@ -187,7 +187,7 @@ int main() {
"0\r\n"
"\r\n");
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).value();
 
expect_eq(response.body,
"<!DOCTYPE html>\r\n"
@@ -206,7 +206,7 @@ int main() {
" 5\r\nhello\r\n"
" 0\r\n\r\n");
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).value();
 
expect_eq(response.body, "hello");
});
@@ -216,7 +216,7 @@ int main() {
"5 \r\nhello\r\n"
"0 \r\n\r\n");
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).value();
 
expect_eq(response.body, "hello");
});
@@ -226,7 +226,7 @@ int main() {
"8684838388283847263674\r\nhello\r\n"
"0\r\n\r\n");
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
 
expect_eq(response.err, protocol::ErrorCode::InvalidResponse);
});
@@ -236,7 +236,7 @@ int main() {
"5\r\nhello"
"0\r\n\r\n");
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
 
expect_eq(response.err, protocol::ErrorCode::InvalidResponse);
});
@@ -246,7 +246,7 @@ int main() {
"6\r\nhello\r\n"
"0\r\n\r\n");
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
 
expect_eq(response.err, protocol::ErrorCode::InvalidResponse);
});
@@ -256,7 +256,7 @@ int main() {
"3\r\nhello\r\n"
"0\r\n\r\n");
 
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
 
expect_eq(response.err, protocol::ErrorCode::InvalidResponse);
});
@@ -264,37 +264,36 @@ int main() {
etest::test("404 no headers no body", [] {
FakeSocket socket;
socket.read_data = "HTTP/1.1 404 Not Found\r\n\r\n";
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
 
require(response.headers.size() == 0);
expect_eq(response.status_line.version, "HTTP/1.1"sv);
expect_eq(response.status_line.status_code, 404);
expect_eq(response.status_line.reason, "Not Found");
expect_eq(response.status_line->version, "HTTP/1.1"sv);
expect_eq(response.status_line->status_code, 404);
expect_eq(response.status_line->reason, "Not Found");
});
 
etest::test("connect failure", [] {
FakeSocket socket{.connect_result = false};
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
expect_eq(response, protocol::Response{.err = protocol::ErrorCode::Unresolved});
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
expect_eq(response, protocol::Error{.err = protocol::ErrorCode::Unresolved});
});
 
etest::test("empty response", [] {
FakeSocket socket{};
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
expect_eq(response, protocol::Response{.err = protocol::ErrorCode::InvalidResponse});
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
expect_eq(response, protocol::Error{.err = protocol::ErrorCode::InvalidResponse});
});
 
etest::test("empty status line", [] {
FakeSocket socket{.read_data = "\r\n"};
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
expect_eq(response, protocol::Response{.err = protocol::ErrorCode::InvalidResponse});
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
expect_eq(response, protocol::Error{.err = protocol::ErrorCode::InvalidResponse});
});
 
etest::test("no headers", [] {
FakeSocket socket{.read_data = "HTTP/1.1 200 OK\r\n \r\n\r\n"};
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
auto response = protocol::Http::get(socket, create_uri(), std::nullopt).error();
expect_eq(response,
protocol::Response{.err = protocol::ErrorCode::InvalidResponse, .status_line{"HTTP/1.1", 200, "OK"}});
protocol::Error{protocol::ErrorCode::InvalidResponse, protocol::StatusLine{"HTTP/1.1", 200, "OK"}});
});
 
etest::test("mixed valid and invalid headers", [] {
@@ -302,7 +301,6 @@ int main() {
auto response = protocol::Http::get(socket, create_uri(), std::nullopt);
expect_eq(response,
protocol::Response{
.err = protocol::ErrorCode::Ok,
.status_line{"HTTP/1.1", 200, "OK"},
.headers{{"one", "1"}, {"two", "2"}},
});
 
protocol/https_handler.cpp added: 184, removed: 173, total 11
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021-2022 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021-2024 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021 Mikael Larsson <c.mikael.larsson@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
@@ -10,9 +10,11 @@
#include "protocol/response.h"
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
namespace protocol {
 
Response HttpsHandler::handle(uri::Uri const &uri) {
tl::expected<Response, Error> HttpsHandler::handle(uri::Uri const &uri) {
return Http::get(net::SecureSocket{}, uri, user_agent_);
}
 
 
protocol/https_handler.h added: 184, removed: 173, total 11
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2022-2024 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
@@ -10,6 +10,8 @@
 
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
#include <optional>
#include <string>
#include <utility>
@@ -20,7 +22,7 @@ class HttpsHandler final : public IProtocolHandler {
public:
explicit HttpsHandler(std::optional<std::string> user_agent) : user_agent_{std::move(user_agent)} {}
 
[[nodiscard]] Response handle(uri::Uri const &) override;
[[nodiscard]] tl::expected<Response, Error> handle(uri::Uri const &) override;
 
private:
std::optional<std::string> user_agent_;
 
protocol/iprotocol_handler.h added: 184, removed: 173, total 11
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2022-2024 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
@@ -9,12 +9,14 @@
 
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
namespace protocol {
 
class IProtocolHandler {
public:
virtual ~IProtocolHandler() = default;
[[nodiscard]] virtual Response handle(uri::Uri const &) = 0;
[[nodiscard]] virtual tl::expected<Response, Error> handle(uri::Uri const &) = 0;
};
 
} // namespace protocol
 
protocol/multi_protocol_handler.h added: 184, removed: 173, total 11
@@ -8,6 +8,8 @@
 
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
#include <functional>
#include <map>
#include <memory>
@@ -22,12 +24,12 @@ public:
handlers_[std::move(protocol)] = std::move(handler);
}
 
[[nodiscard]] Response handle(uri::Uri const &uri) override {
[[nodiscard]] tl::expected<Response, Error> handle(uri::Uri const &uri) override {
if (auto it = handlers_.find(uri.scheme); it != handlers_.end()) {
return it->second->handle(uri);
}
 
return {ErrorCode::Unhandled};
return tl::unexpected{Error{ErrorCode::Unhandled}};
}
 
private:
 
protocol/multi_protocol_handler_test.cpp added: 184, removed: 173, total 11
@@ -10,6 +10,8 @@
#include "etest/etest.h"
#include "uri/uri.h"
 
#include <tl/expected.hpp>
 
#include <memory>
#include <utility>
 
@@ -21,10 +23,12 @@ namespace {
class FakeProtocolHandler final : public protocol::IProtocolHandler {
public:
explicit FakeProtocolHandler(protocol::Response response) : response_{std::move(response)} {}
[[nodiscard]] protocol::Response handle(uri::Uri const &) override { return response_; }
[[nodiscard]] tl::expected<protocol::Response, protocol::Error> handle(uri::Uri const &) override {
return response_;
}
 
private:
protocol::Response response_;
tl::expected<protocol::Response, protocol::Error> response_;
};
 
} // namespace
@@ -32,10 +36,11 @@ private:
int main() {
etest::test("added protocols are handled", [] {
MultiProtocolHandler handler;
expect_eq(handler.handle(uri::Uri{.scheme = "hax"}).err, protocol::ErrorCode::Unhandled);
expect_eq(handler.handle(uri::Uri{.scheme = "hax"}),
tl::unexpected{protocol::Error{protocol::ErrorCode::Unhandled}});
 
handler.add("hax", std::make_unique<FakeProtocolHandler>(protocol::Response{protocol::ErrorCode::Ok}));
expect_eq(handler.handle(uri::Uri{.scheme = "hax"}).err, protocol::ErrorCode::Ok);
handler.add("hax", std::make_unique<FakeProtocolHandler>(protocol::Response{}));
expect_eq(handler.handle(uri::Uri{.scheme = "hax"}), protocol::Response{});
});
 
return etest::run_all_tests();
 
protocol/response.h added: 184, removed: 173, total 11
@@ -56,7 +56,6 @@ private:
};
 
struct Response {
ErrorCode err{};
StatusLine status_line;
Headers headers;
std::string body;
@@ -64,6 +63,13 @@ struct Response {
[[nodiscard]] bool operator==(Response const &) const = default;
};
 
struct Error {
ErrorCode err{};
std::optional<StatusLine> status_line;
 
[[nodiscard]] bool operator==(Error const &) const = default;
};
 
} // namespace protocol
 
#endif