Skip to content

Commit 523f375

Browse files
committed
HTTP client: CONNECT tunnel support
Signed-off-by: zhuangbowei.zbw <zhuangbowei.zbw@alibaba-inc.com>
1 parent c300327 commit 523f375

7 files changed

Lines changed: 985 additions & 185 deletions

File tree

ecosystem/test/test_oss_custom_headers.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class OssCustomHeaderTest : public ::testing::Test {
7979

8080
ClientOptions make_opts() {
8181
ClientOptions opts;
82-
opts.endpoint = "oss-test.example.com";
82+
opts.endpoint = "http://oss-test.example.com";
8383
opts.bucket = "test-bucket";
8484
opts.proxy = estring().appends("http://127.0.0.1:",
8585
tcp_server->getsockname().port);

net/http/client.cpp

Lines changed: 175 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ limitations under the License.
1919
#include <algorithm>
2020
#include <random>
2121
#include <photon/common/alog-stdstring.h>
22+
#include <photon/common/estring.h>
2223
#include <photon/common/iovector.h>
2324
#include <photon/common/string_view.h>
2425
#include <photon/net/socket.h>
@@ -37,9 +38,9 @@ class PooledDialer {
3738
public:
3839
net::TLSContext* tls_ctx = nullptr;
3940
std::unique_ptr<ISocketClient> tcpsock;
40-
std::unique_ptr<ISocketClient> tlssock;
4141
std::unique_ptr<ISocketClient> udssock;
4242
std::unique_ptr<Resolver> resolver;
43+
std::unique_ptr<ISocketPool> tcp_pool;
4344
photon::mutex init_mtx;
4445
bool initialized = false;
4546
bool tls_ctx_ownership = false;
@@ -61,73 +62,174 @@ class PooledDialer {
6162
tls_ctx = new_tls_context(nullptr, nullptr, nullptr);
6263
tls_ctx->set_verify_mode(VerifyMode::PEER); // act like curl
6364
}
64-
auto tcp_cli = new_tcp_socket_client(src_ips.data(), src_ips.size());
65-
auto tls_cli = new_tls_client(tls_ctx, new_tcp_socket_client(src_ips.data(), src_ips.size()), true);
66-
tcpsock.reset(new_tcp_socket_pool(tcp_cli, -1, true));
67-
tlssock.reset(new_tcp_socket_pool(tls_cli, -1, true));
65+
tcpsock.reset(new_tcp_socket_client(src_ips.data(), src_ips.size()));
6866
udssock.reset(new_uds_client());
67+
tcp_pool.reset(new_tcp_socket_pool(nullptr, -1ULL, false));
6968
resolver.reset(new_default_resolver(kDNSCacheLife));
7069
initialized = true;
7170
return 0;
7271
}
7372

7473
void at_photon_fini() {
74+
tcp_pool.reset();
7575
resolver.reset();
7676
udssock.reset();
77-
tlssock.reset();
7877
tcpsock.reset();
7978
if (tls_ctx_ownership)
8079
delete tls_ctx;
8180
initialized = false;
8281
tls_ctx_ownership = false;
8382
}
8483

85-
ISocketStream* dial(std::string_view host, uint16_t port, bool secure,
86-
uint64_t timeout = -1ULL);
84+
// Single entry point: choose dial method based on Operation proxy config.
85+
ISocketStream* dial(Client::Operation* op, Timeout tmo);
8786

88-
template <typename T>
89-
ISocketStream* dial(const T& x, uint64_t timeout = -1ULL) {
90-
return dial(x.host_no_port(), x.port(), x.secure(), timeout);
87+
ISocketStream* dial(std::string_view host, uint16_t port, bool secure, uint64_t timeout);
88+
89+
private:
90+
// Key format for pool connection grouping:
91+
// direct/proxy: <host>:<port>:<0|1> (0=TCP, 1=TLS)
92+
// tunnel: <proxy_host>:<proxy_port>:<s|p>:<target_host>:<target_port>[:<auth>]
93+
static std::string make_key(std::string_view host, uint16_t port, bool secure) {
94+
return estring().appends(host, ":", port, ":", secure ? "1" : "0");
9195
}
9296

93-
ISocketStream* dial(std::string_view uds_path, uint64_t timeout = -1ULL);
94-
};
97+
static std::string make_tunnel_key(const StoredURL& proxy, std::string_view target_host,
98+
uint16_t target_port, const Headers* headers) {
99+
auto auth = headers ? headers->get_value("Proxy-Authorization") : std::string_view{};
100+
auto phost = proxy.host_no_port();
101+
estring key;
102+
key.appends(phost, ":", proxy.port(), ":", proxy.secure() ? "s" : "p",
103+
":", target_host, ":", target_port,
104+
estring::make_conditional_cat_list(!auth.empty(), ":", auth));
105+
return std::string(std::move(key));
106+
}
95107

96-
ISocketStream* PooledDialer::dial(std::string_view host, uint16_t port, bool secure, uint64_t timeout) {
97-
LOG_DEBUG("Dialing to `:`", host, port);
98-
auto ipaddr = resolver->resolve(host);
99-
if (ipaddr.undefined()) {
100-
LOG_ERROR_RETURN(ENOENT, nullptr, "DNS resolve failed, name = `", host)
108+
// Factory: DNS resolve -> TCP connect -> optional TLS wrap with SNI.
109+
// Discards the resolved IP from cache on connection failure.
110+
ISocketStream* make_stream(std::string_view host, uint16_t port, bool secure, uint64_t timeout) {
111+
auto ip = resolver->resolve(host);
112+
if (ip.undefined())
113+
LOG_ERROR_RETURN(ENOENT, nullptr, "DNS resolve failed, name=`", host);
114+
EndPoint ep(ip, port);
115+
tcpsock->timeout(timeout);
116+
auto stream = tcpsock->connect(ep);
117+
if (!stream) {
118+
resolver->discard_cache(host, ip);
119+
LOG_ERROR_RETURN(0, nullptr, "connection failed, ep=` host=` secure=`", ep, host, secure);
120+
}
121+
if (secure) {
122+
stream = new_tls_stream(tls_ctx, stream, SecurityRole::Client, true);
123+
if (!stream)
124+
LOG_ERRNO_RETURN(0, nullptr, "TLS handshake failed, host=`", host);
125+
tls_stream_set_hostname(stream, std::string(host).c_str());
126+
}
127+
LOG_DEBUG("Connected ` ssl:` host:`", ep, secure, host);
128+
return stream;
101129
}
102130

103-
EndPoint ep(ipaddr, port);
104-
LOG_DEBUG("Connecting ` ssl: `", ep, secure);
105-
ISocketStream *sock = nullptr;
106-
if (secure) {
107-
tlssock->timeout(timeout);
108-
sock = tlssock->connect(ep);
109-
tls_stream_set_hostname(sock, estring_view(host).extract_c_str());
110-
} else {
131+
// Factory: full CONNECT tunnel handshake sequence.
132+
// DNS resolve proxy -> TCP -> [TLS to proxy] -> CONNECT handshake -> TLS to target.
133+
ISocketStream* make_tunnel_stream(const StoredURL& proxy_url, std::string_view target_host,
134+
uint16_t target_port, const Headers* headers, uint64_t timeout) {
135+
auto proxy_ip = resolver->resolve(proxy_url.host_no_port());
136+
if (proxy_ip.undefined())
137+
LOG_ERROR_RETURN(ENOENT, nullptr, "CONNECT tunnel: DNS resolve failed for proxy `", proxy_url.host_no_port());
138+
EndPoint proxy_ep(proxy_ip, proxy_url.port());
111139
tcpsock->timeout(timeout);
112-
sock = tcpsock->connect(ep);
140+
auto stream = tcpsock->connect(proxy_ep);
141+
if (!stream) {
142+
resolver->discard_cache(proxy_url.host_no_port(), proxy_ip);
143+
LOG_ERRNO_RETURN(0, nullptr, "CONNECT tunnel: failed to connect to proxy");
144+
}
145+
stream->timeout(timeout);
146+
if (proxy_url.secure()) {
147+
auto tls = new_tls_stream(tls_ctx, stream, SecurityRole::Client, true);
148+
if (!tls) {
149+
delete stream;
150+
LOG_ERRNO_RETURN(0, nullptr, "CONNECT tunnel: TLS to proxy failed");
151+
}
152+
stream = tls;
153+
tls_stream_set_hostname(stream, std::string(proxy_url.host_no_port()).c_str());
154+
}
155+
if (do_connect_handshake(stream, target_host, target_port, headers, timeout) != 0) {
156+
delete stream;
157+
return nullptr;
158+
}
159+
auto tls = new_tls_stream(tls_ctx, stream, SecurityRole::Client, true);
160+
if (!tls) {
161+
delete stream;
162+
LOG_ERRNO_RETURN(0, nullptr, "CONNECT tunnel: TLS to target failed");
163+
}
164+
stream = tls;
165+
tls_stream_set_hostname(stream, std::string(target_host).c_str());
166+
return stream;
167+
}
168+
169+
// Send CONNECT request and verify 2xx response.
170+
int do_connect_handshake(ISocketStream* stream, std::string_view target_host,
171+
uint16_t target_port, const Headers* headers, uint64_t timeout) {
172+
char buf[4096];
173+
estring authority;
174+
authority.appends(target_host, ":", target_port);
175+
Request req(buf, (uint16_t)sizeof(buf), Verb::CONNECT, authority);
176+
if (headers) {
177+
for (auto it = headers->begin(); it != headers->end(); ++it)
178+
req.headers.insert(it.first(), it.second());
179+
}
180+
if (req.send_header(stream) < 0)
181+
LOG_ERRNO_RETURN(0, -1, "CONNECT tunnel: failed to send CONNECT request");
182+
183+
// Response needs room for receive_bytes' MAX_TRANSFER_BYTES + RESERVED_INDEX_SIZE.
184+
char rbuf[8192];
185+
Response resp(rbuf, (uint16_t)sizeof(rbuf));
186+
resp.reset(stream, false);
187+
if (resp.receive_header(timeout) != 0)
188+
LOG_ERRNO_RETURN(0, -1, "CONNECT tunnel: failed to read proxy response");
189+
auto sc = resp.status_code();
190+
if (sc < 200 || sc >= 300)
191+
LOG_ERROR_RETURN(EINVAL, -1, "CONNECT tunnel: proxy returned status `", sc);
192+
return 0;
193+
}
194+
};
195+
196+
ISocketStream* PooledDialer::dial(Client::Operation* op, Timeout tmo) {
197+
auto* proxy = op->get_active_proxy();
198+
199+
// HTTPS target + proxy: CONNECT tunnel.
200+
if (proxy && op->req.secure()) {
201+
auto key = make_tunnel_key(*proxy, op->req.host_no_port(), op->req.port(), &op->proxy_header);
202+
return tcp_pool->connect(key, [&]() -> ISocketStream* {
203+
return make_tunnel_stream(*proxy, op->req.host_no_port(), op->req.port(), &op->proxy_header, tmo.timeout());
204+
});
113205
}
114-
if (sock) {
115-
LOG_DEBUG("Connected ` ", ep, VALUE(host), VALUE(secure));
116-
return sock;
206+
207+
// HTTP proxy: connect to proxy endpoint.
208+
if (proxy) {
209+
auto key = make_key(proxy->host_no_port(), proxy->port(), proxy->secure());
210+
return tcp_pool->connect(key, [&]() -> ISocketStream* {
211+
return make_stream(proxy->host_no_port(), proxy->port(), proxy->secure(), tmo.timeout());
212+
});
117213
}
118-
LOG_ERROR("connection failed, ssl : ` ep : ` host : `", secure, ep, host);
119-
// When failed, remove resolved result from dns cache so that following retries can try
120-
// different ips.
121-
resolver->discard_cache(host, ipaddr);
122-
return nullptr;
214+
215+
// Unix domain socket.
216+
if (!op->uds_path.empty()) {
217+
udssock->timeout(tmo.timeout());
218+
return udssock->connect(op->uds_path.data());
219+
}
220+
221+
// Direct TCP/TLS connection.
222+
auto key = make_key(op->req.host_no_port(), op->req.port(), op->req.secure());
223+
return tcp_pool->connect(key, [&]() -> ISocketStream* {
224+
return make_stream(op->req.host_no_port(), op->req.port(), op->req.secure(), tmo.timeout());
225+
});
123226
}
124227

125-
ISocketStream* PooledDialer::dial(std::string_view uds_path, uint64_t timeout) {
126-
udssock->timeout(timeout);
127-
auto stream = udssock->connect(uds_path.data());
128-
if (!stream)
129-
LOG_ERRNO_RETURN(0, nullptr, "failed to dial to unix socket `", uds_path);
130-
return stream;
228+
ISocketStream* PooledDialer::dial(std::string_view host, uint16_t port, bool secure, uint64_t timeout) {
229+
auto key = make_key(host, port, secure);
230+
return tcp_pool->connect(key, [&]() -> ISocketStream* {
231+
return make_stream(host, port, secure, timeout);
232+
});
131233
}
132234

133235
constexpr uint64_t code3xx() { return 0; }
@@ -141,19 +243,6 @@ constexpr static std::bitset<10>
141243

142244
static constexpr size_t kMinimalHeadersSize = 8 * 1024 - 1;
143245

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-
157246
enum RoundtripStatus {
158247
ROUNDTRIP_SUCCESS,
159248
ROUNDTRIP_FAILED,
@@ -166,6 +255,7 @@ enum RoundtripStatus {
166255
class ClientImpl : public Client {
167256
public:
168257
CommonHeaders<> m_common_headers;
258+
CommonHeaders<> m_proxy_headers;
169259
TLSContext *m_tls_ctx;
170260
ICookieJar *m_cookie_jar;
171261
ClientImpl(ICookieJar *cookie_jar, TLSContext *tls_ctx) :
@@ -202,7 +292,7 @@ class ClientImpl : public Client {
202292
"invalid 3xx status code: ", op->status_code);
203293
}
204294

205-
if (op->req.redirect(v, location, op->enable_proxy) < 0) {
295+
if (op->req.redirect(v, location, op->get_active_proxy() != nullptr) < 0) {
206296
LOG_ERRNO_RETURN(0, ROUNDTRIP_FAILED, "redirect failed");
207297
}
208298
return ROUNDTRIP_REDIRECT;
@@ -213,15 +303,7 @@ class ClientImpl : public Client {
213303
if (tmo.timeout() == 0)
214304
LOG_ERROR_RETURN(ETIMEDOUT, ROUNDTRIP_FAILED, "connection timedout");
215305
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());
306+
ISocketStream* s = get_dialer().dial(op, tmo);
225307
if (!s) {
226308
if (errno == ECONNREFUSED || errno == ENOENT) {
227309
LOG_ERROR_RETURN(0, ROUNDTRIP_FAST_RETRY, "connection refused")
@@ -304,10 +386,33 @@ class ClientImpl : public Client {
304386
op->req.headers.insert("User-Agent", m_user_agent.empty() ? std::string_view(USERAGENT)
305387
: std::string_view(m_user_agent));
306388
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);
309389
if (m_cookie_jar && m_cookie_jar->set_cookies_to_headers(&op->req) != 0)
310390
LOG_ERROR_RETURN(0, -1, "set_cookies_to_headers failed");
391+
392+
// Proxy header assembly: op proxy_header <- client proxy_headers <- URL auth
393+
auto proxy = op->get_active_proxy();
394+
if (proxy) {
395+
op->proxy_header.merge(m_proxy_headers);
396+
if (!proxy->user_passwd().empty()) {
397+
std::string encoded;
398+
Base64Encode(proxy->user_passwd(), encoded);
399+
op->proxy_header.insert("Proxy-Authorization", "Basic " + encoded);
400+
}
401+
}
402+
403+
// Rebuild request line if necessary (absolute URI for HTTP proxy)
404+
auto need_abs = proxy && !op->req.secure();
405+
auto is_abs = what_protocol(estring_view(op->req.target())) != 0;
406+
if (need_abs != is_abs) {
407+
op->req.redirect(op->req.verb(), op->req.target(), need_abs);
408+
}
409+
410+
// For HTTP/1.1 proxy: merge proxy_header into req.headers (sent with the request).
411+
// For HTTPS proxy: proxy_header stays separate (used by CONNECT handshake only).
412+
if (proxy && !op->req.secure()) {
413+
op->req.headers.merge(op->proxy_header);
414+
}
415+
311416
Timeout tmo(std::min(op->timeout.timeout(), m_timeout));
312417
int retry = 0, followed = 0, ret = 0;
313418
uint64_t sleep_interval = 0;
@@ -346,6 +451,10 @@ class ClientImpl : public Client {
346451
CommonHeaders<>* common_headers() override {
347452
return &m_common_headers;
348453
}
454+
455+
CommonHeaders<>* proxy_headers() override {
456+
return &m_proxy_headers;
457+
}
349458
};
350459

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

0 commit comments

Comments
 (0)