Skip to content

Commit 9d54b0c

Browse files
committed
HTTP client: CONNECT tunnel support
1 parent ebe1055 commit 9d54b0c

8 files changed

Lines changed: 1246 additions & 362 deletions

File tree

net/http/client.cpp

Lines changed: 226 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,172 @@ limitations under the License.
2121
#include <photon/common/alog-stdstring.h>
2222
#include <photon/common/iovector.h>
2323
#include <photon/common/string_view.h>
24+
#include <photon/common/estring.h>
2425
#include <photon/net/socket.h>
2526
#include <photon/net/security-context/tls-stream.h>
2627
#include <photon/net/utils.h>
2728
#include <photon/photon.h>
29+
#include "../stream_pool.h"
2830

2931
namespace photon {
3032
namespace net {
3133
namespace http {
3234
static const uint64_t kDNSCacheLife = 3600UL * 1000 * 1000;
3335
static constexpr char USERAGENT[] = "PhotonLibOS_HTTP";
3436

37+
// Creates a CONNECT tunnel through an HTTP or HTTPS proxy (RFC 7231 §4.3.6).
38+
class ConnectTunnelClient {
39+
public:
40+
ConnectTunnelClient(ISocketClient* tcp_client, bool ownership,
41+
TLSContext* tls_ctx, Resolver* resolver)
42+
: m_tcp_cli(tcp_client), m_tcp_cli_ownership(ownership),
43+
m_tls_ctx(tls_ctx), m_resolver(resolver) {}
44+
45+
~ConnectTunnelClient() {
46+
if (m_tcp_cli_ownership)
47+
delete m_tcp_cli;
48+
}
49+
50+
// Establish a CONNECT tunnel: DNS resolve -> TCP -> [TLS to proxy]
51+
// -> CONNECT handshake -> TLS to target.
52+
ISocketStream* connect(const StoredURL& proxy_url,
53+
std::string_view target_host, uint16_t target_port,
54+
const Headers* headers) {
55+
auto proxy_ip = m_resolver->resolve(proxy_url.host_no_port());
56+
if (proxy_ip.undefined()) {
57+
LOG_ERROR_RETURN(ENOENT, nullptr,
58+
"CONNECT tunnel: DNS resolve failed for proxy `",
59+
proxy_url.host_no_port());
60+
}
61+
EndPoint proxy_ep(proxy_ip, proxy_url.port());
62+
auto stream = m_tcp_cli->connect(proxy_ep);
63+
if (!stream) {
64+
m_resolver->discard_cache(proxy_url.host_no_port(), proxy_ip);
65+
LOG_ERRNO_RETURN(0, nullptr, "CONNECT tunnel: failed to connect to proxy");
66+
}
67+
68+
if (proxy_url.secure()) {
69+
stream = new_tls_stream(m_tls_ctx, stream, SecurityRole::Client, true);
70+
if (!stream)
71+
LOG_ERRNO_RETURN(0, nullptr, "CONNECT tunnel: TLS to proxy failed");
72+
auto host = proxy_url.host_no_port();
73+
tls_stream_set_hostname(stream, std::string(host).c_str());
74+
}
75+
76+
if (do_connect_handshake(stream, target_host, target_port, headers) != 0) {
77+
delete stream;
78+
return nullptr;
79+
}
80+
81+
stream = new_tls_stream(m_tls_ctx, stream, SecurityRole::Client, true);
82+
if (!stream)
83+
LOG_ERRNO_RETURN(0, nullptr, "CONNECT tunnel: TLS to target failed");
84+
tls_stream_set_hostname(stream, std::string(target_host).c_str());
85+
return stream;
86+
}
87+
88+
private:
89+
int do_connect_handshake(ISocketStream* stream,
90+
std::string_view target_host, uint16_t target_port,
91+
const Headers* headers) {
92+
char buf[4096];
93+
// CONNECT target is always host:port per RFC 7231 §4.3.6.
94+
int n = snprintf(buf, sizeof(buf),
95+
"CONNECT %.*s:%u HTTP/1.1\r\n",
96+
(int)target_host.size(), target_host.data(), target_port);
97+
if (n < 0 || n >= (int)sizeof(buf))
98+
LOG_ERROR_RETURN(ENOMEM, -1, "CONNECT request too long");
99+
// Host header is required for CONNECT (points to the target).
100+
n += snprintf(buf + n, sizeof(buf) - n, "Host: %.*s:%u\r\n",
101+
(int)target_host.size(), target_host.data(), target_port);
102+
if (n >= (int)sizeof(buf))
103+
LOG_ERROR_RETURN(ENOMEM, -1, "CONNECT request too long");
104+
// Forward proxy-specific headers (Proxy-Authorization, etc.).
105+
if (headers) {
106+
for (auto it = headers->begin(); it != headers->end(); ++it) {
107+
auto k = it.first();
108+
auto v = it.second();
109+
if ((size_t)(n + k.size() + v.size() + 4) >= sizeof(buf))
110+
LOG_ERROR_RETURN(ENOMEM, -1, "CONNECT request too long");
111+
memcpy(buf + n, k.data(), k.size()); n += k.size();
112+
buf[n++] = ':'; buf[n++] = ' ';
113+
memcpy(buf + n, v.data(), v.size()); n += v.size();
114+
buf[n++] = '\r'; buf[n++] = '\n';
115+
}
116+
}
117+
if (n + 2 >= (int)sizeof(buf))
118+
LOG_ERROR_RETURN(ENOMEM, -1, "CONNECT request too long");
119+
buf[n++] = '\r'; buf[n++] = '\n'; buf[n] = '\0';
120+
if (stream->write(buf, n) != n)
121+
LOG_ERRNO_RETURN(0, -1, "CONNECT tunnel: failed to send CONNECT request");
122+
123+
size_t total = 0;
124+
while (total < sizeof(buf) - 1) {
125+
auto rc = stream->recv(buf + total, sizeof(buf) - 1 - total);
126+
if (rc <= 0)
127+
LOG_ERRNO_RETURN(0, -1, "CONNECT tunnel: failed to read proxy response");
128+
total += rc;
129+
buf[total] = '\0';
130+
if (strstr(buf, "\r\n\r\n"))
131+
break;
132+
}
133+
134+
auto space = strchr(buf, ' ');
135+
if (!space)
136+
LOG_ERROR_RETURN(EINVAL, -1, "CONNECT tunnel: invalid proxy response");
137+
int status_code = atoi(space + 1);
138+
if (status_code < 200 || status_code >= 300) {
139+
auto end = strstr(buf, "\r\n\r\n");
140+
if (end) *end = '\0';
141+
LOG_ERROR_RETURN(EINVAL, -1,
142+
"CONNECT tunnel: proxy returned status ", status_code,
143+
", response: ", buf);
144+
}
145+
return 0;
146+
}
147+
148+
ISocketClient* m_tcp_cli;
149+
bool m_tcp_cli_ownership;
150+
TLSContext* m_tls_ctx;
151+
Resolver* m_resolver;
152+
};
153+
154+
// Caches CONNECT tunnel streams keyed by (proxy, target, auth).
155+
class TunnelPool : public StreamPoolBase<std::string> {
156+
public:
157+
TunnelPool(ConnectTunnelClient* tunnel_cli, bool ownership, uint64_t ttl_us = -1UL)
158+
: StreamPoolBase(ttl_us), m_tunnel_cli(tunnel_cli), m_ownership(ownership) {}
159+
160+
~TunnelPool() {
161+
if (m_ownership)
162+
delete m_tunnel_cli;
163+
}
164+
165+
// Establish or reuse a cached CONNECT tunnel.
166+
// headers contains proxy-specific headers (Proxy-Authorization, etc.) from call().
167+
ISocketStream* connect_tunnel(const StoredURL& proxy_url, std::string_view target_host,
168+
uint16_t target_port, const Headers* headers) {
169+
auto auth = std::string(headers ? headers->get_value("Proxy-Authorization")
170+
: std::string_view{});
171+
auto host = proxy_url.host_no_port();
172+
auto key = estring::snprintf("%.*s:%u:%c:%.*s:%u",
173+
(int)host.size(), host.data(), proxy_url.port(),
174+
proxy_url.secure() ? 's' : 'p',
175+
(int)target_host.size(), target_host.data(), target_port);
176+
if (!auth.empty()) {
177+
key += ':';
178+
key += auth;
179+
}
180+
181+
return acquire(key, [this, &proxy_url, target_host, target_port, headers]() {
182+
return m_tunnel_cli->connect(proxy_url, target_host, target_port, headers);
183+
});
184+
}
185+
186+
private:
187+
ConnectTunnelClient* m_tunnel_cli;
188+
bool m_ownership;
189+
};
35190

36191
class PooledDialer {
37192
public:
@@ -40,6 +195,7 @@ class PooledDialer {
40195
std::unique_ptr<ISocketClient> tlssock;
41196
std::unique_ptr<ISocketClient> udssock;
42197
std::unique_ptr<Resolver> resolver;
198+
std::unique_ptr<TunnelPool> tunnel_pool;
43199
photon::mutex init_mtx;
44200
bool initialized = false;
45201
bool tls_ctx_ownership = false;
@@ -67,11 +223,15 @@ class PooledDialer {
67223
tlssock.reset(new_tcp_socket_pool(tls_cli, -1, true));
68224
udssock.reset(new_uds_client());
69225
resolver.reset(new_default_resolver(kDNSCacheLife));
226+
auto tunnel_tcp = new_tcp_socket_client(src_ips.data(), src_ips.size());
227+
auto tunnel_cli = new ConnectTunnelClient(tunnel_tcp, true, tls_ctx, resolver.get());
228+
tunnel_pool.reset(new TunnelPool(tunnel_cli, true));
70229
initialized = true;
71230
return 0;
72231
}
73232

74233
void at_photon_fini() {
234+
tunnel_pool.reset();
75235
resolver.reset();
76236
udssock.reset();
77237
tlssock.reset();
@@ -91,6 +251,9 @@ class PooledDialer {
91251
}
92252

93253
ISocketStream* dial(std::string_view uds_path, uint64_t timeout = -1UL);
254+
255+
// Single entry point: choose dial method based on Operation proxy config.
256+
ISocketStream* dial(Client::Operation* op, Timeout tmo);
94257
};
95258

96259
ISocketStream* PooledDialer::dial(std::string_view host, uint16_t port, bool secure, uint64_t timeout) {
@@ -130,6 +293,36 @@ ISocketStream* PooledDialer::dial(std::string_view uds_path, uint64_t timeout) {
130293
return stream;
131294
}
132295

296+
ISocketStream* PooledDialer::dial(Client::Operation* op, Timeout tmo) {
297+
auto* active_proxy_url = op->get_active_proxy();
298+
299+
// CONNECT tunnel: proxy + HTTPS target
300+
// proxy_header is used for the CONNECT handshake (req.headers is for the target server).
301+
if (active_proxy_url && op->req.secure()) {
302+
auto* stream = tunnel_pool->connect_tunnel(
303+
*active_proxy_url, op->req.host_no_port(), op->req.port(), &op->proxy_header);
304+
if (!stream) {
305+
LOG_ERROR("CONNECT tunnel failed, target: `:`",
306+
op->req.host_no_port(), op->req.port());
307+
return nullptr;
308+
}
309+
return stream;
310+
}
311+
312+
// HTTP proxy: dial proxy endpoint directly (secure case handled by CONNECT above)
313+
if (active_proxy_url) {
314+
return dial(*active_proxy_url, tmo.timeout());
315+
}
316+
317+
// UDS
318+
if (!op->uds_path.empty()) {
319+
return dial(op->uds_path, tmo.timeout());
320+
}
321+
322+
// Direct connection
323+
return dial(op->req, tmo.timeout());
324+
}
325+
133326
constexpr uint64_t code3xx() { return 0; }
134327
template<typename...Ts>
135328
constexpr uint64_t code3xx(uint64_t x, Ts...xs)
@@ -141,19 +334,6 @@ constexpr static std::bitset<10>
141334

142335
static constexpr size_t kMinimalHeadersSize = 8 * 1024 - 1;
143336

144-
void Client::set_proxy(std::string_view proxy) {
145-
m_proxy_url.from_string(proxy);
146-
m_proxy = true;
147-
auto ui = m_proxy_url.user_passwd();
148-
if (!ui.empty()) {
149-
std::string encoded;
150-
Base64Encode(ui, encoded);
151-
m_proxy_auth = "Basic " + encoded;
152-
} else {
153-
m_proxy_auth.clear();
154-
}
155-
}
156-
157337
enum RoundtripStatus {
158338
ROUNDTRIP_SUCCESS,
159339
ROUNDTRIP_FAILED,
@@ -166,6 +346,7 @@ enum RoundtripStatus {
166346
class ClientImpl : public Client {
167347
public:
168348
CommonHeaders<> m_common_headers;
349+
CommonHeaders<> m_proxy_headers;
169350
TLSContext *m_tls_ctx;
170351
ICookieJar *m_cookie_jar;
171352
ClientImpl(ICookieJar *cookie_jar, TLSContext *tls_ctx) :
@@ -202,7 +383,8 @@ class ClientImpl : public Client {
202383
"invalid 3xx status code: ", op->status_code);
203384
}
204385

205-
if (op->req.redirect(v, location, op->enable_proxy) < 0) {
386+
bool use_proxy = op->get_active_proxy() != nullptr;
387+
if (op->req.redirect(v, location, use_proxy) < 0) {
206388
LOG_ERRNO_RETURN(0, ROUNDTRIP_FAILED, "redirect failed");
207389
}
208390
return ROUNDTRIP_REDIRECT;
@@ -213,15 +395,7 @@ class ClientImpl : public Client {
213395
if (tmo.timeout() == 0)
214396
LOG_ERROR_RETURN(ETIMEDOUT, ROUNDTRIP_FAILED, "connection timedout");
215397
auto &req = op->req;
216-
ISocketStream* s;
217-
if (op->enable_proxy && !op->proxy_url.empty())
218-
s = get_dialer().dial(op->proxy_url, tmo.timeout());
219-
else if (op->enable_proxy && !m_proxy_url.empty())
220-
s = get_dialer().dial(m_proxy_url, tmo.timeout());
221-
else if (!op->uds_path.empty())
222-
s = get_dialer().dial(op->uds_path, tmo.timeout());
223-
else
224-
s = get_dialer().dial(req, tmo.timeout());
398+
ISocketStream* s = get_dialer().dial(op, tmo);
225399
if (!s) {
226400
if (errno == ECONNREFUSED || errno == ENOENT) {
227401
LOG_ERROR_RETURN(0, ROUNDTRIP_FAST_RETRY, "connection refused")
@@ -304,10 +478,33 @@ class ClientImpl : public Client {
304478
op->req.headers.insert("User-Agent", m_user_agent.empty() ? std::string_view(USERAGENT)
305479
: std::string_view(m_user_agent));
306480
op->req.headers.insert("Connection", "keep-alive");
307-
if (op->enable_proxy && !m_proxy_auth.empty())
308-
op->req.headers.insert("Proxy-Authorization", m_proxy_auth);
309481
if (m_cookie_jar && m_cookie_jar->set_cookies_to_headers(&op->req) != 0)
310482
LOG_ERROR_RETURN(0, -1, "set_cookies_to_headers failed");
483+
484+
// Proxy header assembly: op proxy_header (already set) <- client proxy_header <- URL auth
485+
auto proxy = op->get_active_proxy();
486+
if (proxy) {
487+
op->proxy_header.merge(m_proxy_headers);
488+
if (!proxy->user_passwd().empty()) {
489+
std::string encoded;
490+
Base64Encode(proxy->user_passwd(), encoded);
491+
op->proxy_header.insert("Proxy-Authorization", "Basic " + encoded);
492+
}
493+
}
494+
495+
// rebuild request line if necessary
496+
auto need_abs = proxy && !op->req.secure();
497+
auto is_abs = what_protocol(estring_view(op->req.target())) == 1;
498+
if (need_abs != is_abs) {
499+
op->req.redirect(op->req.verb(), op->req.target(), need_abs);
500+
}
501+
502+
// For HTTP/1.1 proxy: merge proxy_header into req.headers (sent with the request).
503+
// For HTTPS proxy: proxy_header stays separate (used by CONNECT handshake only).
504+
if (proxy && !op->req.secure()) {
505+
op->req.headers.merge(op->proxy_header);
506+
}
507+
311508
Timeout tmo(std::min(op->timeout.timeout(), m_timeout));
312509
int retry = 0, followed = 0, ret = 0;
313510
uint64_t sleep_interval = 0;
@@ -346,6 +543,10 @@ class ClientImpl : public Client {
346543
CommonHeaders<>* common_headers() override {
347544
return &m_common_headers;
348545
}
546+
547+
CommonHeaders<>* proxy_headers() override {
548+
return &m_proxy_headers;
549+
}
349550
};
350551

351552
Client* new_http_client(ICookieJar *cookie_jar, TLSContext *tls_ctx) {

0 commit comments

Comments
 (0)