Преглед изворни кода

Keep a client's limits and TLS checks across redirects

A cross-host redirect is followed by a new client, which did not get the
payload limit, the max timeout, the connection timeout's microseconds or
the hostname address map. Over HTTPS it also lost the session verifier,
the server certificate verifier and a store given to set_ca_cert_store(),
and fell back to the default CAs. Copy them over. The store belongs to
the original TLS context, so pass on the CA certificates it holds as PEM.

After the redirect, the original client also answered a 401 or 407 from
the new host with its own digest credentials and sent that to its own
host, with the new host's path. Return the redirected response as it is,
since the client that followed it has already answered what it could.
yhirose пре 1 дан
родитељ
комит
2b66aa65e1
2 измењених фајлова са 247 додато и 96 уклоњено
  1. 60 16
      httplib.h
  2. 187 80
      test/test.cc

+ 60 - 16
httplib.h

@@ -3455,11 +3455,12 @@ private:
 
   std::function<SSLVerifierResponse(tls::session_t)> 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<tls::cert_t> certs;
+  tls::get_ca_certs(ctx, certs);
+
+  std::string pem;
+  for (auto cert : certs) {
+    std::vector<unsigned char> 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<SSLClient &>(*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 <typename ClientType>
 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<bool>(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<bool>(server_certificate_verifier_);
 #endif
 
   tls::session_t session = nullptr;

+ 187 - 80
test/test.cc

@@ -3801,51 +3801,59 @@ TEST(RedirectToDifferentPort, Redirect) {
   EXPECT_EQ("Hello World!", res->body);
 }
 
-static void
-TestDoNotForwardCredentialsOnRedirect(std::function<void(Client &)> 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");
-  });
-
-  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) {
+// 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<void(Client &)> &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();
+  });
+
+  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<void(Client &)> 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<void(SSLClient &)> &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