//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // FetchHTTP1 follows redirects the way Python's urllib does, so code ported // from urllib keeps its behaviour: // - a relative Location is resolved against the URL that sent it (urljoin, // `..` included), and a redirect may move from http:// to https://; // - 301/302/303/307/308 are followed for a GET; a POST follows 301/302/303 // as a GET without its body, and comes back with a 307 or 308 unfollowed; // - a redirect without a Location comes back as the response; // - the chain stops at the 5th redirect to one URL (4 repeats) or after 10 // different URLs, returning that redirect with `limited` set; // - a Location with a space is percent-encoded, as urllib quotes it; // - a URL that is not http(s), has no host or has a bad port is refused. // // Two listeners: plaintext on 8130, TLS on 8131 with a self-signed certificate // the client is handed as its trust anchor. #include import Crafter.Network; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } HTTPResponse Redirect(std::string status, std::string location) { return CreateResponseHTTP(std::move(status), {{"location", std::move(location)}}, std::string()); } std::unordered_map> Routes() { std::unordered_map> routes; routes["/start"] = [](const HTTPRequest&) { return Redirect("302", "next"); }; routes["/next"] = [](const HTTPRequest&) { return Redirect("301", "/final?x=1"); }; routes["/a/b/start"] = [](const HTTPRequest&) { return Redirect("302", "../c/d"); }; routes["/to-https"] = [](const HTTPRequest&) { return Redirect("307", "https://localhost:8131/secure"); }; routes["/loop"] = [](const HTTPRequest&) { return Redirect("302", "/loop"); }; routes["/nowhere"] = [](const HTTPRequest&) { return CreateResponseHTTP("302", std::string("no location")); }; routes["/form"] = [](const HTTPRequest&) { return Redirect("303", "/method"); }; routes["/keep"] = [](const HTTPRequest&) { return Redirect("307", "/method"); }; routes["/method"] = [](const HTTPRequest& request) { return CreateResponseHTTP("200", request.method + " body=" + request.body); }; routes["/spaced"] = [](const HTTPRequest&) { return Redirect("302", "/with space"); }; return routes; } // Everything else echoes its target, and /hop/ redirects to /hop/. HTTPResponse Fallback(const HTTPRequest& request) { if (request.path.starts_with("/hop/")) { const std::int32_t next = std::stoi(request.path.substr(5)) + 1; return Redirect("302", std::format("/hop/{}", next)); } return CreateResponseHTTP("200", request.path); } bool Refused(std::string_view url) { try { FetchHTTP1(url); } catch (const std::invalid_argument&) { return true; } catch (const std::exception&) { } return false; } } // namespace int main() { try { ListenerAsyncHTTP1 plain(8130, Routes(), Fallback); std::unordered_map> secureRoutes; secureRoutes["/secure"] = [](const HTTPRequest& request) { return CreateResponseHTTP("200", "secure over " + request.scheme); }; ListenerAsyncHTTP1 secure(8131, secureRoutes, TLSServerCredentials { .selfSigned = true }); { const FetchResultHTTP1 result = FetchHTTP1("http://127.0.0.1:8130/start"); Check(result.response.status == "200" && result.response.body == "/final?x=1", std::format("a relative chain arrives at /final?x=1 (got {} '{}')", result.response.status, result.response.body)); Check(result.url == "http://127.0.0.1:8130/final?x=1", std::format("the URL that answered is reported ({})", result.url)); Check(result.redirects == 2 && !result.limited, std::format("two redirects, not limited (got {})", result.redirects)); } { const FetchResultHTTP1 result = FetchHTTP1("http://127.0.0.1:8130/a/b/start"); Check(result.response.body == "/a/c/d", std::format("../c/d from /a/b/start resolves to /a/c/d (got '{}')", result.response.body)); } { FetchOptionsHTTP1 options; options.credentials.caPem = GetSelfSignedCertificatePem().certificate; const FetchResultHTTP1 result = FetchHTTP1("http://127.0.0.1:8130/to-https", options); Check(result.response.status == "200" && result.response.body == "secure over https", std::format("a redirect from http:// to https:// is followed (got {} '{}')", result.response.status, result.response.body)); Check(result.url == "https://localhost:8131/secure", std::format("and lands on the https:// URL ({})", result.url)); } { const FetchResultHTTP1 result = FetchHTTP1("http://127.0.0.1:8130/loop"); Check(result.limited && result.response.status == "302" && result.redirects == 4, std::format("a redirect loop stops after 4 repeats (limited {}, {} redirects)", result.limited, result.redirects)); } { const FetchResultHTTP1 result = FetchHTTP1("http://127.0.0.1:8130/hop/0"); Check(result.limited && result.redirects == 10 && result.url == "http://127.0.0.1:8130/hop/10", std::format("an endless chain stops after 10 different URLs ({} redirects, at {})", result.redirects, result.url)); } { const FetchResultHTTP1 result = FetchHTTP1("http://127.0.0.1:8130/nowhere"); Check(result.response.status == "302" && result.redirects == 0 && !result.limited, "a redirect without a Location comes back as the response"); } { FetchOptionsHTTP1 post; post.method = "POST"; post.body = "payload"; post.headers["content-type"] = "text/plain"; const FetchResultHTTP1 seeOther = FetchHTTP1("http://127.0.0.1:8130/form", post); Check(seeOther.response.body == "GET body=", std::format("a POST answered with 303 is followed as a GET without its body (got '{}')", seeOther.response.body)); const FetchResultHTTP1 kept = FetchHTTP1("http://127.0.0.1:8130/keep", post); Check(kept.response.status == "307" && kept.redirects == 0, "a POST answered with 307 comes back unfollowed"); } { const FetchResultHTTP1 result = FetchHTTP1("http://127.0.0.1:8130/spaced"); Check(result.response.body == "/with%20space", std::format("a Location with a space is percent-encoded before it is followed (got '{}')", result.response.body)); } Check(Refused("ftp://127.0.0.1/file"), "an ftp:// URL is refused"); Check(Refused("http:///path"), "a URL without a host is refused"); Check(Refused("http://127.0.0.1:99999/"), "a port above 65535 is refused"); Check(Refused("http://127.0.0.1:8x/"), "a port that is not a number is refused"); } catch (const std::exception& e) { Check(false, std::format("unexpected exception: {}", e.what())); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }