diff --git a/httplib.h b/httplib.h index fdfbe0f8..7c8ce867 100644 --- a/httplib.h +++ b/httplib.h @@ -3455,11 +3455,12 @@ private: std::function session_verifier_; + // Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier + // set_server_certificate_verifier() installed there. + tls::VerifyCallback server_certificate_verifier_; + #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE bool enable_windows_cert_verification_ = true; - // Like ca_cert_store_set_, tracks what ctx_ cannot report back: whether - // set_server_certificate_verifier() installed a verifier. - bool server_certificate_verifier_set_ = false; #endif friend class ClientImpl; @@ -11026,6 +11027,26 @@ inline bool load_client_ca_config(tls::ctx_t ctx, return ret; } +inline std::string ca_certs_to_pem(tls::ctx_t ctx) { + std::vector certs; + tls::get_ca_certs(ctx, certs); + + std::string pem; + for (auto cert : certs) { + std::vector der; + if (tls::get_cert_der(cert, der)) { + auto b64 = base64_encode(std::string(der.begin(), der.end())); + pem += "-----BEGIN CERTIFICATE-----\n"; + for (size_t i = 0; i < b64.size(); i += 64) { + pem += b64.substr(i, 64) + "\n"; + } + pem += "-----END CERTIFICATE-----\n"; + } + tls::free_cert(cert); + } + return pem; +} + // The parts of session setup that only SSLClient needs, plus the handful // WebSocketClient also exposes; everything else takes the defaults, which is // what keeps the two clients on one implementation. @@ -15818,7 +15839,10 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req, if (300 < res.status && res.status < 400 && follow_location_) { req = std::move(req_save); - ret = redirect(req, res, error); + // The client that followed the redirect has answered any challenge it + // could. Retrying below would sign another host's challenge and send it + // here. + return redirect(req, res, error); } #ifdef CPPHTTPLIB_SSL_ENABLED @@ -15951,12 +15975,27 @@ inline bool ClientImpl::create_redirect_client( redirect_client.system_ca_mode_ = system_ca_mode_; // Transfer CA certificate to redirect client - if (!ca_cert_pem_.empty()) { - redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(), - ca_cert_pem_.size()); - } - if (!ca_cert_file_path_.empty()) { - redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); + redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(), + ca_cert_pem_.size()); + redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); + + if (is_ssl()) { + auto &self = static_cast(*this); + // A store given to set_ca_cert_store() lives only in this client's + // context, so pass on the CA certificates the context holds. + if (self.ca_cert_store_set_) { + auto pem = detail::ca_certs_to_pem(self.ctx_); + redirect_client.load_ca_cert_store(pem.data(), pem.size()); + } + redirect_client.set_session_verifier(self.session_verifier_); + if (self.server_certificate_verifier_) { + redirect_client.set_server_certificate_verifier( + self.server_certificate_verifier_); + } +#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE + redirect_client.enable_windows_cert_verification_ = + self.enable_windows_cert_verification_; +#endif } // Client certificates are set through constructor for SSLClient @@ -15989,9 +16028,14 @@ inline bool ClientImpl::create_redirect_client( template inline void ClientImpl::setup_redirect_client(ClientType &client) { // Copy basic settings first - client.set_connection_timeout(connection_timeout_sec_); + client.set_connection_timeout(connection_timeout_sec_, + connection_timeout_usec_); client.set_read_timeout(read_timeout_sec_, read_timeout_usec_); client.set_write_timeout(write_timeout_sec_, write_timeout_usec_); + client.set_max_timeout(max_timeout_msec_); + if (has_payload_max_length_) { + client.set_payload_max_length(payload_max_length_); + } client.set_keep_alive(keep_alive_); client.set_follow_location( true); // Enable redirects to handle multi-step redirects @@ -16029,6 +16073,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) { } // Copy network and socket settings + client.addr_map_ = addr_map_; client.set_address_family(address_family_); client.set_tcp_nodelay(tcp_nodelay_); client.set_ipv6_v6only(ipv6_v6only_); @@ -18764,10 +18809,8 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) { inline void SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) { if (!ctx_) { return; } -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE - server_certificate_verifier_set_ = static_cast(verifier); -#endif - tls::set_verify_callback(ctx_, verifier); + server_certificate_verifier_ = verifier; + tls::set_verify_callback(ctx_, std::move(verifier)); } inline void SSLClient::set_session_verifier( @@ -18830,7 +18873,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { ca_cert_dir_path_.empty() && ca_cert_pem_.empty() && !ca_cert_store_set_; // Only a verifier set through set_server_certificate_verifier() is seen // here, not one installed with tls::set_verify_callback() directly. - options.server_certificate_verifier_set = server_certificate_verifier_set_; + options.server_certificate_verifier_set = + static_cast(server_certificate_verifier_); #endif tls::session_t session = nullptr; diff --git a/test/test.cc b/test/test.cc index 2cc821ae..eda4835e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3801,51 +3801,59 @@ TEST(RedirectToDifferentPort, Redirect) { EXPECT_EQ("Hello World!", res->body); } -static void -TestDoNotForwardCredentialsOnRedirect(std::function set_auth) { - Server svr1; - std::string captured_authorization; - svr1.Get("/target", [&](const Request &req, Response &res) { - captured_authorization = req.get_header_value("Authorization"); - res.set_content("OK", "text/plain"); +// GET /redir on one server redirects to /target on another port, so the +// client follows it with a new client. +static Result +GetThroughRedirectToDifferentPort(Server::Handler target, + const std::function &setup, + const Headers &headers = Headers()) { + Server target_svr; + target_svr.Get("/target", target); + + int target_port = 0; + auto target_thread = std::thread([&]() { + target_port = target_svr.bind_to_any_port(HOST); + target_svr.listen_after_bind(); }); - int svr1_port = 0; - auto thread1 = std::thread([&]() { - svr1_port = svr1.bind_to_any_port(HOST); - svr1.listen_after_bind(); - }); - - Server svr2; - svr2.Get("/redir", [&](const Request & /*req*/, Response &res) { + Server origin_svr; + origin_svr.Get("/redir", [&](const Request &, Response &res) { res.set_redirect( - "http://localhost:" + std::to_string(svr1_port) + "/target", 302); + "http://localhost:" + std::to_string(target_port) + "/target", 302); }); - int svr2_port = 0; - auto thread2 = std::thread([&]() { - svr2_port = svr2.bind_to_any_port(HOST); - svr2.listen_after_bind(); + int origin_port = 0; + auto origin_thread = std::thread([&]() { + origin_port = origin_svr.bind_to_any_port(HOST); + origin_svr.listen_after_bind(); }); auto se = detail::scope_exit([&] { - svr2.stop(); - thread2.join(); - svr1.stop(); - thread1.join(); - ASSERT_FALSE(svr2.is_running()); - ASSERT_FALSE(svr1.is_running()); + origin_svr.stop(); + origin_thread.join(); + target_svr.stop(); + target_thread.join(); }); - svr1.wait_until_ready(); - svr2.wait_until_ready(); + target_svr.wait_until_ready(); + origin_svr.wait_until_ready(); - Client cli("localhost", svr2_port); + Client cli("localhost", origin_port); cli.set_follow_location(true); - set_auth(cli); + setup(cli); + return cli.Get("/redir", headers); +} - auto res = cli.Get("/redir"); - ASSERT_TRUE(res) << "Error: " << to_string(res.error()); - EXPECT_EQ(StatusCode::OK_200, res->status); +static void +TestDoNotForwardCredentialsOnRedirect(std::function set_auth) { + std::string captured_authorization; + auto result = GetThroughRedirectToDifferentPort( + [&](const Request &req, Response &res) { + captured_authorization = req.get_header_value("Authorization"); + res.set_content("OK", "text/plain"); + }, + set_auth); + ASSERT_TRUE(result) << "Error: " << to_string(result.error()); + EXPECT_EQ(StatusCode::OK_200, result->status); // RFC 9110: credentials MUST NOT be forwarded to a different host EXPECT_TRUE(captured_authorization.empty()); } @@ -3861,52 +3869,15 @@ TEST(RedirectToDifferentPort, DoNotForwardCredentialsBearerToken) { } TEST(RedirectToDifferentPort, DoNotForwardCookie) { - Server svr1; std::string captured_cookie; - bool target_hit = false; - svr1.Get("/target", [&](const Request &req, Response &res) { - captured_cookie = req.get_header_value("Cookie"); - target_hit = true; - res.set_content("OK", "text/plain"); - }); - - int svr1_port = 0; - auto thread1 = std::thread([&]() { - svr1_port = svr1.bind_to_any_port(HOST); - svr1.listen_after_bind(); - }); - - Server svr2; - svr2.Get("/redir", [&](const Request & /*req*/, Response &res) { - res.set_redirect( - "http://localhost:" + std::to_string(svr1_port) + "/target", 302); - }); - - int svr2_port = 0; - auto thread2 = std::thread([&]() { - svr2_port = svr2.bind_to_any_port(HOST); - svr2.listen_after_bind(); - }); - auto se = detail::scope_exit([&] { - svr2.stop(); - thread2.join(); - svr1.stop(); - thread1.join(); - ASSERT_FALSE(svr2.is_running()); - ASSERT_FALSE(svr1.is_running()); - }); - - svr1.wait_until_ready(); - svr2.wait_until_ready(); - - Client cli("localhost", svr2_port); - cli.set_follow_location(true); - - Headers headers = {{"Cookie", "session_id=SECRET; auth=PRIVATE"}}; - auto res = cli.Get("/redir", headers); - ASSERT_TRUE(res) << "Error: " << to_string(res.error()); - EXPECT_EQ(StatusCode::OK_200, res->status); - EXPECT_TRUE(target_hit); + auto result = GetThroughRedirectToDifferentPort( + [&](const Request &req, Response &res) { + captured_cookie = req.get_header_value("Cookie"); + res.set_content("OK", "text/plain"); + }, + [](Client &) {}, {{"Cookie", "session_id=SECRET; auth=PRIVATE"}}); + ASSERT_TRUE(result) << "Error: " << to_string(result.error()); + EXPECT_EQ(StatusCode::OK_200, result->status); // Cookie MUST NOT be forwarded to a different host (GHSA-22mf-w2v3-r2jv) EXPECT_TRUE(captured_cookie.empty()); } @@ -3944,6 +3915,64 @@ TEST(RedirectToSamePort, ForwardCookie) { EXPECT_EQ("session_id=SECRET", captured_cookie); } +TEST(RedirectToDifferentPort, ForwardDefaultHeadersButNotCredentials) { + std::string authorization; + std::string trace_id; + auto result = GetThroughRedirectToDifferentPort( + [&](const Request &req, Response &res) { + authorization = req.get_header_value("Authorization"); + trace_id = req.get_header_value("X-Trace-Id"); + res.set_content("OK", "text/plain"); + }, + [](Client &cli) { + cli.set_default_headers( + {{"Authorization", "Bearer SECRET"}, {"X-Trace-Id", "42"}}); + }); + ASSERT_TRUE(result) << "Error: " << to_string(result.error()); + EXPECT_EQ(StatusCode::OK_200, result->status); + // Like a header given with the request, a default one goes along unless it + // carries credentials (RFC 9110 15.4). + EXPECT_TRUE(authorization.empty()); + EXPECT_EQ("42", trace_id); +} + +TEST(RedirectToDifferentPort, KeepPayloadMaxLength) { + auto result = GetThroughRedirectToDifferentPort( + [](const Request &, Response &res) { + res.set_content(std::string(2048, 'x'), "text/plain"); + }, + [](Client &cli) { cli.set_payload_max_length(1024); }); + EXPECT_FALSE(result); +} + +TEST(RedirectToDifferentPort, KeepMaxTimeout) { + auto result = GetThroughRedirectToDifferentPort( + [](const Request &req, Response &res) { + // Answer after a second, or once the client gives up. + for (auto i = 0; i < 100 && !req.is_connection_closed(); i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + res.set_content("late", "text/plain"); + }, + [](Client &cli) { cli.set_max_timeout(std::chrono::milliseconds(200)); }); + ASSERT_FALSE(result); + EXPECT_EQ(Error::Read, result.error()); +} + +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(RedirectToDifferentPort, DoNotAnswerChallengeFromRedirectTarget) { + auto result = GetThroughRedirectToDifferentPort( + [](const Request &, Response &res) { + res.status = StatusCode::Unauthorized_401; + res.set_header("WWW-Authenticate", + "Digest realm=\"r\", nonce=\"n\", qop=\"auth\""); + }, + [](Client &cli) { cli.set_digest_auth("user", "pass"); }); + ASSERT_TRUE(result) << "Error: " << to_string(result.error()); + EXPECT_EQ(StatusCode::Unauthorized_401, result->status); +} +#endif + TEST(RedirectToDifferentPort, OverflowPortNumber) { Server svr; svr.Get("/redir", [&](const Request & /*req*/, Response &res) { @@ -16349,6 +16378,84 @@ TEST(SSLClientRedirectTest, CertFile) { ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_EQ(StatusCode::OK_200, res->status); } + +// Redirects from a server with SERVER_CERT2_FILE to one with target_cert on +// another port, so the client follows it with a new client. +static Result GetThroughTlsRedirectToDifferentPort( + const std::function &setup, + const char *target_cert = SERVER_CERT2_FILE) { + auto ssl_port = PORT + 1; + + SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); + ssl_svr1.Get("/index", [&](const Request &, Response &res) { + res.set_redirect("https://127.0.0.1:" + std::to_string(ssl_port) + + "/index"); + }); + + SSLServer ssl_svr2(target_cert, SERVER_PRIVATE_KEY_FILE); + ssl_svr2.Get("/index", [&](const Request &, Response &res) { + res.set_content("test", "text/plain"); + }); + + thread t = thread([&]() { ssl_svr1.listen("127.0.0.1", PORT); }); + thread t2 = thread([&]() { ssl_svr2.listen("127.0.0.1", ssl_port); }); + auto se = detail::scope_exit([&] { + ssl_svr2.stop(); + ssl_svr1.stop(); + t2.join(); + t.join(); + }); + + ssl_svr1.wait_until_ready(); + ssl_svr2.wait_until_ready(); + + SSLClient cli("127.0.0.1", PORT); + cli.set_follow_location(true); + cli.set_connection_timeout(30); + setup(cli); + return cli.Get("/index"); +} + +TEST(SSLClientRedirectTest, KeepSessionVerifier) { + auto calls = 0; + auto res = GetThroughTlsRedirectToDifferentPort([&](SSLClient &cli) { + cli.enable_server_certificate_verification(false); + // Accept the first server only. + cli.set_session_verifier([&](tls::session_t) { + return ++calls == 1 ? SSLVerifierResponse::NoDecisionMade + : SSLVerifierResponse::CertificateRejected; + }); + }); + EXPECT_FALSE(res); + EXPECT_EQ(2, calls); +} + +TEST(SSLClientRedirectTest, KeepServerCertificateVerifier) { + std::string pinned; + auto res = GetThroughTlsRedirectToDifferentPort( + [&](SSLClient &cli) { + cli.enable_server_certificate_verification(false); + // Pin the first certificate seen. + cli.set_server_certificate_verifier([&](const tls::VerifyContext &ctx) { + if (pinned.empty()) { pinned = ctx.serial(); } + return ctx.serial() == pinned; + }); + }, + // A certificate other than the first server's. + SERVER_CERT_FILE); + EXPECT_FALSE(res); + EXPECT_FALSE(pinned.empty()); +} + +TEST(SSLClientRedirectTest, KeepCaCertStore) { + auto res = GetThroughTlsRedirectToDifferentPort([](SSLClient &cli) { + std::string cert; + read_file(SERVER_CERT2_FILE, cert); + cli.set_ca_cert_store(tls::create_ca_store(cert.c_str(), cert.size())); + }); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); +} #endif #ifdef CPPHTTPLIB_SSL_ENABLED