mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-24 12:04:40 +00:00
Send the client's credentials and proxy target on a stream request
open_stream(), and with it stream::Get() and SSEClient, wrote its own request head and left out what a buffered request gets: the Authorization from set_basic_auth() or set_bearer_token_auth(), and through a plain HTTP proxy the absolute-form target (RFC 9112 3.2.2) and Proxy-Authorization. A proxy refused such a request, and an origin that needs credentials answered 401. Move the two decisions out of write_request() and handle_request() into add_credential_headers() and proxy_target_prefix(), and use them in open_stream() too, after connecting as a buffered request does. A Digest challenge or a redirect is still handed to the caller.
This commit is contained in:
1 parent
ebcd5551fd
commit
f17d314979
5 files changed
+124
-27
No files matched your search
@@ -161,6 +161,8 @@ httplib::sse::SSEClient sse(cli, "/events", headers);
|
|||||||
sse.start();
|
sse.start();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `Client`'s `set_basic_auth()` and `set_bearer_token_auth()` apply too; an `Authorization` header passed like this takes precedence.
|
||||||
|
|
||||||
### Refreshing Auth Token on Reconnect
|
### Refreshing Auth Token on Reconnect
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -353,6 +353,10 @@ auto res = cli.Get("/api/data"); // Connection can be reused
|
|||||||
|
|
||||||
`open_stream()` and the `stream::` helpers apply the client's `set_payload_max_length()` (100MB by default, `CPPHTTPLIB_PAYLOAD_MAX_LENGTH`) to the body, both as received and after decompression. Past the limit, `read()` and `next()` stop, and the read error is `Error::ExceedMaxPayloadSize`. To download something larger, raise the limit, or call `cli.set_payload_max_length(0)` for no limit.
|
`open_stream()` and the `stream::` helpers apply the client's `set_payload_max_length()` (100MB by default, `CPPHTTPLIB_PAYLOAD_MAX_LENGTH`) to the body, both as received and after decompression. Past the limit, `read()` and `next()` stop, and the read error is `Error::ExceedMaxPayloadSize`. To download something larger, raise the limit, or call `cli.set_payload_max_length(0)` for no limit.
|
||||||
|
|
||||||
|
### Authentication and Proxies
|
||||||
|
|
||||||
|
A stream request carries the same credentials as `client.Get()`: `set_basic_auth()` and `set_bearer_token_auth()` for the origin, and the proxy's own through `set_proxy()`, over HTTP or through a CONNECT tunnel. It does not answer a Digest challenge or follow a redirect: the `401` or `3xx` response is handed to you.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Issue #2269](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request
|
- [Issue #2269](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request
|
||||||
|
|||||||
@@ -3130,6 +3130,8 @@ private:
|
|||||||
|
|
||||||
void transfer_socket_ownership_to_handle(StreamHandle &handle);
|
void transfer_socket_ownership_to_handle(StreamHandle &handle);
|
||||||
StreamHandle open_stream_(Request &req);
|
StreamHandle open_stream_(Request &req);
|
||||||
|
void add_credential_headers(Request &req);
|
||||||
|
std::string proxy_target_prefix() const;
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
public:
|
public:
|
||||||
@@ -15889,12 +15891,16 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
|
|||||||
|
|
||||||
auto &strm = *handle.stream_;
|
auto &strm = *handle.stream_;
|
||||||
|
|
||||||
|
// After connecting, as for a buffered request
|
||||||
|
add_credential_headers(req);
|
||||||
|
|
||||||
// Build the request line and headers in memory first, like write_request()
|
// Build the request line and headers in memory first, like write_request()
|
||||||
// does, so that a rejected header leaves nothing on the wire.
|
// does, so that a rejected header leaves nothing on the wire.
|
||||||
{
|
{
|
||||||
detail::BufferStream bstrm;
|
detail::BufferStream bstrm;
|
||||||
|
|
||||||
if (detail::write_request_line(bstrm, req.method, req.path) < 0) {
|
if (detail::write_request_line(bstrm, req.method,
|
||||||
|
proxy_target_prefix() + req.path) < 0) {
|
||||||
handle.error = Error::Write;
|
handle.error = Error::Write;
|
||||||
handle.response.reset();
|
handle.response.reset();
|
||||||
return handle;
|
return handle;
|
||||||
@@ -16165,6 +16171,12 @@ ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) {
|
|||||||
socket_.sock = INVALID_SOCKET;
|
socket_.sock = INVALID_SOCKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A plain HTTP proxy reads the target in absolute-form (RFC 9112 3.2.2)
|
||||||
|
inline std::string ClientImpl::proxy_target_prefix() const {
|
||||||
|
if (is_ssl() || !is_proxy_enabled_for_host(host_)) { return std::string(); }
|
||||||
|
return "http://" + detail::make_host_and_port_string(host_, port_, false);
|
||||||
|
}
|
||||||
|
|
||||||
inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
||||||
Response &res, bool close_connection,
|
Response &res, bool close_connection,
|
||||||
Error &error) {
|
Error &error) {
|
||||||
@@ -16177,11 +16189,10 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
|||||||
|
|
||||||
bool ret;
|
bool ret;
|
||||||
|
|
||||||
if (!is_ssl() && is_proxy_enabled_for_host(host_)) {
|
auto prefix = proxy_target_prefix();
|
||||||
|
if (!prefix.empty()) {
|
||||||
auto req2 = req;
|
auto req2 = req;
|
||||||
req2.path = "http://" +
|
req2.path = prefix + req.path;
|
||||||
detail::make_host_and_port_string(host_, port_, false) +
|
|
||||||
req.path;
|
|
||||||
ret = process_request(strm, req2, res, close_connection, error);
|
ret = process_request(strm, req2, res, close_connection, error);
|
||||||
req = std::move(req2);
|
req = std::move(req2);
|
||||||
req.path = req_save.path;
|
req.path = req_save.path;
|
||||||
@@ -16495,6 +16506,27 @@ inline std::string ClientImpl::make_request_target(const Request &req) const {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void ClientImpl::add_credential_headers(Request &req) {
|
||||||
|
// A CONNECT request is read by the proxy; everything sent through the tunnel
|
||||||
|
// it opens is read by the origin. Each credential goes only to its own hop.
|
||||||
|
auto is_connect = req.method == "CONNECT";
|
||||||
|
|
||||||
|
if (!is_connect) {
|
||||||
|
detail::add_authorization_header(req, basic_auth_username_,
|
||||||
|
basic_auth_password_,
|
||||||
|
bearer_token_auth_token_, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy-Authorization is only sent when the proxy reads this message —
|
||||||
|
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
||||||
|
// would leak proxy credentials to the destination server.
|
||||||
|
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
||||||
|
detail::add_authorization_header(req, proxy_basic_auth_username_,
|
||||||
|
proxy_basic_auth_password_,
|
||||||
|
proxy_bearer_token_auth_token_, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||||
bool close_connection, Error &error,
|
bool close_connection, Error &error,
|
||||||
bool skip_body, bool &rejected_locally) {
|
bool skip_body, bool &rejected_locally) {
|
||||||
@@ -16529,24 +16561,7 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A CONNECT request is read by the proxy; everything sent through the tunnel
|
add_credential_headers(req);
|
||||||
// it opens is read by the origin. Each credential goes only to its own hop.
|
|
||||||
auto is_connect = req.method == "CONNECT";
|
|
||||||
|
|
||||||
if (!is_connect) {
|
|
||||||
detail::add_authorization_header(req, basic_auth_username_,
|
|
||||||
basic_auth_password_,
|
|
||||||
bearer_token_auth_token_, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proxy-Authorization is only sent when the proxy reads this message —
|
|
||||||
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
|
||||||
// would leak proxy credentials to the destination server.
|
|
||||||
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
|
||||||
detail::add_authorization_header(req, proxy_basic_auth_username_,
|
|
||||||
proxy_basic_auth_password_,
|
|
||||||
proxy_bearer_token_auth_token_, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request line and headers
|
// Request line and headers
|
||||||
{
|
{
|
||||||
|
|||||||
+67
-4
@@ -29808,6 +29808,53 @@ inline std::unique_ptr<Client> make_client(const std::string &host,
|
|||||||
using no_proxy_test::make_client;
|
using no_proxy_test::make_client;
|
||||||
using no_proxy_test::ProxyAndTargetServers;
|
using no_proxy_test::ProxyAndTargetServers;
|
||||||
|
|
||||||
|
// A stream request carries the credentials a buffered request does
|
||||||
|
TEST(StreamRequestHeadTest, SendsTheClientsCredentials) {
|
||||||
|
no_proxy_test::ScopedServer origin;
|
||||||
|
origin.svr().Get("/x", [](const Request &req, Response &res) {
|
||||||
|
res.set_content(req.get_header_value("Authorization"), "text/plain");
|
||||||
|
});
|
||||||
|
origin.listen();
|
||||||
|
|
||||||
|
Client cli("127.0.0.1", origin.port());
|
||||||
|
cli.set_basic_auth("user", "pass");
|
||||||
|
auto handle = cli.open_stream("GET", "/x");
|
||||||
|
ASSERT_TRUE(handle.is_valid());
|
||||||
|
EXPECT_EQ("Basic dXNlcjpwYXNz", read_all(handle));
|
||||||
|
|
||||||
|
Client bearer("127.0.0.1", origin.port());
|
||||||
|
bearer.set_bearer_token_auth("tok");
|
||||||
|
auto handle2 = bearer.open_stream("GET", "/x");
|
||||||
|
ASSERT_TRUE(handle2.is_valid());
|
||||||
|
EXPECT_EQ("Bearer tok", read_all(handle2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A plain HTTP proxy reads the target in absolute-form (RFC 9112 3.2.2) and
|
||||||
|
// takes its own credentials, as for a buffered request
|
||||||
|
TEST(StreamRequestHeadTest, GoesThroughAPlainHttpProxy) {
|
||||||
|
std::string target, authz, proxy_authz;
|
||||||
|
no_proxy_test::ScopedServer proxy;
|
||||||
|
proxy.svr().Get(".*", [&](const Request &req, Response &res) {
|
||||||
|
target = req.target;
|
||||||
|
authz = req.get_header_value("Authorization");
|
||||||
|
proxy_authz = req.get_header_value("Proxy-Authorization");
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
proxy.listen();
|
||||||
|
|
||||||
|
Client cli("origin.example", 8080);
|
||||||
|
cli.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
cli.set_proxy_basic_auth("puser", "ppass");
|
||||||
|
cli.set_basic_auth("user", "pass");
|
||||||
|
auto handle = cli.open_stream("GET", "/x");
|
||||||
|
ASSERT_TRUE(handle.is_valid());
|
||||||
|
EXPECT_EQ("ok", read_all(handle));
|
||||||
|
|
||||||
|
EXPECT_EQ("http://origin.example:8080/x", target);
|
||||||
|
EXPECT_EQ("Basic dXNlcjpwYXNz", authz);
|
||||||
|
EXPECT_EQ("Basic cHVzZXI6cHBhc3M=", proxy_authz);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(NoProxyTest, ExactHostnameBypasses) {
|
TEST(NoProxyTest, ExactHostnameBypasses) {
|
||||||
ProxyAndTargetServers s;
|
ProxyAndTargetServers s;
|
||||||
auto cli = make_client("example.com", s);
|
auto cli = make_client("example.com", s);
|
||||||
@@ -30529,7 +30576,8 @@ private:
|
|||||||
void CredentialsStayWithTheirHop(
|
void CredentialsStayWithTheirHop(
|
||||||
const std::function<void(SSLClient &)> &set_credentials,
|
const std::function<void(SSLClient &)> &set_credentials,
|
||||||
const std::string &scheme,
|
const std::string &scheme,
|
||||||
const std::vector<std::string> &origin_headers = {"Authorization"}) {
|
const std::vector<std::string> &origin_headers = {"Authorization"},
|
||||||
|
bool stream = false) {
|
||||||
std::atomic<int> origin_hits{0};
|
std::atomic<int> origin_hits{0};
|
||||||
std::atomic<bool> origin_saw_proxy_authz{false};
|
std::atomic<bool> origin_saw_proxy_authz{false};
|
||||||
std::atomic<bool> origin_saw_headers{false};
|
std::atomic<bool> origin_saw_headers{false};
|
||||||
@@ -30554,9 +30602,15 @@ void CredentialsStayWithTheirHop(
|
|||||||
cli.set_proxy("127.0.0.1", proxy.port());
|
cli.set_proxy("127.0.0.1", proxy.port());
|
||||||
set_credentials(cli);
|
set_credentials(cli);
|
||||||
|
|
||||||
auto res = cli.Get("/x");
|
if (stream) {
|
||||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
auto handle = cli.open_stream("GET", "/x");
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
ASSERT_TRUE(handle.is_valid()) << "Error: " << to_string(handle.error);
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, handle.response->status);
|
||||||
|
} else {
|
||||||
|
auto res = cli.Get("/x");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
}
|
||||||
EXPECT_EQ(1, origin_hits.load());
|
EXPECT_EQ(1, origin_hits.load());
|
||||||
EXPECT_EQ(1, proxy.connect_hits());
|
EXPECT_EQ(1, proxy.connect_hits());
|
||||||
|
|
||||||
@@ -30584,6 +30638,15 @@ TEST(ProxyTunnelTest, BasicCredentialsStayWithTheirHop) {
|
|||||||
"Basic");
|
"Basic");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ProxyTunnelTest, StreamCredentialsStayWithTheirHop) {
|
||||||
|
proxy_tunnel_test::CredentialsStayWithTheirHop(
|
||||||
|
[](SSLClient &cli) {
|
||||||
|
cli.set_proxy_basic_auth("proxy-user", "proxy-pass");
|
||||||
|
cli.set_basic_auth("origin-user", "origin-pass");
|
||||||
|
},
|
||||||
|
"Basic", {"Authorization"}, true);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ProxyTunnelTest, BearerCredentialsStayWithTheirHop) {
|
TEST(ProxyTunnelTest, BearerCredentialsStayWithTheirHop) {
|
||||||
proxy_tunnel_test::CredentialsStayWithTheirHop(
|
proxy_tunnel_test::CredentialsStayWithTheirHop(
|
||||||
[](SSLClient &cli) {
|
[](SSLClient &cli) {
|
||||||
|
|||||||
@@ -361,6 +361,19 @@ TEST(KeepAliveTest, SSLWithDigest) {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The proxy needs the absolute-form target and its credentials, the origin
|
||||||
|
// its own
|
||||||
|
TEST(ProxyTest, NoSSLOpenStream) {
|
||||||
|
Client cli("httpbin");
|
||||||
|
cli.set_proxy("localhost", 3128);
|
||||||
|
cli.set_proxy_basic_auth("hello", "world");
|
||||||
|
cli.set_basic_auth("hello", "world");
|
||||||
|
|
||||||
|
auto handle = cli.open_stream("GET", "/basic-auth/hello/world");
|
||||||
|
ASSERT_TRUE(handle.response);
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, handle.response->status);
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(ProxyTest, SSLOpenStream) {
|
TEST(ProxyTest, SSLOpenStream) {
|
||||||
SSLClient cli("httpbin");
|
SSLClient cli("httpbin");
|
||||||
|
|||||||
Reference in new issue
Block a user