Files
libtorrent/test/test_http_connection.cpp
2026-06-07 12:43:35 +02:00

255 lines
8.9 KiB
C++

/*
Copyright (c) 2016, Steven Siloti
Copyright (c) 2007-2010, 2013-2022, Arvid Norberg
Copyright (c) 2016, Andrei Kurushin
Copyright (c) 2017-2018, 2020-2021, Alden Torres
Copyright (c) 2020, Paul-Louis Ageneau
All rights reserved.
You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/
#include "test.hpp"
#include "setup_transfer.hpp"
#include "test_utils.hpp"
#include "libtorrent/socket.hpp"
#include "libtorrent/aux_/socket_io.hpp" // print_endpoint
#include "libtorrent/aux_/http_connection.hpp"
#include "libtorrent/aux_/resolver.hpp"
#include "libtorrent/aux_/storage_utils.hpp"
#include "libtorrent/aux_/random.hpp"
#include <iostream>
#include <optional>
using namespace lt;
namespace {
io_context ios;
aux::resolver res(ios);
int connect_handler_called = 0;
int handler_called = 0;
int data_size = 0;
int http_status = 0;
error_code g_error_code;
char data_buffer[4000];
void print_http_header(aux::http_parser const& p)
{
std::cout << time_now_string() << " < " << p.status_code() << " " << p.message() << std::endl;
for (auto const& i : p.headers())
{
std::cout << time_now_string() << " < " << i.first << ": " << i.second << std::endl;
}
}
void http_connect_handler_test(aux::http_connection& c)
{
++connect_handler_called;
TEST_CHECK(c.socket().is_open());
error_code ec;
std::cout << time_now_string() << " connected to: "
<< aux::print_endpoint(c.socket().remote_endpoint(ec)) << std::endl;
// this is not necessarily true when using a proxy and proxying hostnames
// TEST_CHECK(c.socket().remote_endpoint(ec).address() == make_address("127.0.0.1", ec));
}
void http_handler_test(error_code const& ec, aux::http_parser const& parser
, span<char const> data, aux::http_connection&)
{
++handler_called;
data_size = int(data.size());
g_error_code = ec;
TORRENT_ASSERT(data.empty() || parser.finished());
if (parser.header_finished())
{
http_status = parser.status_code();
if (http_status == 200)
{
TEST_CHECK(span<char>(data_buffer, data.size()) == data);
}
}
print_http_header(parser);
}
void reset_globals()
{
connect_handler_called = 0;
handler_called = 0;
data_size = 0;
http_status = 0;
g_error_code = error_code();
}
void run_test(std::string const& url, int size, int status, int connected
, std::optional<error_code> ec, aux::proxy_settings const& ps
, std::string const& auth = std::string())
{
reset_globals();
std::cout << " ===== TESTING: " << url << " =====" << std::endl;
std::cout << time_now_string()
<< " expecting: size: " << size
<< " status: " << status
<< " connected: " << connected
<< " error: " << (ec?ec->message():"no error") << std::endl;
#if TORRENT_USE_SSL
aux::ssl::context ssl_ctx(aux::ssl::context::sslv23_client);
ssl_ctx.set_verify_mode(aux::ssl::context::verify_none);
#endif
std::shared_ptr<aux::http_connection> h = std::make_shared<aux::http_connection>(ios
, res, &::http_handler_test, 1024*1024, &::http_connect_handler_test
, aux::http_filter_handler()
, aux::hostname_filter_handler()
#if TORRENT_USE_SSL
, &ssl_ctx
#endif
);
h->get(url, seconds(5), &ps, 5, "test/user-agent", std::nullopt, aux::resolver_flags{}, auth);
ios.restart();
ios.run();
std::string const n = time_now_string();
std::cout << n << " connect_handler_called: " << connect_handler_called << std::endl;
std::cout << n << " handler_called: " << handler_called << std::endl;
std::cout << n << " status: " << http_status << std::endl;
std::cout << n << " size: " << data_size << std::endl;
std::cout << n << " expected-size: " << size << std::endl;
std::cout << n << " error_code: " << g_error_code.message() << std::endl;
TEST_CHECK(connect_handler_called == connected);
TEST_CHECK(handler_called == 1);
TEST_CHECK(data_size == size || size == -1);
TEST_CHECK(!ec || g_error_code == *ec);
TEST_CHECK(http_status == status || status == -1);
}
enum suite_flags_t
{
flag_chunked_encoding = 1,
flag_keepalive = 2,
flag_send_host = 4,
};
void run_suite(std::string const& protocol
, settings_pack::proxy_type_t proxy_type
, int flags = flag_keepalive)
{
aux::random_bytes(data_buffer);
ofstream("test_file").write(data_buffer, 3216);
// starting the web server will also generate test_file.gz (from test_file)
// so it has to happen after we write test_file
int port = start_web_server(protocol == "https"
, (flags & flag_chunked_encoding) != 0
, (flags & flag_keepalive) != 0);
aux::proxy_settings ps;
ps.hostname = "127.0.0.1";
ps.username = "testuser";
ps.password = "testpass";
ps.type = proxy_type;
if (ps.type != settings_pack::none)
ps.port = aux::numeric_cast<std::uint16_t>(start_proxy(ps.type));
ps.send_host_in_connect = flags & flag_send_host;
using err = std::optional<error_code>;
char url[256];
std::snprintf(url, sizeof(url), "%s://127.0.0.1:%d/", protocol.c_str(), port);
std::string url_base(url);
run_test(url_base + "relative/redirect", 3216, 200, 2, error_code(), ps);
run_test(url_base + "redirect", 3216, 200, 2, error_code(), ps);
// the actual error code for an abort caused by too many
// redirects is a bit unpredictable. under SSL for instance,
// it will be an ungraceful shutdown
run_test(url_base + "infinite_redirect", 0, 301, 6, err(), ps);
run_test(url_base + "test_file", 3216, 200, 1, error_code(), ps);
run_test(url_base + "test_file.gz", 3216, 200, 1, error_code(), ps);
run_test(url_base + "non-existing-file", -1, 404, 1, err(), ps);
run_test(url_base + "password_protected", 3216, 200, 1, error_code(), ps
, "testuser:testpass");
// same-origin redirect: the Authorization header must be forwarded so
// the redirect target (/password_protected) responds with 200
run_test(url_base + "redirect_auth", 3216, 200, 2, error_code(), ps, "testuser:testpass");
// cross-origin redirect: the Authorization header must NOT be forwarded
// to the new origin, so /password_protected responds with 401. Only
// run this without a proxy -- the redirect target uses "localhost"
// instead of "127.0.0.1" and our test proxy can't always reach it.
if (ps.type == settings_pack::none)
{
run_test(url_base + "redirect_auth_external", -1, 401, 2, err(), ps, "testuser:testpass");
}
// try a very long path
std::string path;
for (int i = 0; i < 6000; ++i)
{
path += static_cast<char>(i % 26) + 'a';
}
run_test(url_base + path, 0, 404, 1, err(), ps);
// only run the tests to handle NX_DOMAIN if we have a proper internet
// connection that doesn't inject false DNS responses (like Comcast does)
hostent* h = gethostbyname("non-existent-domain.se");
std::printf("gethostbyname(\"non-existent-domain.se\") = %p. h_errno = %d\n"
, static_cast<void*>(h), h_errno);
if (h == nullptr && h_errno == HOST_NOT_FOUND)
{
// if we have a proxy, we'll be able to connect to it, we will just get an
// error from the proxy saying it failed to connect to the final target
if (protocol == "http" && (ps.type == settings_pack::http || ps.type == settings_pack::http_pw))
run_test(protocol + "://non-existent-domain.se/non-existing-file", -1, -1, 1, err(), ps);
else
run_test(protocol + "://non-existent-domain.se/non-existing-file", -1, -1, 0, err(), ps);
}
if (ps.type != settings_pack::none)
stop_proxy(ps.port);
stop_web_server();
}
} // anonymous namespace
#if TORRENT_USE_SSL
TORRENT_TEST(no_proxy_ssl) { run_suite("https", settings_pack::none); }
TORRENT_TEST(http_ssl) { run_suite("https", settings_pack::http); }
TORRENT_TEST(http_pw_ssl) { run_suite("https", settings_pack::http_pw); }
TORRENT_TEST(socks5_proxy_ssl) { run_suite("https", settings_pack::socks5); }
TORRENT_TEST(socks5_pw_proxy_ssl) { run_suite("https", settings_pack::socks5_pw); }
// Test CONNECT requests with/without Host header for SSL
// When send_host_in_connect=false (default): CONNECT should use IP:port and Host header should contain IP:port
TORRENT_TEST(https_connect_without_host_header) { run_suite("https", settings_pack::http, flag_keepalive); }
// When send_host_in_connect=true: CONNECT should use hostname:port and Host header should contain hostname:port
TORRENT_TEST(https_connect_with_host_header) { run_suite("https", settings_pack::http, flag_keepalive | flag_send_host); }
#endif // TORRENT_USE_SSL
TORRENT_TEST(http_proxy) { run_suite("http", settings_pack::http); }
TORRENT_TEST(http_pwproxy) { run_suite("http", settings_pack::http_pw); }
TORRENT_TEST(socks5_proxy) { run_suite("http", settings_pack::socks5); }
TORRENT_TEST(socks5_pw_proxy) { run_suite("http", settings_pack::socks5_pw); }
// Test CONNECT requests with/without Host header
// When send_host_in_connect=false (default): CONNECT should use IP:port and Host header should contain IP:port
TORRENT_TEST(http_connect_without_host_header) { run_suite("http", settings_pack::http, flag_keepalive); }
// When send_host_in_connect=true: CONNECT should use hostname:port and Host header should contain hostname:port
TORRENT_TEST(http_connect_with_host_header) { run_suite("http", settings_pack::http, flag_keepalive | flag_send_host); }
TORRENT_TEST(no_keepalive)
{
run_suite("http", settings_pack::none, 0);
}