@@ -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
2931namespace photon {
3032namespace net {
3133namespace http {
3234static const uint64_t kDNSCacheLife = 3600UL * 1000 * 1000 ;
3335static 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
36191class PooledDialer {
37192public:
@@ -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
96259ISocketStream* 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+
133326constexpr uint64_t code3xx () { return 0 ; }
134327template <typename ...Ts>
135328constexpr uint64_t code3xx (uint64_t x, Ts...xs)
@@ -141,19 +334,6 @@ constexpr static std::bitset<10>
141334
142335static 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-
157337enum RoundtripStatus {
158338 ROUNDTRIP_SUCCESS ,
159339 ROUNDTRIP_FAILED ,
@@ -166,6 +346,7 @@ enum RoundtripStatus {
166346class ClientImpl : public Client {
167347public:
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
351552Client* new_http_client (ICookieJar *cookie_jar, TLSContext *tls_ctx) {
0 commit comments