mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-24 12:04:40 +00:00
Support HTTP proxies in WebSocketClient (#2635)
WebSocketClient gets the proxy setters Client has: set_proxy(), set_proxy_basic_auth(), set_proxy_bearer_token_auth(), set_no_proxy() and, in SSL builds, set_proxy_digest_auth(). connect() opens a CONNECT tunnel for ws:// as well as wss:// (RFC 6455 4.1) and sends the proxy credentials on the CONNECT request only. A proxy that does not answer 200 is reported as Error::ProxyConnection, with its status and headers in Result.
This commit is contained in:
1 parent
7220bdb8b2
commit
94cf1b5c9a
5 files changed
+683
-221
No files matched your search
+16
-2
@@ -170,8 +170,10 @@ bool is_valid() const;
|
|||||||
// only when the handshake fully succeeded; on failure it describes what went
|
// only when the handshake fully succeeded; on failure it describes what went
|
||||||
// wrong:
|
// wrong:
|
||||||
// res.error() httplib::Error identifying the failing layer
|
// res.error() httplib::Error identifying the failing layer
|
||||||
// res.status() HTTP status of the upgrade response (-1 if none)
|
// res.status() HTTP status of the upgrade response (-1 if none),
|
||||||
// res.headers() headers of the upgrade response
|
// or of the proxy's CONNECT reply when error() is
|
||||||
|
// Error::ProxyConnection
|
||||||
|
// res.headers() headers of that same response
|
||||||
// res.ssl_error() TLS error detail (wss://, SSL builds only)
|
// res.ssl_error() TLS error detail (wss://, SSL builds only)
|
||||||
// res.ssl_backend_error() backend-specific TLS error code (SSL builds only)
|
// res.ssl_backend_error() backend-specific TLS error code (SSL builds only)
|
||||||
Result connect();
|
Result connect();
|
||||||
@@ -187,6 +189,18 @@ void close(CloseStatus status = CloseStatus::Normal,
|
|||||||
const std::string &reason = "");
|
const std::string &reason = "");
|
||||||
bool is_open() const;
|
bool is_open() const;
|
||||||
|
|
||||||
|
// Proxy - the same setters Client has. connect() opens a CONNECT tunnel
|
||||||
|
// through the proxy for ws:// as well as wss:// (RFC 6455 4.1); the proxy
|
||||||
|
// credentials are sent only on the CONNECT request, never through the tunnel.
|
||||||
|
// Hosts matching set_no_proxy() are connected to directly.
|
||||||
|
void set_proxy(const std::string &host, int port);
|
||||||
|
void set_proxy_basic_auth(const std::string &username,
|
||||||
|
const std::string &password);
|
||||||
|
void set_proxy_bearer_token_auth(const std::string &token);
|
||||||
|
void set_no_proxy(const std::vector<std::string> &patterns);
|
||||||
|
void set_proxy_digest_auth(const std::string &username,
|
||||||
|
const std::string &password); // SSL builds only
|
||||||
|
|
||||||
// Timeouts
|
// Timeouts
|
||||||
void set_read_timeout(time_t sec, time_t usec = 0);
|
void set_read_timeout(time_t sec, time_t usec = 0);
|
||||||
void set_write_timeout(time_t sec, time_t usec = 0);
|
void set_write_timeout(time_t sec, time_t usec = 0);
|
||||||
|
|||||||
@@ -69,6 +69,20 @@ Hostname matching is case-insensitive and uses a dot-boundary rule, so an entry
|
|||||||
|
|
||||||
Malformed entries are silently dropped. Port-specific entries such as `example.com:8080` are not supported (cpp-httplib's other host-keyed APIs are also keyed on hostname only).
|
Malformed entries are silently dropped. Port-specific entries such as `example.com:8080` are not supported (cpp-httplib's other host-keyed APIs are also keyed on hostname only).
|
||||||
|
|
||||||
|
## Use a proxy for WebSocket
|
||||||
|
|
||||||
|
`ws::WebSocketClient` has the same proxy setters.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient cli("wss://ws.example.com/chat");
|
||||||
|
cli.set_proxy("proxy.internal", 8080);
|
||||||
|
cli.set_proxy_basic_auth("user", "password");
|
||||||
|
|
||||||
|
auto res = cli.connect();
|
||||||
|
```
|
||||||
|
|
||||||
|
For both `ws://` and `wss://`, the client opens a tunnel with the CONNECT method and then performs the WebSocket handshake through it. `set_no_proxy()` works the same way.
|
||||||
|
|
||||||
## Read proxy settings from the environment
|
## Read proxy settings from the environment
|
||||||
|
|
||||||
cpp-httplib doesn't touch `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` on its own — the config API is always explicit, the same way `set_ca_cert_path()` is. If you'd like that behavior, read the variables in your application and feed them to `set_proxy()` and `set_no_proxy()`.
|
cpp-httplib doesn't touch `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` on its own — the config API is always explicit, the same way `set_ca_cert_path()` is. If you'd like that behavior, read the variables in your application and feed them to `set_proxy()` and `set_no_proxy()`.
|
||||||
|
|||||||
@@ -69,6 +69,20 @@ cli.set_no_proxy({"internal.corp", "10.0.0.0/8", ".dev.local"});
|
|||||||
|
|
||||||
不正な書式のエントリは黙って捨てられます。`example.com:8080`のようなポート指定エントリはサポート外です(cpp-httplibの他のホストキーAPIもホスト名のみを扱う設計のため)。
|
不正な書式のエントリは黙って捨てられます。`example.com:8080`のようなポート指定エントリはサポート外です(cpp-httplibの他のホストキーAPIもホスト名のみを扱う設計のため)。
|
||||||
|
|
||||||
|
## WebSocketでもプロキシを使う
|
||||||
|
|
||||||
|
`ws::WebSocketClient`にも同じプロキシ設定があります。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient cli("wss://ws.example.com/chat");
|
||||||
|
cli.set_proxy("proxy.internal", 8080);
|
||||||
|
cli.set_proxy_basic_auth("user", "password");
|
||||||
|
|
||||||
|
auto res = cli.connect();
|
||||||
|
```
|
||||||
|
|
||||||
|
`ws://`でも`wss://`でも、CONNECTメソッドでトンネルを張ってからWebSocketのハンドシェイクを行います。`set_no_proxy()`も同じように使えます。
|
||||||
|
|
||||||
## 環境変数からプロキシ設定を読み込む
|
## 環境変数からプロキシ設定を読み込む
|
||||||
|
|
||||||
cpp-httplib本体は`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`を読みません。`set_ca_cert_path()`と同じで、設定APIは常に明示的にしています。環境変数を反映させたい場合は、アプリ側で読んで`set_proxy()`や`set_no_proxy()`に渡してください。
|
cpp-httplib本体は`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`を読みません。`set_ca_cert_path()`と同じで、設定APIは常に明示的にしています。環境変数を反映させたい場合は、アプリ側で読んで`set_proxy()`や`set_no_proxy()`に渡してください。
|
||||||
|
|||||||
@@ -4535,7 +4535,8 @@ enum ReadResult : int { Fail = 0, Text = 1, Binary = 2, Timeout = 3 };
|
|||||||
// Result of WebSocketClient::connect(). Truthy only when the WebSocket
|
// Result of WebSocketClient::connect(). Truthy only when the WebSocket
|
||||||
// upgrade handshake fully succeeded. On failure error() identifies the
|
// upgrade handshake fully succeeded. On failure error() identifies the
|
||||||
// failing layer; status()/headers() expose the server's upgrade response
|
// failing layer; status()/headers() expose the server's upgrade response
|
||||||
// when one was received (status() is -1 otherwise).
|
// when one was received (status() is -1 otherwise), or the proxy's reply to
|
||||||
|
// CONNECT when error() is ProxyConnection.
|
||||||
class Result {
|
class Result {
|
||||||
public:
|
public:
|
||||||
Result() = default;
|
Result() = default;
|
||||||
@@ -4694,6 +4695,12 @@ public:
|
|||||||
void set_interface(const std::string &intf);
|
void set_interface(const std::string &intf);
|
||||||
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
|
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
|
||||||
|
|
||||||
|
void set_proxy(const std::string &host, int port);
|
||||||
|
void set_proxy_basic_auth(const std::string &username,
|
||||||
|
const std::string &password);
|
||||||
|
void set_proxy_bearer_token_auth(const std::string &token);
|
||||||
|
void set_no_proxy(const std::vector<std::string> &patterns);
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
struct PemMemory {
|
struct PemMemory {
|
||||||
const char *cert_pem;
|
const char *cert_pem;
|
||||||
@@ -4715,13 +4722,20 @@ public:
|
|||||||
void set_server_certificate_verifier(tls::VerifyCallback verifier);
|
void set_server_certificate_verifier(tls::VerifyCallback verifier);
|
||||||
void set_session_verifier(
|
void set_session_verifier(
|
||||||
std::function<SSLVerifierResponse(tls::session_t)> verifier);
|
std::function<SSLVerifierResponse(tls::session_t)> verifier);
|
||||||
|
void set_proxy_digest_auth(const std::string &username,
|
||||||
|
const std::string &password);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void shutdown_and_close();
|
void shutdown_and_close();
|
||||||
|
std::pair<time_t, time_t> handshake_timeout() const;
|
||||||
bool create_stream(std::unique_ptr<Stream> &strm, Error &error,
|
bool create_stream(std::unique_ptr<Stream> &strm, Error &error,
|
||||||
int &ssl_error, uint64_t &ssl_backend_error);
|
int &ssl_error, uint64_t &ssl_backend_error);
|
||||||
void prepare_default_headers(Request &req);
|
void prepare_default_headers(Request &req);
|
||||||
|
bool is_proxy_enabled() const;
|
||||||
|
bool create_and_connect_socket(Error &error);
|
||||||
|
bool send_proxy_connect(Request &req, Response &proxy_res, Error &error);
|
||||||
|
bool connect_with_proxy(Response &proxy_res, Error &error);
|
||||||
|
|
||||||
std::string host_;
|
std::string host_;
|
||||||
int port_;
|
int port_;
|
||||||
@@ -4751,6 +4765,13 @@ private:
|
|||||||
// hostname; only the connection target changes, never the identity.
|
// hostname; only the connection target changes, never the identity.
|
||||||
std::map<std::string, std::string> addr_map_;
|
std::map<std::string, std::string> addr_map_;
|
||||||
|
|
||||||
|
std::string proxy_host_;
|
||||||
|
int proxy_port_ = -1;
|
||||||
|
std::string proxy_basic_auth_username_;
|
||||||
|
std::string proxy_basic_auth_password_;
|
||||||
|
std::string proxy_bearer_token_auth_token_;
|
||||||
|
std::vector<detail::NoProxyEntry> no_proxy_entries_;
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
bool is_ssl_ = false;
|
bool is_ssl_ = false;
|
||||||
tls::ctx_t tls_ctx_ = nullptr;
|
tls::ctx_t tls_ctx_ = nullptr;
|
||||||
@@ -4763,6 +4784,8 @@ private:
|
|||||||
bool server_certificate_verification_ = true;
|
bool server_certificate_verification_ = true;
|
||||||
bool server_hostname_verification_ = true;
|
bool server_hostname_verification_ = true;
|
||||||
std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
|
std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
|
||||||
|
std::string proxy_digest_auth_username_;
|
||||||
|
std::string proxy_digest_auth_password_;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -8463,31 +8486,30 @@ struct WebSocketUpgradeResponse {
|
|||||||
std::string selected_subprotocol;
|
std::string selected_subprotocol;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reads a response's status line and headers, leaving any body unread. An
|
||||||
|
// unparsable status line is no stream error; it just leaves `status` as is.
|
||||||
|
inline bool read_response_head(Stream &strm, int &status, Headers &headers) {
|
||||||
|
const auto bufsiz = 2048;
|
||||||
|
char buf[bufsiz];
|
||||||
|
stream_line_reader line_reader(strm, buf, bufsiz);
|
||||||
|
if (!line_reader.getline()) { return false; }
|
||||||
|
|
||||||
|
std::string version;
|
||||||
|
std::string reason;
|
||||||
|
if (!parse_status_line(line_reader.ptr(), version, status, reason)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return read_headers(strm, headers);
|
||||||
|
}
|
||||||
|
|
||||||
inline bool
|
inline bool
|
||||||
read_websocket_upgrade_response(Stream &strm,
|
read_websocket_upgrade_response(Stream &strm,
|
||||||
const std::string &expected_accept,
|
const std::string &expected_accept,
|
||||||
const std::string &offered_subprotocols,
|
const std::string &offered_subprotocols,
|
||||||
WebSocketUpgradeResponse &upgrade) {
|
WebSocketUpgradeResponse &upgrade) {
|
||||||
// Read status line
|
// The headers of a rejection are kept so the caller can see why the server
|
||||||
const auto bufsiz = 2048;
|
// refused the upgrade.
|
||||||
char buf[bufsiz];
|
if (!read_response_head(strm, upgrade.status, upgrade.headers)) {
|
||||||
stream_line_reader line_reader(strm, buf, bufsiz);
|
|
||||||
if (!line_reader.getline()) {
|
|
||||||
upgrade.error = Error::Read;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string version;
|
|
||||||
std::string reason;
|
|
||||||
if (!parse_status_line(line_reader.ptr(), version, upgrade.status, reason)) {
|
|
||||||
upgrade.error = Error::WebSocketHandshake;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the headers even for a rejection so the caller can see why the
|
|
||||||
// server refused the upgrade. A non-101 response may carry a body; it is
|
|
||||||
// deliberately left unread since the caller closes the socket right away.
|
|
||||||
if (!read_headers(strm, upgrade.headers)) {
|
|
||||||
upgrade.error = Error::Read;
|
upgrade.error = Error::Read;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -10545,6 +10567,25 @@ inline bool is_host(const std::string &s) {
|
|||||||
|
|
||||||
} // namespace fields
|
} // namespace fields
|
||||||
|
|
||||||
|
// Built in memory first, like ClientImpl::write_request, so a rejected header
|
||||||
|
// never leaves a partial request on the wire.
|
||||||
|
inline bool write_request_head(Stream &strm, Request &req, Error &error) {
|
||||||
|
BufferStream bstrm;
|
||||||
|
if (write_request_line(bstrm, req.method, req.path) < 0) {
|
||||||
|
error = Error::Write;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!check_and_write_headers(bstrm, req.headers, write_headers, error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto &data = bstrm.get_buffer();
|
||||||
|
if (!write_data(strm, data.data(), data.size())) {
|
||||||
|
error = Error::Write;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||||
WebSocketUpgradeResponse &upgrade) {
|
WebSocketUpgradeResponse &upgrade) {
|
||||||
// Generate a random 16-byte Sec-WebSocket-Key (RFC 6455 Section 4.1)
|
// Generate a random 16-byte Sec-WebSocket-Key (RFC 6455 Section 4.1)
|
||||||
@@ -10562,28 +10603,7 @@ inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
|||||||
req.headers.emplace("Sec-WebSocket-Key", client_key);
|
req.headers.emplace("Sec-WebSocket-Key", client_key);
|
||||||
req.headers.emplace("Sec-WebSocket-Version", "13");
|
req.headers.emplace("Sec-WebSocket-Version", "13");
|
||||||
|
|
||||||
// Build the request in memory first, like ClientImpl::write_request does.
|
if (!write_request_head(strm, req, upgrade.error)) { return false; }
|
||||||
// Writing straight to the socket would leak a request line onto the wire
|
|
||||||
// before check_and_write_headers gets a chance to reject an invalid header,
|
|
||||||
// and would emit one small write per header.
|
|
||||||
BufferStream bstrm;
|
|
||||||
|
|
||||||
if (write_request_line(bstrm, req.method, req.path) < 0) {
|
|
||||||
upgrade.error = Error::Write;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto error = Error::Success;
|
|
||||||
if (!check_and_write_headers(bstrm, req.headers, write_headers, error)) {
|
|
||||||
upgrade.error = error;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &data = bstrm.get_buffer();
|
|
||||||
if (!write_data(strm, data.data(), data.size())) {
|
|
||||||
upgrade.error = Error::Write;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify 101 response and Sec-WebSocket-Accept header
|
// Verify 101 response and Sec-WebSocket-Accept header
|
||||||
auto expected_accept = websocket_accept_key(client_key);
|
auto expected_accept = websocket_accept_key(client_key);
|
||||||
@@ -11900,6 +11920,27 @@ make_bearer_token_authentication_header(const std::string &token,
|
|||||||
return std::make_pair(key, std::move(field));
|
return std::make_pair(key, std::move(field));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
// Basic auth when a username or password is set, otherwise a bearer token,
|
||||||
|
// unless the request already carries the header.
|
||||||
|
inline void add_authorization_header(Request &req, const std::string &username,
|
||||||
|
const std::string &password,
|
||||||
|
const std::string &token, bool is_proxy) {
|
||||||
|
if (req.has_header(is_proxy ? "Proxy-Authorization" : "Authorization")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!username.empty() || !password.empty()) {
|
||||||
|
req.headers.insert(
|
||||||
|
make_basic_authentication_header(username, password, is_proxy));
|
||||||
|
} else if (!token.empty()) {
|
||||||
|
req.headers.insert(
|
||||||
|
make_bearer_token_authentication_header(token, is_proxy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
// Request implementation
|
// Request implementation
|
||||||
inline size_t Request::get_header_value_u64(const std::string &key, size_t def,
|
inline size_t Request::get_header_value_u64(const std::string &key, size_t def,
|
||||||
size_t id) const {
|
size_t id) const {
|
||||||
@@ -12993,6 +13034,21 @@ inline bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::vector<NoProxyEntry>
|
||||||
|
parse_no_proxy_entries(const std::vector<std::string> &patterns) {
|
||||||
|
std::vector<NoProxyEntry> entries;
|
||||||
|
entries.reserve(patterns.size());
|
||||||
|
for (const auto &p : patterns) {
|
||||||
|
auto trimmed = trim_copy(p);
|
||||||
|
if (trimmed.empty()) { continue; }
|
||||||
|
NoProxyEntry entry;
|
||||||
|
if (parse_no_proxy_entry(trimmed, entry)) {
|
||||||
|
entries.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
inline NormalizedTarget normalize_target(const std::string &host) {
|
inline NormalizedTarget normalize_target(const std::string &host) {
|
||||||
NormalizedTarget t;
|
NormalizedTarget t;
|
||||||
std::string h = host;
|
std::string h = host;
|
||||||
@@ -16415,31 +16471,19 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
// it opens is read by the origin. Each credential goes only to its own hop.
|
// it opens is read by the origin. Each credential goes only to its own hop.
|
||||||
auto is_connect = req.method == "CONNECT";
|
auto is_connect = req.method == "CONNECT";
|
||||||
|
|
||||||
if (!is_connect && !req.has_header("Authorization")) {
|
if (!is_connect) {
|
||||||
if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
|
detail::add_authorization_header(req, basic_auth_username_,
|
||||||
req.headers.insert(make_basic_authentication_header(
|
basic_auth_password_,
|
||||||
basic_auth_username_, basic_auth_password_, false));
|
bearer_token_auth_token_, false);
|
||||||
} else if (!bearer_token_auth_token_.empty()) {
|
|
||||||
req.headers.insert(make_bearer_token_authentication_header(
|
|
||||||
bearer_token_auth_token_, false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy-Authorization is only sent when the proxy reads this message —
|
// Proxy-Authorization is only sent when the proxy reads this message —
|
||||||
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
||||||
// would leak proxy credentials to the destination server.
|
// would leak proxy credentials to the destination server.
|
||||||
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
||||||
if ((!proxy_basic_auth_username_.empty() ||
|
detail::add_authorization_header(req, proxy_basic_auth_username_,
|
||||||
!proxy_basic_auth_password_.empty()) &&
|
proxy_basic_auth_password_,
|
||||||
!req.has_header("Proxy-Authorization")) {
|
proxy_bearer_token_auth_token_, true);
|
||||||
req.headers.insert(make_basic_authentication_header(
|
|
||||||
proxy_basic_auth_username_, proxy_basic_auth_password_, true));
|
|
||||||
}
|
|
||||||
if (!proxy_bearer_token_auth_token_.empty() &&
|
|
||||||
!req.has_header("Proxy-Authorization")) {
|
|
||||||
req.headers.insert(make_bearer_token_authentication_header(
|
|
||||||
proxy_bearer_token_auth_token_, true));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request line and headers
|
// Request line and headers
|
||||||
@@ -17924,17 +17968,7 @@ inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void ClientImpl::set_no_proxy(const std::vector<std::string> &patterns) {
|
inline void ClientImpl::set_no_proxy(const std::vector<std::string> &patterns) {
|
||||||
std::vector<detail::NoProxyEntry> parsed;
|
no_proxy_entries_ = detail::parse_no_proxy_entries(patterns);
|
||||||
parsed.reserve(patterns.size());
|
|
||||||
for (const auto &p : patterns) {
|
|
||||||
auto trimmed = detail::trim_copy(p);
|
|
||||||
if (trimmed.empty()) { continue; }
|
|
||||||
detail::NoProxyEntry entry;
|
|
||||||
if (detail::parse_no_proxy_entry(trimmed, entry)) {
|
|
||||||
parsed.push_back(std::move(entry));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
no_proxy_entries_ = std::move(parsed);
|
|
||||||
std::lock_guard<std::mutex> guard(socket_mutex_);
|
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||||
disconnect(/*gracefully=*/true);
|
disconnect(/*gracefully=*/true);
|
||||||
}
|
}
|
||||||
@@ -23527,6 +23561,15 @@ inline void WebSocketClient::shutdown_and_close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The handshakes belong to establishing the connection, so an unset read
|
||||||
|
// timeout leaves them bounded by the connection timeout instead of forever.
|
||||||
|
inline std::pair<time_t, time_t> WebSocketClient::handshake_timeout() const {
|
||||||
|
if (read_timeout_sec_ == 0 && read_timeout_usec_ == 0) {
|
||||||
|
return {connection_timeout_sec_, connection_timeout_usec_};
|
||||||
|
}
|
||||||
|
return {read_timeout_sec_, read_timeout_usec_};
|
||||||
|
}
|
||||||
|
|
||||||
inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||||
Error &error, int &ssl_error,
|
Error &error, int &ssl_error,
|
||||||
uint64_t &ssl_backend_error) {
|
uint64_t &ssl_backend_error) {
|
||||||
@@ -23536,10 +23579,6 @@ inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
|||||||
auto unbounded = read_timeout_sec_ == 0 && read_timeout_usec_ == 0;
|
auto unbounded = read_timeout_sec_ == 0 && read_timeout_usec_ == 0;
|
||||||
time_t strm_read_sec = unbounded ? -1 : read_timeout_sec_;
|
time_t strm_read_sec = unbounded ? -1 : read_timeout_sec_;
|
||||||
time_t strm_read_usec = unbounded ? 0 : read_timeout_usec_;
|
time_t strm_read_usec = unbounded ? 0 : read_timeout_usec_;
|
||||||
// The handshake belongs to establishing the connection, so an unset read
|
|
||||||
// timeout leaves it bounded by the connection timeout instead of forever.
|
|
||||||
time_t hs_sec = unbounded ? connection_timeout_sec_ : read_timeout_sec_;
|
|
||||||
time_t hs_usec = unbounded ? connection_timeout_usec_ : read_timeout_usec_;
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
if (is_ssl_) {
|
if (is_ssl_) {
|
||||||
// A plain flag rather than SSLClient::load_certs()'s call_once: connect()
|
// A plain flag rather than SSLClient::load_certs()'s call_once: connect()
|
||||||
@@ -23557,11 +23596,12 @@ inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
|||||||
options.server_hostname_verification = server_hostname_verification_;
|
options.server_hostname_verification = server_hostname_verification_;
|
||||||
options.session_verifier = session_verifier_;
|
options.session_verifier = session_verifier_;
|
||||||
|
|
||||||
|
auto timeout = handshake_timeout();
|
||||||
detail::ClientTlsSessionError tls_error;
|
detail::ClientTlsSessionError tls_error;
|
||||||
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
|
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
|
||||||
server_certificate_verification_,
|
server_certificate_verification_,
|
||||||
hs_sec, hs_usec, &tls_error,
|
timeout.first, timeout.second,
|
||||||
options)) {
|
&tls_error, options)) {
|
||||||
error = tls_error.error;
|
error = tls_error.error;
|
||||||
ssl_error = tls_error.ssl_error;
|
ssl_error = tls_error.ssl_error;
|
||||||
ssl_backend_error = tls_error.backend_error;
|
ssl_backend_error = tls_error.backend_error;
|
||||||
@@ -23577,8 +23617,6 @@ inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
|||||||
(void)error;
|
(void)error;
|
||||||
(void)ssl_error;
|
(void)ssl_error;
|
||||||
(void)ssl_backend_error;
|
(void)ssl_backend_error;
|
||||||
(void)hs_sec;
|
|
||||||
(void)hs_usec;
|
|
||||||
#endif
|
#endif
|
||||||
strm = std::unique_ptr<Stream>(
|
strm = std::unique_ptr<Stream>(
|
||||||
new detail::SocketStream(sock_, strm_read_sec, strm_read_usec,
|
new detail::SocketStream(sock_, strm_read_sec, strm_read_usec,
|
||||||
@@ -23605,23 +23643,19 @@ inline Result WebSocketClient::connect() {
|
|||||||
if (!is_valid_) { return Result{Error::Connection, -1, Headers{}}; }
|
if (!is_valid_) { return Result{Error::Connection, -1, Headers{}}; }
|
||||||
shutdown_and_close();
|
shutdown_and_close();
|
||||||
|
|
||||||
// Check is custom IP or hostname specified for host_
|
|
||||||
std::string connect_host;
|
|
||||||
std::string ip;
|
|
||||||
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
|
||||||
|
|
||||||
auto error = Error::Success;
|
auto error = Error::Success;
|
||||||
sock_ = detail::create_client_socket(
|
if (!create_and_connect_socket(error)) {
|
||||||
connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
|
||||||
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
|
|
||||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
|
||||||
write_timeout_usec_, interface_, error);
|
|
||||||
|
|
||||||
if (sock_ == INVALID_SOCKET) {
|
|
||||||
if (error == Error::Success) { error = Error::Connection; }
|
|
||||||
return Result{error, -1, Headers{}};
|
return Result{error, -1, Headers{}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_proxy_enabled()) {
|
||||||
|
Response proxy_res;
|
||||||
|
if (!connect_with_proxy(proxy_res, error)) {
|
||||||
|
shutdown_and_close();
|
||||||
|
return Result{error, proxy_res.status, std::move(proxy_res.headers)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Stream> strm;
|
std::unique_ptr<Stream> strm;
|
||||||
auto stream_error = Error::SSLConnection;
|
auto stream_error = Error::SSLConnection;
|
||||||
int ssl_error = 0;
|
int ssl_error = 0;
|
||||||
@@ -23731,6 +23765,110 @@ inline void WebSocketClient::set_hostname_addr_map(
|
|||||||
addr_map_ = std::move(addr_map);
|
addr_map_ = std::move(addr_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool WebSocketClient::is_proxy_enabled() const {
|
||||||
|
if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
|
||||||
|
if (no_proxy_entries_.empty()) { return true; }
|
||||||
|
return !detail::host_matches_no_proxy(detail::normalize_target(host_),
|
||||||
|
no_proxy_entries_);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool WebSocketClient::create_and_connect_socket(Error &error) {
|
||||||
|
std::string connect_host;
|
||||||
|
std::string ip;
|
||||||
|
auto port = port_;
|
||||||
|
if (is_proxy_enabled()) {
|
||||||
|
connect_host = proxy_host_;
|
||||||
|
port = proxy_port_;
|
||||||
|
} else {
|
||||||
|
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
sock_ = detail::create_client_socket(
|
||||||
|
connect_host, ip, port, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||||
|
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
|
||||||
|
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||||
|
write_timeout_usec_, interface_, error);
|
||||||
|
if (sock_ != INVALID_SOCKET) { return true; }
|
||||||
|
if (error == Error::Success) { error = Error::Connection; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool WebSocketClient::send_proxy_connect(Request &req,
|
||||||
|
Response &proxy_res,
|
||||||
|
Error &error) {
|
||||||
|
auto timeout = handshake_timeout();
|
||||||
|
detail::SocketStream strm(sock_, timeout.first, timeout.second,
|
||||||
|
write_timeout_sec_, write_timeout_usec_);
|
||||||
|
|
||||||
|
if (!detail::write_request_head(strm, req, error)) { return false; }
|
||||||
|
if (!detail::read_response_head(strm, proxy_res.status, proxy_res.headers)) {
|
||||||
|
error = Error::Read;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 6455 4.1: ws:// is tunnelled with CONNECT as well, since a proxy need not
|
||||||
|
// forward an Upgrade request.
|
||||||
|
inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
|
||||||
|
Error &error) {
|
||||||
|
Request req;
|
||||||
|
req.method = "CONNECT";
|
||||||
|
req.path = detail::make_host_and_port_string_always_port(host_, port_);
|
||||||
|
req.headers.emplace("Host", req.path);
|
||||||
|
detail::add_default_user_agent_header(req);
|
||||||
|
detail::add_authorization_header(req, proxy_basic_auth_username_,
|
||||||
|
proxy_basic_auth_password_,
|
||||||
|
proxy_bearer_token_auth_token_, true);
|
||||||
|
if (!send_proxy_connect(req, proxy_res, error)) { return false; }
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
// A Digest challenge gets one retry on a fresh connection, like SSLClient.
|
||||||
|
std::map<std::string, std::string> auth;
|
||||||
|
if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407 &&
|
||||||
|
!proxy_digest_auth_username_.empty() &&
|
||||||
|
!proxy_digest_auth_password_.empty() &&
|
||||||
|
detail::parse_www_authenticate(proxy_res, auth, true)) {
|
||||||
|
proxy_res = Response();
|
||||||
|
shutdown_and_close();
|
||||||
|
if (!create_and_connect_socket(error)) { return false; }
|
||||||
|
|
||||||
|
req.headers.erase("Proxy-Authorization");
|
||||||
|
req.headers.insert(detail::make_digest_authentication_header(
|
||||||
|
req, req.path, auth, 1, detail::random_string(10),
|
||||||
|
proxy_digest_auth_username_, proxy_digest_auth_password_, true));
|
||||||
|
if (!send_proxy_connect(req, proxy_res, error)) { return false; }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (proxy_res.status != StatusCode::OK_200) {
|
||||||
|
error = Error::ProxyConnection;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WebSocketClient::set_proxy(const std::string &host, int port) {
|
||||||
|
proxy_host_ = host;
|
||||||
|
proxy_port_ = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WebSocketClient::set_proxy_basic_auth(const std::string &username,
|
||||||
|
const std::string &password) {
|
||||||
|
proxy_basic_auth_username_ = username;
|
||||||
|
proxy_basic_auth_password_ = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
WebSocketClient::set_proxy_bearer_token_auth(const std::string &token) {
|
||||||
|
proxy_bearer_token_auth_token_ = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
WebSocketClient::set_no_proxy(const std::vector<std::string> &patterns) {
|
||||||
|
no_proxy_entries_ = detail::parse_no_proxy_entries(patterns);
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
|
||||||
inline void
|
inline void
|
||||||
@@ -23781,6 +23919,13 @@ inline void WebSocketClient::enable_system_ca(bool enabled) {
|
|||||||
system_ca_mode_ = enabled ? SystemCAMode::Enabled : SystemCAMode::Disabled;
|
system_ca_mode_ = enabled ? SystemCAMode::Enabled : SystemCAMode::Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
WebSocketClient::set_proxy_digest_auth(const std::string &username,
|
||||||
|
const std::string &password) {
|
||||||
|
proxy_digest_auth_username_ = username;
|
||||||
|
proxy_digest_auth_password_ = password;
|
||||||
|
}
|
||||||
|
|
||||||
#endif // CPPHTTPLIB_SSL_ENABLED
|
#endif // CPPHTTPLIB_SSL_ENABLED
|
||||||
|
|
||||||
} // namespace ws
|
} // namespace ws
|
||||||
|
|||||||
+400
-125
@@ -29682,7 +29682,158 @@ TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) {
|
|||||||
EXPECT_EQ(1, target_hits.load());
|
EXPECT_EQ(1, target_hits.load());
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(CPPHTTPLIB_SSL_ENABLED) && !defined(_WIN32)
|
// Forward declarations (see the base64_encode note).
|
||||||
|
namespace httplib {
|
||||||
|
namespace detail {
|
||||||
|
ssize_t select_read(socket_t sock, time_t sec, time_t usec);
|
||||||
|
int shutdown_socket(socket_t sock) noexcept;
|
||||||
|
} // namespace detail
|
||||||
|
} // namespace httplib
|
||||||
|
|
||||||
|
namespace proxy_tunnel_test {
|
||||||
|
|
||||||
|
const char *const kTunnelEstablished =
|
||||||
|
"HTTP/1.1 200 Connection established\r\n\r\n";
|
||||||
|
|
||||||
|
// Answers the head of a CONNECT request; a 200 opens the tunnel.
|
||||||
|
using ConnectReply = std::function<std::string(const std::string &head)>;
|
||||||
|
|
||||||
|
// Accepts CONNECT on 127.0.0.1, answers it with `reply` (200 by default) and,
|
||||||
|
// once tunnelled, byte-forwards to 127.0.0.1:forward_port.
|
||||||
|
class ScopedConnectProxy {
|
||||||
|
public:
|
||||||
|
explicit ScopedConnectProxy(int forward_port, ConnectReply reply = nullptr)
|
||||||
|
: forward_port_(forward_port), reply_(std::move(reply)) {
|
||||||
|
listen_sock_ = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (listen_sock_ == INVALID_SOCKET) { return; }
|
||||||
|
auto a = loopback(0);
|
||||||
|
auto addr = reinterpret_cast<sockaddr *>(&a);
|
||||||
|
socklen_t alen = sizeof(a);
|
||||||
|
if (::bind(listen_sock_, addr, sizeof(a)) != 0 ||
|
||||||
|
::getsockname(listen_sock_, addr, &alen) != 0 ||
|
||||||
|
::listen(listen_sock_, 4) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
port_ = ntohs(a.sin_port);
|
||||||
|
th_ = std::thread([this] { run(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedConnectProxy() {
|
||||||
|
if (th_.joinable()) {
|
||||||
|
stop_ = true;
|
||||||
|
auto wake = connect_to(port_);
|
||||||
|
th_.join();
|
||||||
|
if (wake != INVALID_SOCKET) { detail::close_socket(wake); }
|
||||||
|
}
|
||||||
|
if (listen_sock_ != INVALID_SOCKET) { detail::close_socket(listen_sock_); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int port() const { return port_; }
|
||||||
|
int connect_hits() const { return connect_hits_.load(); }
|
||||||
|
// Head of the last CONNECT request, as the proxy saw it on the wire.
|
||||||
|
std::string connect_request() const {
|
||||||
|
std::lock_guard<std::mutex> lock(connect_request_mutex_);
|
||||||
|
return connect_request_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static sockaddr_in loopback(int port) {
|
||||||
|
sockaddr_in a{};
|
||||||
|
a.sin_family = AF_INET;
|
||||||
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
a.sin_port = htons(static_cast<unsigned short>(port));
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
static socket_t connect_to(int port) {
|
||||||
|
auto sock = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (sock == INVALID_SOCKET) { return sock; }
|
||||||
|
auto a = loopback(port);
|
||||||
|
if (::connect(sock, reinterpret_cast<sockaddr *>(&a), sizeof(a)) != 0) {
|
||||||
|
detail::close_socket(sock);
|
||||||
|
return INVALID_SOCKET;
|
||||||
|
}
|
||||||
|
return sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_all(socket_t sock, const char *data, size_t size) {
|
||||||
|
while (size > 0) {
|
||||||
|
auto n = detail::send_socket(sock, data, size, 0);
|
||||||
|
if (n <= 0) { return false; }
|
||||||
|
data += n;
|
||||||
|
size -= static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
for (;;) {
|
||||||
|
auto client = ::accept(listen_sock_, nullptr, nullptr);
|
||||||
|
if (stop_) {
|
||||||
|
if (client != INVALID_SOCKET) { detail::close_socket(client); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client == INVALID_SOCKET) { continue; }
|
||||||
|
serve(client);
|
||||||
|
detail::close_socket(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void serve(socket_t client) {
|
||||||
|
std::string head;
|
||||||
|
char buf[2048];
|
||||||
|
while (head.find("\r\n\r\n") == std::string::npos) {
|
||||||
|
if (detail::select_read(client, 5, 0) <= 0) { return; }
|
||||||
|
auto n = detail::read_socket(client, buf, sizeof(buf), 0);
|
||||||
|
if (n <= 0) { return; }
|
||||||
|
head.append(buf, static_cast<size_t>(n));
|
||||||
|
}
|
||||||
|
if (head.compare(0, 8, "CONNECT ") != 0) { return; }
|
||||||
|
connect_hits_++;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(connect_request_mutex_);
|
||||||
|
connect_request_ = head;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto res = reply_ ? reply_(head) : std::string(kTunnelEstablished);
|
||||||
|
if (!send_all(client, res.data(), res.size()) ||
|
||||||
|
res.compare(0, 12, "HTTP/1.1 200") != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto origin = connect_to(forward_port_);
|
||||||
|
if (origin == INVALID_SOCKET) { return; }
|
||||||
|
|
||||||
|
// When either side stops, shut both down so the other pump returns too.
|
||||||
|
auto pump = [&](socket_t from, socket_t to) {
|
||||||
|
char b[8192];
|
||||||
|
for (;;) {
|
||||||
|
auto n = detail::read_socket(from, b, sizeof(b), 0);
|
||||||
|
if (n <= 0 || !send_all(to, b, static_cast<size_t>(n))) { break; }
|
||||||
|
}
|
||||||
|
detail::shutdown_socket(client);
|
||||||
|
detail::shutdown_socket(origin);
|
||||||
|
};
|
||||||
|
std::thread up([&] { pump(client, origin); });
|
||||||
|
pump(origin, client);
|
||||||
|
up.join();
|
||||||
|
detail::close_socket(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
int forward_port_;
|
||||||
|
ConnectReply reply_;
|
||||||
|
socket_t listen_sock_ = INVALID_SOCKET;
|
||||||
|
int port_ = 0;
|
||||||
|
std::thread th_;
|
||||||
|
std::atomic<bool> stop_{false};
|
||||||
|
std::atomic<int> connect_hits_{0};
|
||||||
|
mutable std::mutex connect_request_mutex_;
|
||||||
|
std::string connect_request_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace proxy_tunnel_test
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
namespace proxy_tunnel_test {
|
namespace proxy_tunnel_test {
|
||||||
|
|
||||||
// SSLServer counterpart to no_proxy_test::ScopedServer.
|
// SSLServer counterpart to no_proxy_test::ScopedServer.
|
||||||
@@ -29708,130 +29859,6 @@ private:
|
|||||||
int port_ = 0;
|
int port_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Accepts CONNECT, replies 200, byte-forwards to forward_port.
|
|
||||||
class ScopedConnectProxy {
|
|
||||||
public:
|
|
||||||
explicit ScopedConnectProxy(int forward_port) : forward_port_(forward_port) {
|
|
||||||
listen_fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
|
|
||||||
if (listen_fd_ < 0) { return; }
|
|
||||||
int yes = 1;
|
|
||||||
::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
|
||||||
sockaddr_in a{};
|
|
||||||
a.sin_family = AF_INET;
|
|
||||||
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
||||||
a.sin_port = 0;
|
|
||||||
if (::bind(listen_fd_, reinterpret_cast<sockaddr *>(&a), sizeof(a)) != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socklen_t alen = sizeof(a);
|
|
||||||
if (::getsockname(listen_fd_, reinterpret_cast<sockaddr *>(&a), &alen) !=
|
|
||||||
0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
port_ = ntohs(a.sin_port);
|
|
||||||
if (::listen(listen_fd_, 1) != 0) { return; }
|
|
||||||
th_ = std::thread([this] { run(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
~ScopedConnectProxy() {
|
|
||||||
stop_ = true;
|
|
||||||
if (listen_fd_ >= 0) {
|
|
||||||
::shutdown(listen_fd_, SHUT_RDWR);
|
|
||||||
::close(listen_fd_);
|
|
||||||
}
|
|
||||||
if (th_.joinable()) { th_.join(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
int port() const { return port_; }
|
|
||||||
int connect_hits() const { return connect_hits_.load(); }
|
|
||||||
// Head of the last CONNECT request, as the proxy saw it on the wire.
|
|
||||||
std::string connect_request() const {
|
|
||||||
std::lock_guard<std::mutex> lock(connect_request_mutex_);
|
|
||||||
return connect_request_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void run() {
|
|
||||||
while (!stop_.load()) {
|
|
||||||
fd_set rfds;
|
|
||||||
FD_ZERO(&rfds);
|
|
||||||
FD_SET(listen_fd_, &rfds);
|
|
||||||
timeval tv{0, 100 * 1000};
|
|
||||||
int sel = ::select(listen_fd_ + 1, &rfds, nullptr, nullptr, &tv);
|
|
||||||
if (sel <= 0) { continue; }
|
|
||||||
|
|
||||||
int client_fd = ::accept(listen_fd_, nullptr, nullptr);
|
|
||||||
if (client_fd < 0) { continue; }
|
|
||||||
|
|
||||||
std::string req;
|
|
||||||
char buf[2048];
|
|
||||||
while (req.find("\r\n\r\n") == std::string::npos) {
|
|
||||||
ssize_t n = ::recv(client_fd, buf, sizeof(buf), 0);
|
|
||||||
if (n <= 0) { break; }
|
|
||||||
req.append(buf, static_cast<size_t>(n));
|
|
||||||
}
|
|
||||||
if (req.compare(0, 7, "CONNECT") != 0) {
|
|
||||||
::close(client_fd);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
connect_hits_++;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(connect_request_mutex_);
|
|
||||||
connect_request_ = req;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *ok = "HTTP/1.1 200 Connection established\r\n\r\n";
|
|
||||||
::send(client_fd, ok, std::strlen(ok), 0);
|
|
||||||
|
|
||||||
int origin_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
|
||||||
sockaddr_in o{};
|
|
||||||
o.sin_family = AF_INET;
|
|
||||||
o.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
||||||
o.sin_port = htons(static_cast<unsigned short>(forward_port_));
|
|
||||||
if (::connect(origin_fd, reinterpret_cast<sockaddr *>(&o), sizeof(o)) !=
|
|
||||||
0) {
|
|
||||||
::close(client_fd);
|
|
||||||
::close(origin_fd);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto forward = [](int from, int to) {
|
|
||||||
char b[8192];
|
|
||||||
for (;;) {
|
|
||||||
ssize_t n = ::recv(from, b, sizeof(b), 0);
|
|
||||||
if (n <= 0) { break; }
|
|
||||||
ssize_t off = 0;
|
|
||||||
while (off < n) {
|
|
||||||
ssize_t s = ::send(to, b + off, static_cast<size_t>(n - off), 0);
|
|
||||||
if (s <= 0) {
|
|
||||||
off = n;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
off += s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
::shutdown(to, SHUT_WR);
|
|
||||||
};
|
|
||||||
|
|
||||||
std::thread t1([&] { forward(client_fd, origin_fd); });
|
|
||||||
std::thread t2([&] { forward(origin_fd, client_fd); });
|
|
||||||
t1.join();
|
|
||||||
t2.join();
|
|
||||||
::close(client_fd);
|
|
||||||
::close(origin_fd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int forward_port_ = -1;
|
|
||||||
int listen_fd_ = -1;
|
|
||||||
int port_ = 0;
|
|
||||||
std::thread th_;
|
|
||||||
std::atomic<bool> stop_{false};
|
|
||||||
std::atomic<int> connect_hits_{0};
|
|
||||||
mutable std::mutex connect_request_mutex_;
|
|
||||||
std::string connect_request_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sends one request through the CONNECT proxy to the TLS origin with a
|
// Sends one request through the CONNECT proxy to the TLS origin with a
|
||||||
// credential configured for each hop, and checks that each credential reaches
|
// credential configured for each hop, and checks that each credential reaches
|
||||||
// only the hop it was configured for: the proxy's on the CONNECT request, the
|
// only the hop it was configured for: the proxy's on the CONNECT request, the
|
||||||
@@ -29953,3 +29980,251 @@ TEST(ProxyTunnelTest, OriginReturning407InsideTunnelDoesNotLeakProxyDigest) {
|
|||||||
EXPECT_EQ(1, proxy.connect_hits());
|
EXPECT_EQ(1, proxy.connect_hits());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace proxy_tunnel_test {
|
||||||
|
|
||||||
|
// Tunnels only a CONNECT carrying `expected` as its Proxy-Authorization;
|
||||||
|
// anything else gets a 407 with `challenge`.
|
||||||
|
ConnectReply require_proxy_auth(const std::string &expected,
|
||||||
|
const std::string &challenge) {
|
||||||
|
return [=](const std::string &head) -> std::string {
|
||||||
|
if (head.find("\r\nProxy-Authorization: " + expected + "\r\n") !=
|
||||||
|
std::string::npos) {
|
||||||
|
return kTunnelEstablished;
|
||||||
|
}
|
||||||
|
return "HTTP/1.1 407 Proxy Authentication Required\r\n"
|
||||||
|
"Proxy-Authenticate: " +
|
||||||
|
challenge + "\r\nContent-Length: 0\r\n\r\n";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reports the Authorization and Proxy-Authorization headers of the upgrade
|
||||||
|
// request, then echoes.
|
||||||
|
void report_auth_then_echo(const Request &req, ws::WebSocket &ws) {
|
||||||
|
ws.send(
|
||||||
|
"authorization:" + req.get_header_value("Authorization") +
|
||||||
|
"|proxy-authorization:" + req.get_header_value("Proxy-Authorization"));
|
||||||
|
std::string msg;
|
||||||
|
while (ws.read(msg)) {
|
||||||
|
ws.send(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A WebSocket origin on 127.0.0.1 serving report_auth_then_echo at /ws.
|
||||||
|
class ScopedWebSocketOrigin {
|
||||||
|
public:
|
||||||
|
ScopedWebSocketOrigin() {
|
||||||
|
server_.svr().WebSocket("/ws", report_auth_then_echo);
|
||||||
|
server_.listen();
|
||||||
|
}
|
||||||
|
|
||||||
|
int port() const { return server_.port(); }
|
||||||
|
// Pinned to 127.0.0.1, the only address the proxy forwards to.
|
||||||
|
std::string url() const {
|
||||||
|
return "ws://127.0.0.1:" + std::to_string(port()) + "/ws";
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
no_proxy_test::ScopedServer server_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connects through a proxy that requires `proxy_authz`, with `origin_authz`
|
||||||
|
// passed as a constructor header, and checks that each reaches only its hop.
|
||||||
|
void WebSocketCredentialsStayWithTheirHop(
|
||||||
|
const std::function<void(ws::WebSocketClient &)> &set_proxy_credentials,
|
||||||
|
const std::string &proxy_authz, const std::string &origin_authz) {
|
||||||
|
ScopedWebSocketOrigin origin;
|
||||||
|
ScopedConnectProxy proxy(
|
||||||
|
origin.port(), require_proxy_auth(proxy_authz, "Basic realm=\"proxy\""));
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(origin.url(), {{"Authorization", origin_authz}});
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
set_proxy_credentials(client);
|
||||||
|
auto res = client.connect();
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_TRUE(client.read(msg));
|
||||||
|
EXPECT_EQ("authorization:" + origin_authz + "|proxy-authorization:", msg);
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(1, proxy.connect_hits());
|
||||||
|
EXPECT_EQ(std::string::npos,
|
||||||
|
proxy.connect_request().find("\r\nAuthorization: "))
|
||||||
|
<< "origin credentials must not be sent to the proxy";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace proxy_tunnel_test
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, TunnelsThroughConnect) {
|
||||||
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(origin.url());
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
auto res = client.connect();
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_TRUE(client.read(msg));
|
||||||
|
ASSERT_TRUE(client.send("hello"));
|
||||||
|
ASSERT_TRUE(client.read(msg));
|
||||||
|
EXPECT_EQ("hello", msg);
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(1, proxy.connect_hits());
|
||||||
|
auto target = "127.0.0.1:" + std::to_string(origin.port());
|
||||||
|
auto connect_req = proxy.connect_request();
|
||||||
|
EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
|
||||||
|
EXPECT_NE(std::string::npos,
|
||||||
|
connect_req.find("\r\nHost: " + target + "\r\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, BasicCredentialsStayWithTheirHop) {
|
||||||
|
proxy_tunnel_test::WebSocketCredentialsStayWithTheirHop(
|
||||||
|
[](ws::WebSocketClient &cli) {
|
||||||
|
cli.set_proxy_basic_auth("proxy-user", "proxy-pass");
|
||||||
|
},
|
||||||
|
"Basic " + detail::base64_encode("proxy-user:proxy-pass"),
|
||||||
|
"Basic " + detail::base64_encode("origin-user:origin-pass"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, BearerCredentialsStayWithTheirHop) {
|
||||||
|
proxy_tunnel_test::WebSocketCredentialsStayWithTheirHop(
|
||||||
|
[](ws::WebSocketClient &cli) {
|
||||||
|
cli.set_proxy_bearer_token_auth("proxy-token");
|
||||||
|
},
|
||||||
|
"Bearer proxy-token", "Bearer origin-token");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, BasicCredentialsWithEmptyPasswordAreSent) {
|
||||||
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(
|
||||||
|
origin.port(), proxy_tunnel_test::require_proxy_auth(
|
||||||
|
"Basic " + detail::base64_encode("proxy-user:"),
|
||||||
|
"Basic realm=\"proxy\""));
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(origin.url());
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
client.set_proxy_basic_auth("proxy-user", "");
|
||||||
|
auto res = client.connect();
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(1, proxy.connect_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, ProxyRejectionReportsProxyConnection) {
|
||||||
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(
|
||||||
|
origin.port(), proxy_tunnel_test::require_proxy_auth(
|
||||||
|
"Bearer proxy-token", "Basic realm=\"proxy\""));
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(origin.url());
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
auto res = client.connect();
|
||||||
|
EXPECT_FALSE(res);
|
||||||
|
EXPECT_EQ(Error::ProxyConnection, res.error());
|
||||||
|
EXPECT_EQ(StatusCode::ProxyAuthenticationRequired_407, res.status());
|
||||||
|
EXPECT_EQ("Basic realm=\"proxy\"",
|
||||||
|
res.get_header_value("Proxy-Authenticate"));
|
||||||
|
EXPECT_FALSE(client.is_open());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, NoProxyBypassesProxy) {
|
||||||
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(origin.url());
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
client.set_proxy_basic_auth("proxy-user", "proxy-pass");
|
||||||
|
client.set_no_proxy({"127.0.0.1"});
|
||||||
|
auto res = client.connect();
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_TRUE(client.read(msg));
|
||||||
|
EXPECT_EQ("authorization:|proxy-authorization:", msg)
|
||||||
|
<< "proxy credentials must not reach a bypassed origin";
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(0, proxy.connect_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, ProxySettingAppliesOnReconnect) {
|
||||||
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(origin.url());
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(2, proxy.connect_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) {
|
||||||
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(
|
||||||
|
origin.port(), [](const std::string &head) -> std::string {
|
||||||
|
if (head.find(
|
||||||
|
"\r\nProxy-Authorization: Digest username=\"proxy-user\"") !=
|
||||||
|
std::string::npos) {
|
||||||
|
return proxy_tunnel_test::kTunnelEstablished;
|
||||||
|
}
|
||||||
|
return "HTTP/1.1 407 Proxy Authentication Required\r\n"
|
||||||
|
"Proxy-Authenticate: Digest realm=\"proxy\", qop=\"auth\", "
|
||||||
|
"nonce=\"abc\", algorithm=MD5\r\n"
|
||||||
|
"Content-Length: 0\r\n\r\n";
|
||||||
|
});
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(origin.url());
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
client.set_proxy_digest_auth("proxy-user", "proxy-pass");
|
||||||
|
auto res = client.connect();
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_TRUE(client.read(msg));
|
||||||
|
EXPECT_EQ("authorization:|proxy-authorization:", msg);
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(2, proxy.connect_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketProxyTest, WssTunnelsThroughConnect) {
|
||||||
|
proxy_tunnel_test::ScopedSSLServer origin;
|
||||||
|
origin.svr().WebSocket("/ws", proxy_tunnel_test::report_auth_then_echo);
|
||||||
|
origin.listen();
|
||||||
|
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(
|
||||||
|
origin.port(), proxy_tunnel_test::require_proxy_auth(
|
||||||
|
"Bearer proxy-token", "Bearer realm=\"proxy\""));
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
ws::WebSocketClient client(
|
||||||
|
"wss://127.0.0.1:" + std::to_string(origin.port()) + "/ws");
|
||||||
|
client.enable_server_certificate_verification(false);
|
||||||
|
client.set_proxy("127.0.0.1", proxy.port());
|
||||||
|
client.set_proxy_bearer_token_auth("proxy-token");
|
||||||
|
auto res = client.connect();
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_TRUE(client.read(msg));
|
||||||
|
EXPECT_EQ("authorization:|proxy-authorization:", msg)
|
||||||
|
<< "Proxy-Authorization must not be sent inside the tunnel";
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(1, proxy.connect_hits());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Reference in new issue
Block a user