// The HTTP client is exercised against a throwaway loopback server rather than // the real VPNGate endpoint: the tests must pass on a machine with no network, // and the interesting cases (chunked encoding, redirect loops, byte caps) are // hard to provoke against a live server anyway. #include #include #include #include #include "common/http_get.h" #include "harness.h" using namespace ovg; namespace { // Serves one canned response per connection, then closes. class TinyServer { public: TinyServer(asio::io_context &io, std::string response) : acceptor_(io, asio::ip::tcp::endpoint( asio::ip::make_address("127.0.0.1"), 0)), response_(std::move(response)) { acceptor_.listen(); accept(); } uint16_t port() const { return acceptor_.local_endpoint().port(); } std::string url(const std::string &path = "/") const { return "http://127.0.0.1:" + std::to_string(port()) + path; } int connections() const { return connections_; } void stop() { std::error_code ec; acceptor_.close(ec); } // Second response, used for redirect targets. void set_next(std::string r) { next_ = std::move(r); } private: void accept() { auto sock = std::make_shared(acceptor_.get_executor()); acceptor_.async_accept(*sock, [this, sock](std::error_code ec) { if (ec) return; ++connections_; auto body = std::make_shared( (connections_ > 1 && !next_.empty()) ? next_ : response_); // Read (and discard) the request line before answering; some clients get // upset if the response arrives before they finish writing. auto buf = std::make_shared(); asio::async_read_until( *sock, *buf, "\r\n\r\n", [sock, body, buf](std::error_code, size_t) { asio::async_write(*sock, asio::buffer(*body), [sock, body](std::error_code, size_t) { std::error_code ig; sock->shutdown( asio::ip::tcp::socket::shutdown_both, ig); sock->close(ig); }); }); accept(); }); } asio::ip::tcp::acceptor acceptor_; std::string response_; std::string next_; int connections_ = 0; }; } // namespace OVG_TEST(ParseUrlForms) { http::Url u; CHECK(http::parse_url("http://www.vpngate.net/api/iphone/", &u)); CHECK_EQ(u.scheme, std::string("http")); CHECK_EQ(u.host, std::string("www.vpngate.net")); CHECK_EQ(u.port, std::string("80")); CHECK_EQ(u.target, std::string("/api/iphone/")); CHECK(http::parse_url("https://example.com", &u)); CHECK_EQ(u.port, std::string("443")); CHECK_EQ(u.target, std::string("/")); CHECK(http::parse_url("http://example.com:8080/x?y=1", &u)); CHECK_EQ(u.port, std::string("8080")); CHECK_EQ(u.target, std::string("/x?y=1")); CHECK(!http::parse_url("ftp://example.com/", &u)); CHECK(!http::parse_url("example.com", &u)); CHECK(!http::parse_url("", &u)); } OVG_TEST(HttpGetContentLengthBody) { asio::io_context io; TinyServer srv(io, "HTTP/1.1 200 OK\r\n" "Content-Length: 11\r\n" "Content-Type: text/plain\r\n\r\n" "hello world"); std::error_code got_ec; http::Response got; http::async_get(io, srv.url(), {}, [&](std::error_code ec, http::Response r) { got_ec = ec; got = std::move(r); srv.stop(); }); io.run(); CHECK(!got_ec); CHECK_EQ(got.status, 200); CHECK_EQ(got.body, std::string("hello world")); } OVG_TEST(HttpGetChunkedBody) { asio::io_context io; TinyServer srv(io, "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n\r\n" "5\r\nhello\r\n" "1\r\n \r\n" "5\r\nworld\r\n" "0\r\n\r\n"); std::error_code got_ec; http::Response got; http::async_get(io, srv.url(), {}, [&](std::error_code ec, http::Response r) { got_ec = ec; got = std::move(r); srv.stop(); }); io.run(); CHECK(!got_ec); CHECK_EQ(got.body, std::string("hello world")); } OVG_TEST(HttpGetEofTerminatedBody) { // HTTP/1.0-style: no Content-Length, no chunking, body ends at close. The // VPNGate endpoint has been observed doing exactly this. asio::io_context io; TinyServer srv(io, "HTTP/1.1 200 OK\r\n\r\nbody-until-eof"); std::error_code got_ec; http::Response got; http::async_get(io, srv.url(), {}, [&](std::error_code ec, http::Response r) { got_ec = ec; got = std::move(r); srv.stop(); }); io.run(); CHECK(!got_ec); CHECK_EQ(got.body, std::string("body-until-eof")); } OVG_TEST(HttpGetFollowsRedirect) { asio::io_context io; TinyServer target(io, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"); TinyServer front(io, "HTTP/1.1 302 Found\r\nLocation: " + target.url() + "\r\nContent-Length: 0\r\n\r\n"); std::error_code got_ec; http::Response got; http::async_get(io, front.url(), {}, [&](std::error_code ec, http::Response r) { got_ec = ec; got = std::move(r); front.stop(); target.stop(); }); io.run(); CHECK(!got_ec); CHECK_EQ(got.status, 200); CHECK_EQ(got.body, std::string("ok")); } OVG_TEST(HttpGetEnforcesByteCap) { asio::io_context io; std::string big(64 * 1024, 'x'); TinyServer srv(io, "HTTP/1.1 200 OK\r\nContent-Length: " + std::to_string(big.size()) + "\r\n\r\n" + big); http::Options opts; opts.max_bytes = 1024; std::error_code got_ec; http::async_get(io, srv.url(), opts, [&](std::error_code ec, http::Response) { got_ec = ec; srv.stop(); }); io.run(); // Must fail rather than buffer 64 KB when told 1 KB is the limit. CHECK(static_cast(got_ec)); } OVG_TEST(HttpGetReportsNonOkStatus) { asio::io_context io; TinyServer srv(io, "HTTP/1.1 503 Service Unavailable\r\n" "Content-Length: 3\r\n\r\nnah"); std::error_code got_ec; http::Response got; http::async_get(io, srv.url(), {}, [&](std::error_code ec, http::Response r) { got_ec = ec; got = std::move(r); srv.stop(); }); io.run(); CHECK(static_cast(got_ec)); CHECK_EQ(got.status, 503); } OVG_TEST(HttpGetTimesOut) { asio::io_context io; // Accept but never answer. asio::ip::tcp::acceptor acc( io, asio::ip::tcp::endpoint(asio::ip::make_address("127.0.0.1"), 0)); acc.listen(); auto held = std::make_shared(io); acc.async_accept(*held, [held](std::error_code) {}); http::Options opts; opts.timeout = std::chrono::milliseconds(200); std::error_code got_ec; http::async_get(io, "http://127.0.0.1:" + std::to_string(acc.local_endpoint().port()), opts, [&](std::error_code ec, http::Response) { got_ec = ec; std::error_code ig; acc.close(ig); held->close(ig); }); io.run(); CHECK(static_cast(got_ec)); } OVG_TEST(HttpGetFailsOnRefusedConnection) { asio::io_context io; std::error_code got_ec; bool called = false; http::async_get(io, "http://127.0.0.1:1/", {}, [&](std::error_code ec, http::Response) { called = true; got_ec = ec; }); io.run(); CHECK(called); CHECK(static_cast(got_ec)); }