/* 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 #include 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 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(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 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 h = std::make_shared(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(start_proxy(ps.type)); ps.send_host_in_connect = flags & flag_send_host; using err = std::optional; 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(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(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); }