diff --git a/docs/cn/HttpClient.md b/docs/cn/HttpClient.md index d67b57ed2..1eedc3403 100644 --- a/docs/cn/HttpClient.md +++ b/docs/cn/HttpClient.md @@ -21,12 +21,14 @@ class HttpClient { // 获取请求头部 const char* getHeader(const char* key); - // 设置http代理 + // 设置http代理(明文HTTP走绝对URI转发) int setHttpProxy(const char* host, int port); - // 设置https代理 + // 设置https代理(HTTPS走HTTP CONNECT隧道,与目标端到端TLS) int setHttpsProxy(const char* host, int port); // 添加不走代理 int addNoProxy(const char* host); + // 设置代理认证(http转发用Basic头,CONNECT隧道用Proxy-Authorization) + int setProxyAuth(const char* username, const char* password); // 同步发送 int send(HttpRequest* req, HttpResponse* resp); diff --git a/event/hevent.c b/event/hevent.c index cd554f285..2332e8758 100644 --- a/event/hevent.c +++ b/event/hevent.c @@ -499,8 +499,11 @@ const char* hio_get_hostname(hio_t* io) { int hio_set_proxy(hio_t* io, proxy_setting_t* setting) { if (io == NULL || setting == NULL) return -1; - // only SOCKS5 is implemented so far - if (setting->protocol != PROXY_PROTOCOL_SOCKS5) return -1; + // implemented: SOCKS5, HTTP CONNECT + if (setting->protocol != PROXY_PROTOCOL_SOCKS5 && + setting->protocol != PROXY_PROTOCOL_HTTP_CONNECT) { + return -1; + } if (io->proxy == NULL) { HV_ALLOC_SIZEOF(io->proxy); if (io->proxy == NULL) return -1; diff --git a/event/hloop.h b/event/hloop.h index cc6aec42b..ec7541f97 100644 --- a/event/hloop.h +++ b/event/hloop.h @@ -359,17 +359,19 @@ HV_EXPORT const char* hio_get_hostname(hio_t* io); // already the proxy connection). // // The setting is copied. Leave username empty for no auth, or set -// username/password for auth (SOCKS5 => RFC 1929). Only PROXY_PROTOCOL_SOCKS5 -// is implemented so far. +// username/password for auth (SOCKS5 => RFC 1929, HTTP CONNECT => Basic). +// Implemented protocols: PROXY_PROTOCOL_SOCKS5, PROXY_PROTOCOL_HTTP_CONNECT. // NOTE: set before hio_connect(). typedef enum { - PROXY_PROTOCOL_NONE = 0, - PROXY_PROTOCOL_SOCKS5 = 1, + PROXY_PROTOCOL_NONE = 0, + PROXY_PROTOCOL_SOCKS5 = 1, + PROXY_PROTOCOL_HTTP_CONNECT = 2, // HTTP CONNECT tunnel (RFC 7231 4.3.6) } proxy_protocol_e; typedef struct proxy_setting_s { int protocol; // proxy_protocol_e - char proxy_host[256]; // proxy host (SOCKS5: unused, socket is the proxy) + char proxy_host[256]; // proxy host (unused by the io layer: the socket + // is already the proxy connection; kept for ref) int proxy_port; char target_host[256]; // final target the proxy should CONNECT to int target_port; diff --git a/event/nio.c b/event/nio.c index 202e14519..0366bf4ca 100644 --- a/event/nio.c +++ b/event/nio.c @@ -263,9 +263,9 @@ enum socks5_state_e { static void socks5_handshake(hio_t* io); -static void socks5_fail(hio_t* io) { +static void proxy_fail(hio_t* io) { if (io->error == 0) io->error = ERR_CONNECT; - hlogw("connfd=%d socks5 handshake error", io->fd); + hlogw("connfd=%d proxy handshake error", io->fd); hio_close(io); } @@ -277,16 +277,18 @@ static void socks5_expect(hio_t* io, int state, int want) { s5->want = want; } -// Raw handshake send. The SOCKS5 handshake runs immediately after the TCP +// Raw handshake send. The proxy handshake runs immediately after the TCP // connection to the proxy is established, when the socket send buffer is empty -// and the messages are tiny (<= 513 bytes), so a short write is not expected. +// and the message is tiny (SOCKS5 <= 513 bytes; HTTP CONNECT < ~1.3KB), far +// smaller than the default send buffer, so a single send() transfers it all. // We deliberately do NOT use hio_write() here: it would invoke the upper-layer // write_cb (leaking handshake bytes, including credentials, to the application // before onConnection), dispatch to hssl_write() with a not-yet-created SSL // handle for a TLS target, and enqueue on EAGAIN via hio_add() which would -// clobber the handshake read handler. A short write or error is treated as -// fatal and closes the connection. -static int socks5_send(hio_t* io, const void* buf, int len) { +// clobber the handshake read handler (io has a single cb slot). A short write +// cannot happen here in practice; if it somehow does, it is treated as fatal +// (return -1) rather than blocking the event loop. +static int proxy_send(hio_t* io, const void* buf, int len) { int flag = 0; #ifdef MSG_NOSIGNAL flag |= MSG_NOSIGNAL; @@ -300,15 +302,15 @@ static void socks5_send_connect(hio_t* io) { proxy_conn_t* s5 = io->proxy; unsigned char buf[300]; int n = socks5_build_connect_request(s5, buf); - if (n < 0) { socks5_fail(io); return; } - if (socks5_send(io, buf, n) != 0) { socks5_fail(io); return; } + if (n < 0) { proxy_fail(io); return; } + if (proxy_send(io, buf, n) != 0) { proxy_fail(io); return; } socks5_expect(io, S5_RECV_REPLY_HEAD, 4); } // hand off the established proxy tunnel to the upper layer: stop the handshake // read handler, then run the SSL handshake / connect_cb. io->read_cb was never // touched, so the upper-layer Channel read callback stays intact. -static void socks5_established(hio_t* io) { +static void proxy_established(hio_t* io) { hio_del(io, HV_READ); nio_connect_established(io); } @@ -321,29 +323,29 @@ static void socks5_dispatch(hio_t* io) { switch (s5->state) { case S5_RECV_METHOD: // VER METHOD - if (buf[0] != SOCKS5_VERSION) { socks5_fail(io); return; } + if (buf[0] != SOCKS5_VERSION) { proxy_fail(io); return; } if (buf[1] == SOCKS5_AUTH_NONE) { socks5_send_connect(io); } else if (buf[1] == SOCKS5_AUTH_USERPASS && s5->setting.username[0]) { unsigned char req[640]; int n = socks5_build_auth_request(s5, req); - if (socks5_send(io, req, n) != 0) { socks5_fail(io); return; } + if (proxy_send(io, req, n) != 0) { proxy_fail(io); return; } socks5_expect(io, S5_RECV_AUTH, 2); } else { - socks5_fail(io); // no acceptable method + proxy_fail(io); // no acceptable method } return; case S5_RECV_AUTH: // VER STATUS (0 == success) - if (buf[1] != 0x00) { socks5_fail(io); return; } + if (buf[1] != 0x00) { proxy_fail(io); return; } socks5_send_connect(io); return; case S5_RECV_REPLY_HEAD: { // VER REP RSV ATYP - if (buf[0] != SOCKS5_VERSION) { socks5_fail(io); return; } - if (buf[1] != SOCKS5_REP_SUCCESS) { io->error = ERR_CONNECT; socks5_fail(io); return; } + if (buf[0] != SOCKS5_VERSION) { proxy_fail(io); return; } + if (buf[1] != SOCKS5_REP_SUCCESS) { io->error = ERR_CONNECT; proxy_fail(io); return; } unsigned char atyp = buf[3]; if (atyp == SOCKS5_ATYP_IPV4) { socks5_expect(io, S5_RECV_REPLY_ADDR, 4 + 2); // addr + port @@ -354,14 +356,14 @@ static void socks5_dispatch(hio_t* io) { // first requiring the length byte. socks5_expect(io, S5_RECV_REPLY_DADDR, 1); } else { - socks5_fail(io); + proxy_fail(io); } return; } case S5_RECV_REPLY_ADDR: // bound addr+port consumed; tunnel is up - socks5_established(io); + proxy_established(io); return; case S5_RECV_REPLY_DADDR: @@ -372,11 +374,11 @@ static void socks5_dispatch(hio_t* io) { socks5_expect(io, S5_RECV_REPLY_DADDR, dlen + 2); return; } - socks5_established(io); + proxy_established(io); return; default: - socks5_fail(io); + proxy_fail(io); return; } } @@ -387,14 +389,14 @@ static void socks5_handshake(hio_t* io) { proxy_conn_t* s5 = io->proxy; while (s5->rlen < s5->want) { int need = s5->want - s5->rlen; - if (s5->want > (int)sizeof(s5->rbuf)) { socks5_fail(io); return; } + if (s5->want > (int)sizeof(s5->rbuf)) { proxy_fail(io); return; } int n = recv(io->fd, (char*)s5->rbuf + s5->rlen, need, 0); - if (n == 0) { socks5_fail(io); return; } // peer closed + if (n == 0) { proxy_fail(io); return; } // peer closed if (n < 0) { int err = socket_errno(); if (err == EAGAIN || err == EINTR) return; // wait for more io->error = err; - socks5_fail(io); + proxy_fail(io); return; } s5->rlen += n; @@ -407,17 +409,100 @@ static void socks5_handshake_start(hio_t* io) { proxy_conn_t* s5 = io->proxy; unsigned char buf[8]; int n = socks5_build_method_request(s5, buf); - if (socks5_send(io, buf, n) != 0) { socks5_fail(io); return; } + if (proxy_send(io, buf, n) != 0) { proxy_fail(io); return; } socks5_expect(io, S5_RECV_METHOD, 2); hio_add(io, socks5_handshake, HV_READ); } -// Dispatch the proxy handshake by protocol (only SOCKS5 implemented so far). +// HTTP CONNECT handshake (RFC 7231 4.3.6): send a CONNECT request, then read +// response headers until the blank line "\r\n\r\n". A 2xx status establishes +// the tunnel. Like the SOCKS5 handshake this uses a dedicated recv() via +// hio_add (never touches io->read_cb) and is robust to fragmentation. +// +// CONNECT responses carry no body, but a server-first origin protocol (SMTP, +// IMAP, FTP...) may send its greeting immediately after the tunnel opens, so +// those bytes can arrive in the same segment as the response headers. To avoid +// swallowing them, we MSG_PEEK to locate the header terminator, then drain +// EXACTLY the header bytes with a real recv(); anything after "\r\n\r\n" stays +// in the socket for the upper-layer read path. +static void http_connect_handshake(hio_t* io) { + proxy_conn_t* p = io->proxy; + for (;;) { + int cap = (int)sizeof(p->rbuf) - p->rlen; + if (cap <= 0) { proxy_fail(io); return; } // headers too large + // peek (non-destructive): inspect what is available without consuming. + int n = recv(io->fd, (char*)p->rbuf + p->rlen, cap, MSG_PEEK); + if (n == 0) { proxy_fail(io); return; } // peer closed + if (n < 0) { + int err = socket_errno(); + if (err == EAGAIN || err == EINTR) return; // wait for more + io->error = err; + proxy_fail(io); + return; + } + int have = p->rlen + n; + // search for "\r\n\r\n" in the peeked window (rescan from a safe offset) + int start = p->rlen >= 3 ? p->rlen - 3 : 0; + int term = -1; + for (int i = start + 3; i < have; ++i) { + if (p->rbuf[i-3]=='\r' && p->rbuf[i-2]=='\n' && + p->rbuf[i-1]=='\r' && p->rbuf[i]=='\n') { term = i; break; } + } + if (term < 0) { + // no full header yet: consume the peeked bytes into the accumulator + // (they are all header bytes) and keep reading. + int got = recv(io->fd, (char*)p->rbuf + p->rlen, n, 0); + if (got <= 0) { proxy_fail(io); return; } + p->rlen += got; + continue; + } + // full header present. Drain exactly up to and including the terminator, + // leaving any trailing tunnel/greeting bytes in the socket. + int header_len = term + 1; // bytes from socket start + int to_drain = header_len - p->rlen; // not yet consumed + if (to_drain > 0) { + int got = recv(io->fd, (char*)p->rbuf + p->rlen, to_drain, 0); + if (got != to_drain) { proxy_fail(io); return; } + p->rlen += got; + } + // parse status line: "HTTP/1.x SP CODE SP ..." + int code = 0; + char* sp = (char*)memchr(p->rbuf, ' ', p->rlen); + if (sp) code = atoi(sp + 1); + if (code >= 200 && code < 300) { + proxy_established(io); + } else { + hlogw("connfd=%d http proxy CONNECT failed: %d", io->fd, code); + io->error = ERR_CONNECT; + proxy_fail(io); + } + return; + } +} + +// Kick off the HTTP CONNECT handshake once the TCP connection to the proxy is up. +static void http_connect_start(hio_t* io) { + proxy_conn_t* p = io->proxy; + // Max request: "CONNECT " + authority(<=262) + " HTTP/1.1\r\nHost: " + + // authority + "\r\nProxy-Authorization: Basic " + base64(255:255)=~684 + + // "\r\n\r\n" ~= 1.3KB. 2048 leaves headroom. + char buf[2048]; + int n = http_connect_build_request(p, buf, (int)sizeof(buf)); + if (n < 0) { proxy_fail(io); return; } + if (proxy_send(io, buf, n) != 0) { proxy_fail(io); return; } + p->rlen = 0; + hio_add(io, http_connect_handshake, HV_READ); +} + +// Dispatch the proxy handshake by protocol. static void proxy_handshake_start(hio_t* io) { switch (io->proxy->setting.protocol) { case PROXY_PROTOCOL_SOCKS5: socks5_handshake_start(io); return; + case PROXY_PROTOCOL_HTTP_CONNECT: + http_connect_start(io); + return; default: io->error = ERR_INVALID_PARAM; hio_close(io); diff --git a/event/socks5.c b/event/socks5.c index c924e7b69..5ab7a0026 100644 --- a/event/socks5.c +++ b/event/socks5.c @@ -1,9 +1,43 @@ #include "socks5.h" #include +#include #include "hsocket.h" // is_ipv4 / is_ipv6 / inet_pton via hplatform +// Minimal base64 encoder for the HTTP CONNECT Proxy-Authorization header. +// NOTE: implemented locally (not via util/base64.h) because the event layer +// must not depend on util/ (core builds only add -I. -Ibase -Issl -Ievent). +// Writes ceil(len/3)*4 bytes to out (no NUL terminator); returns bytes written. +static int socks5_base64_encode(const unsigned char* in, int len, char* out) { + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + int n = 0, i = 0; + while (i + 3 <= len) { + unsigned v = (in[i] << 16) | (in[i+1] << 8) | in[i+2]; + out[n++] = tbl[(v >> 18) & 0x3F]; + out[n++] = tbl[(v >> 12) & 0x3F]; + out[n++] = tbl[(v >> 6) & 0x3F]; + out[n++] = tbl[v & 0x3F]; + i += 3; + } + int rem = len - i; + if (rem == 1) { + unsigned v = in[i] << 16; + out[n++] = tbl[(v >> 18) & 0x3F]; + out[n++] = tbl[(v >> 12) & 0x3F]; + out[n++] = '='; + out[n++] = '='; + } else if (rem == 2) { + unsigned v = (in[i] << 16) | (in[i+1] << 8); + out[n++] = tbl[(v >> 18) & 0x3F]; + out[n++] = tbl[(v >> 12) & 0x3F]; + out[n++] = tbl[(v >> 6) & 0x3F]; + out[n++] = '='; + } + return n; +} + // Build the SOCKS5 method-selection request. // +----+----------+----------+ // |VER | NMETHODS | METHODS | @@ -74,3 +108,44 @@ int socks5_build_connect_request(const proxy_conn_t* s5, unsigned char* buf) { buf[n++] = (unsigned char)(port & 0xFF); return n; } + +// Build an HTTP CONNECT request (RFC 7231 4.3.6). The request-target is the +// authority form "host:port"; an IPv6 literal is bracketed ("[addr]:port") per +// RFC 3986. A Basic Proxy-Authorization header is added when credentials are +// present. +int http_connect_build_request(const proxy_conn_t* p, char* buf, int bufsize) { + const char* host = p->setting.target_host; + int port = p->setting.target_port; + // bracket IPv6 literals in authority form + char authority[300]; + if (is_ipv6(host)) { + snprintf(authority, sizeof(authority), "[%s]:%d", host, port); + } else { + snprintf(authority, sizeof(authority), "%s:%d", host, port); + } + int n = 0; + int r = snprintf(buf + n, bufsize - n, + "CONNECT %s HTTP/1.1\r\nHost: %s\r\n", + authority, authority); + if (r < 0 || r >= bufsize - n) return -1; + n += r; + + if (p->setting.username[0]) { + // credentials = "user:pass" + char cred[520]; + int c = snprintf(cred, sizeof(cred), "%s:%s", + p->setting.username, p->setting.password); + if (c < 0 || c >= (int)sizeof(cred)) return -1; + char b64[768]; + int b = socks5_base64_encode((const unsigned char*)cred, c, b64); + b64[b] = '\0'; + r = snprintf(buf + n, bufsize - n, "Proxy-Authorization: Basic %s\r\n", b64); + if (r < 0 || r >= bufsize - n) return -1; + n += r; + } + + r = snprintf(buf + n, bufsize - n, "\r\n"); + if (r < 0 || r >= bufsize - n) return -1; + n += r; + return n; +} diff --git a/event/socks5.h b/event/socks5.h index 9ca2a4c80..1e4d0af4d 100644 --- a/event/socks5.h +++ b/event/socks5.h @@ -29,15 +29,17 @@ #define SOCKS5_REP_SUCCESS 0x00 // Internal per-connection runtime state for the proxy handshake (held on -// hio_t). Not part of the public configuration. +// hio_t). Not part of the public configuration. Shared by SOCKS5 and HTTP +// CONNECT. typedef struct proxy_conn_s { proxy_setting_t setting; // copied proxy config (target + auth) int state; // socks5_state_e (see nio.c) - // handshake read accumulator: SOCKS5 replies may be fragmented across TCP + // handshake read accumulator: replies may be fragmented across TCP // segments, so bytes are buffered here until a full message is available. - unsigned char rbuf[300]; // max reply: 4 + 1 + 255 + 2 (domain bind) + // SOCKS5 max reply is small; HTTP CONNECT response headers can be larger. + unsigned char rbuf[1024]; int rlen; // bytes currently in rbuf - int want; // bytes needed to complete the current step + int want; // bytes needed to complete the current step (SOCKS5) } proxy_conn_t; BEGIN_EXTERN_C @@ -47,6 +49,14 @@ int socks5_build_method_request (const proxy_conn_t* s5, unsigned char* buf); int socks5_build_auth_request (const proxy_conn_t* s5, unsigned char* buf); int socks5_build_connect_request(const proxy_conn_t* s5, unsigned char* buf); +// Build an HTTP CONNECT request into buf (size bufsize). Sends +// CONNECT target_host:target_port HTTP/1.1 +// Host: target_host:target_port +// [Proxy-Authorization: Basic base64(user:pass)] +// (blank line) +// Returns bytes written (<0 on error / truncation). +int http_connect_build_request(const proxy_conn_t* p, char* buf, int bufsize); + END_EXTERN_C #endif // HV_SOCKS5_H_ diff --git a/http/HttpMessage.cpp b/http/HttpMessage.cpp index fa86c7800..90963ab8a 100644 --- a/http/HttpMessage.cpp +++ b/http/HttpMessage.cpp @@ -660,6 +660,10 @@ void HttpRequest::Init() { redirect = 1; proxy = 0; cancel = 0; + tunnel_proxy_host.clear(); + tunnel_proxy_port = 0; + tunnel_proxy_username.clear(); + tunnel_proxy_password.clear(); } void HttpRequest::Reset() { @@ -775,6 +779,11 @@ void HttpRequest::SetProxy(const char* host, int port) { this->host = host; this->port = port; proxy = 1; + // mutually exclusive with the CONNECT-tunnel mode + tunnel_proxy_host.clear(); + tunnel_proxy_port = 0; + tunnel_proxy_username.clear(); + tunnel_proxy_password.clear(); } void HttpRequest::SetAuth(const std::string& auth) { diff --git a/http/HttpMessage.h b/http/HttpMessage.h index 5be241957..4856bcd97 100644 --- a/http/HttpMessage.h +++ b/http/HttpMessage.h @@ -386,8 +386,16 @@ class HV_EXPORT HttpRequest : public HttpMessage { uint32_t retry_count; uint32_t retry_delay; // unit: ms unsigned redirect: 1; - unsigned proxy : 1; + unsigned proxy : 1; // absolute-URI forward proxy (plain HTTP) unsigned cancel : 1; + // CONNECT-tunnel proxy (for https-over-proxy): when tunnel_proxy_host is set, + // the client connects to the proxy and issues an HTTP CONNECT to + // host:port, then does TLS end-to-end against the origin. Distinct from the + // `proxy` bit above, which is the plain-HTTP absolute-URI forward proxy. + std::string tunnel_proxy_host; + int tunnel_proxy_port; + std::string tunnel_proxy_username; + std::string tunnel_proxy_password; HttpRequest(); @@ -448,6 +456,20 @@ class HV_EXPORT HttpRequest : public HttpMessage { void SetProxy(const char* host, int port); bool IsProxy() { return proxy; } + // CONNECT-tunnel proxy (used for https-over-proxy). Unlike SetProxy (plain + // HTTP absolute-URI forwarding), this connects to the proxy and issues an + // HTTP CONNECT to the origin, then does end-to-end TLS with the origin. + // Mutually exclusive with the forward-proxy mode: clears the `proxy` bit. + void SetTunnelProxy(const char* host, int port, + const char* username = NULL, const char* password = NULL) { + proxy = 0; // not an absolute-URI forward proxy + tunnel_proxy_host = host ? host : ""; + tunnel_proxy_port = port; + tunnel_proxy_username = username ? username : ""; + tunnel_proxy_password = password ? password : ""; + } + bool IsTunnelProxy() { return !tunnel_proxy_host.empty(); } + // Auth void SetAuth(const std::string& auth); void SetBasicAuth(const std::string& username, const std::string& password); diff --git a/http/client/AsyncHttpClient.cpp b/http/client/AsyncHttpClient.cpp index e16073aa9..ebc800b6b 100644 --- a/http/client/AsyncHttpClient.cpp +++ b/http/client/AsyncHttpClient.cpp @@ -34,16 +34,24 @@ int AsyncHttpClient::doTask(const HttpClientTaskPtr& task) { } req->ParseUrl(); + // Where to open the TCP connection: normally the origin, but for an HTTP + // CONNECT tunnel (https over proxy) it is the proxy. The origin is then + // reached via the proxy's CONNECT (see doTaskWithAddr / hio_set_proxy). const char* host = req->host.c_str(); + int port = req->port; + if (req->IsTunnelProxy()) { + host = req->tunnel_proxy_host.c_str(); + port = req->tunnel_proxy_port; + } // If host is a numeric IP (or UDS), resolve synchronously (fast path). // Otherwise resolve the hostname asynchronously via EventLoop::resolveDns // so the event loop is never blocked by getaddrinfo. resolveDns returns a // use-after-free-proof DnsID and owns the underlying hdns_t lifetime. - if (req->port < 0 || is_ipaddr(host)) { + if (port < 0 || is_ipaddr(host)) { sockaddr_u peeraddr; memset(&peeraddr, 0, sizeof(peeraddr)); - int ret = sockaddr_set_ipport(&peeraddr, host, req->port); + int ret = sockaddr_set_ipport(&peeraddr, host, port); if (ret != 0) { hloge("unknown host %s", host); return -20; @@ -53,7 +61,6 @@ int AsyncHttpClient::doTask(const HttpClientTaskPtr& task) { hdns_setting_t opt; if (req->connect_timeout > 0) opt.timeout_ms = req->connect_timeout * 1000; - int port = req->port; DnsID id = EventLoopThread::loop()->resolveDns(host, [this, task, port](int status, int naddrs, const sockaddr_u* addrs) { if (status != HDNS_STATUS_OK || naddrs <= 0) { @@ -90,17 +97,22 @@ int AsyncHttpClient::doTaskWithAddr(const HttpClientTaskPtr& task, const sockadd return -10; } - const char* host = req->host.c_str(); sockaddr_u peeraddr = *paddr; int connfd = -1; - // first get from conn_pools + // Reuse a pooled keep-alive connection when possible. NOT for tunnels: the + // pool is keyed by peeraddr (the proxy), and a pooled plain HTTP-forward (or + // different-origin tunnel) connection to the same proxy would bypass the + // per-connection hio_set_proxy/SSL setup below and send over the wrong + // transport. Tunnels always open a fresh connection. char strAddr[SOCKADDR_STRLEN] = {0}; SOCKADDR_STR(&peeraddr, strAddr); - auto iter = conn_pools.find(strAddr); - if (iter != conn_pools.end()) { - // hlogd("get from conn_pools"); - iter->second.get(connfd); + if (!req->IsTunnelProxy()) { + auto iter = conn_pools.find(strAddr); + if (iter != conn_pools.end()) { + // hlogd("get from conn_pools"); + iter->second.get(connfd); + } } if (connfd < 0) { @@ -114,11 +126,24 @@ int AsyncHttpClient::doTaskWithAddr(const HttpClientTaskPtr& task, const sockadd assert(connio != NULL); hio_set_peeraddr(connio, &peeraddr.sa, sockaddr_len(&peeraddr)); addChannel(connio); - // https - if (req->IsHttps() && !req->IsProxy()) { + // https over proxy: HTTP CONNECT tunnel to the origin, then TLS with it. + if (req->IsTunnelProxy()) { + proxy_setting_t proxy; + proxy.protocol = PROXY_PROTOCOL_HTTP_CONNECT; + hv_strncpy(proxy.target_host, req->host.c_str(), sizeof(proxy.target_host)); + proxy.target_port = req->port; + if (!req->tunnel_proxy_username.empty()) { + hv_strncpy(proxy.username, req->tunnel_proxy_username.c_str(), sizeof(proxy.username)); + hv_strncpy(proxy.password, req->tunnel_proxy_password.c_str(), sizeof(proxy.password)); + } + hio_set_proxy(connio, &proxy); + } + // https: enable TLS against the origin (also for the tunnel case, run + // after the CONNECT handshake completes, with SNI = origin host). + if (req->IsHttps()) { hio_enable_ssl(connio); - if (!is_ipaddr(host)) { - hio_set_hostname(connio, host); + if (!is_ipaddr(req->host.c_str())) { + hio_set_hostname(connio, req->host.c_str()); } } } @@ -159,6 +184,9 @@ int AsyncHttpClient::doTaskWithAddr(const HttpClientTaskPtr& task, const sockadd auto& req = ctx->task->req; auto& resp = ctx->resp; bool keepalive = req->IsKeepAlive() && resp->IsKeepAlive(); + // Snapshot before any callback: successCallback() clears ctx->task, + // which frees the request `req` references (dangling afterwards). + bool is_tunnel = req->IsTunnelProxy(); if (req->redirect && HTTP_STATUS_IS_REDIRECT(resp->status_code)) { std::string location = resp->headers["Location"]; if (!location.empty()) { @@ -174,11 +202,14 @@ int AsyncHttpClient::doTaskWithAddr(const HttpClientTaskPtr& task, const sockadd } else { ctx->successCallback(); } - if (keepalive) { + if (keepalive && !is_tunnel) { // NOTE: add into conn_pools to reuse // hlogd("add into conn_pools"); conn_pools[channel->peeraddr()].add(channel->fd()); } else { + // A CONNECT tunnel is bound to one origin; the pool is keyed by + // peeraddr (the proxy), so reusing it for a different origin + // would send to the wrong target. Never pool tunnels. channel->close(); } } diff --git a/http/client/HttpClient.cpp b/http/client/HttpClient.cpp index 9917dd2d5..ef225eef0 100644 --- a/http/client/HttpClient.cpp +++ b/http/client/HttpClient.cpp @@ -12,6 +12,7 @@ #include "hstring.h" #include "hsocket.h" #include "hssl.h" +#include "base64.h" #include "HttpParser.h" // for async @@ -32,6 +33,9 @@ struct http_client_s { // https_proxy std::string https_proxy_host; int https_proxy_port; + // proxy auth (Basic for http, SOCKS5/CONNECT tunnel auth) + std::string proxy_username; + std::string proxy_password; // no_proxy StringList no_proxy_hosts; //private: @@ -166,6 +170,12 @@ int http_client_add_no_proxy(http_client_t* cli, const char* host) { return 0; } +int http_client_set_proxy_auth(http_client_t* cli, const char* username, const char* password) { + cli->proxy_username = username ? username : ""; + cli->proxy_password = password ? password : ""; + return 0; +} + static int http_client_make_request(http_client_t* cli, HttpRequest* req) { if (req->url.empty() || *req->url.c_str() == '/') { req->scheme = cli->https ? "https" : "http"; @@ -189,9 +199,38 @@ static int http_client_make_request(http_client_t* cli, HttpRequest* req) { } } } + // Recompute proxy routing from scratch for THIS request. HttpClient reuses + // one HttpRequest across redirects and repeated sends, so any proxy mode + // left over from a previous route must be cleared first -- otherwise a prior + // tunnel/forward setting could survive a redirect to a no_proxy host or a + // scheme switch and pick the wrong transport. Also drop any generated + // Proxy-Authorization so it never crosses a CONNECT tunnel to the origin and + // never lingers after setProxyAuth(NULL, NULL); it is regenerated below only + // for the plain-HTTP forward proxy (where the request goes to the proxy). + req->proxy = 0; + req->tunnel_proxy_host.clear(); + req->tunnel_proxy_port = 0; + req->tunnel_proxy_username.clear(); + req->tunnel_proxy_password.clear(); + req->headers.erase("Proxy-Authorization"); if (use_proxy) { - req->SetProxy(https ? cli->https_proxy_host.c_str() : cli->http_proxy_host.c_str(), - https ? cli->https_proxy_port : cli->http_proxy_port); + if (https) { + // https over proxy: use an HTTP CONNECT tunnel (end-to-end TLS with + // the origin), NOT absolute-URI forwarding. Credentials travel in + // the CONNECT request itself (transport-only). + req->SetTunnelProxy(cli->https_proxy_host.c_str(), cli->https_proxy_port, + cli->proxy_username.empty() ? NULL : cli->proxy_username.c_str(), + cli->proxy_password.empty() ? NULL : cli->proxy_password.c_str()); + } else { + // plain http over proxy: absolute-URI forward proxy. The request is + // sent to the proxy, so Proxy-Authorization is correctly consumed by it. + req->SetProxy(cli->http_proxy_host.c_str(), cli->http_proxy_port); + if (!cli->proxy_username.empty()) { + std::string cred = cli->proxy_username + ":" + cli->proxy_password; + req->headers["Proxy-Authorization"] = + "Basic " + hv::Base64Encode((const unsigned char*)cred.data(), cred.size()); + } + } } if (req->timeout == 0) { @@ -207,6 +246,123 @@ static int http_client_make_request(http_client_t* cli, HttpRequest* req) { return 0; } +// Client-side TLS handshake on an already-connected fd. sni_host is the origin +// host used for SNI. On failure frees cli->ssl and returns an error (<0); the +// caller owns/closes connfd. +static int http_client_ssl_handshake(http_client_t* cli, int connfd, const char* sni_host, + int blocktime, unsigned int start_time) { + // cli->ssl_ctx > g_ssl_ctx > hssl_ctx_new + hssl_ctx_t ssl_ctx = NULL; + if (cli->ssl_ctx) { + ssl_ctx = cli->ssl_ctx; + } else if (g_ssl_ctx) { + ssl_ctx = g_ssl_ctx; + } else { + cli->ssl_ctx = ssl_ctx = hssl_ctx_new(NULL); + cli->alloced_ssl_ctx = true; + } + if (ssl_ctx == NULL) { + return NABS(ERR_NEW_SSL_CTX); + } + cli->ssl = hssl_new(ssl_ctx, connfd); + if (cli->ssl == NULL) { + return NABS(ERR_NEW_SSL); + } + if (sni_host && !is_ipaddr(sni_host)) { + hssl_set_sni_hostname(cli->ssl, sni_host); + } +#ifdef WITH_OPENSSL + // Offer ALPN "h2" only when HTTP/2 is intended, so an h2-capable server + // negotiates it (real https servers require ALPN, not prior-knowledge). + // Set it per-connection on the SSL object (not the shared ctx), so it + // works with any ctx source (user/global/allocated) and never leaks h2 + // into other clients or http/1.1 requests. + if (cli->http_version == 2) { + static unsigned char s_alpn_protos[] = "\x02h2\x08http/1.1"; + hssl_set_alpn_protos(cli->ssl, s_alpn_protos, sizeof(s_alpn_protos) - 1); + } +#endif + unsigned int elapsed = gettick_ms() - start_time; + int ssl_timeout = blocktime - (int)elapsed; + if (ssl_timeout <= 0) { + hssl_free(cli->ssl); + cli->ssl = NULL; + return NABS(ETIMEDOUT); + } + so_rcvtimeo(connfd, ssl_timeout); + int ret = hssl_connect(cli->ssl); + if (ret != 0) { + fprintf(stderr, "* ssl handshake failed: %d\n", ret); + hloge("ssl handshake failed: %d", ret); + hssl_free(cli->ssl); + cli->ssl = NULL; + return NABS(ret); + } + return 0; +} + +// Blocking HTTP CONNECT handshake to a proxy on connfd: sends +// CONNECT origin_host:origin_port HTTP/1.1 ... [Proxy-Authorization] \r\n\r\n +// then reads the response headers and requires a 2xx status. Returns 0 on +// success; the caller owns/closes connfd. +static int http_client_http_connect(int connfd, const char* origin_host, int origin_port, + const std::string& user, const std::string& pass, + int blocktime) { + // authority form; bracket IPv6 literals per RFC 3986 + std::string authority = is_ipv6(origin_host) + ? hv::asprintf("[%s]:%d", origin_host, origin_port) + : hv::asprintf("%s:%d", origin_host, origin_port); + std::string reqstr = hv::asprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", + authority.c_str(), authority.c_str()); + if (!user.empty()) { + std::string cred = user + ":" + pass; + reqstr += "Proxy-Authorization: Basic " + + hv::Base64Encode((const unsigned char*)cred.data(), cred.size()) + "\r\n"; + } + reqstr += "\r\n"; + + unsigned int start = gettick_ms(); + size_t total = 0; + while (total < reqstr.size()) { + int left = blocktime - (int)(gettick_ms() - start); + if (left <= 0) return NABS(ETIMEDOUT); + so_sndtimeo(connfd, left); + int nsend = send(connfd, reqstr.data() + total, reqstr.size() - total, 0); + if (nsend <= 0) { + if (socket_errno() == EINTR) continue; + return NABS(socket_errno()); + } + total += nsend; + } + + // read response headers until CRLFCRLF + char buf[1024]; + int rlen = 0; + while (rlen < (int)sizeof(buf)) { + int left = blocktime - (int)(gettick_ms() - start); + if (left <= 0) return NABS(ETIMEDOUT); + so_rcvtimeo(connfd, left); + int nrecv = recv(connfd, buf + rlen, sizeof(buf) - rlen, 0); + if (nrecv == 0) return NABS(ERR_CONNECT); // peer closed + if (nrecv < 0) { + if (socket_errno() == EINTR) continue; + return NABS(socket_errno()); + } + rlen += nrecv; + for (int i = 3; i < rlen; ++i) { + if (buf[i-3]=='\r' && buf[i-2]=='\n' && buf[i-1]=='\r' && buf[i]=='\n') { + int code = 0; + const char* sp = (const char*)memchr(buf, ' ', rlen); + if (sp) code = atoi(sp + 1); + if (code >= 200 && code < 300) return 0; + hloge("http proxy CONNECT failed: %d", code); + return NABS(ERR_CONNECT); + } + } + } + return NABS(ERR_CONNECT); // headers too large, no blank line +} + int http_client_connect(http_client_t* cli, const char* host, int port, int https, int timeout) { cli->Close(); int blocktime = DEFAULT_CONNECT_TIMEOUT; @@ -222,56 +378,49 @@ int http_client_connect(http_client_t* cli, const char* host, int port, int http tcp_nodelay(connfd, 1); if (https && cli->ssl == NULL) { - // cli->ssl_ctx > g_ssl_ctx > hssl_ctx_new - hssl_ctx_t ssl_ctx = NULL; - if (cli->ssl_ctx) { - ssl_ctx = cli->ssl_ctx; - } else if (g_ssl_ctx) { - ssl_ctx = g_ssl_ctx; - } else { - cli->ssl_ctx = ssl_ctx = hssl_ctx_new(NULL); - cli->alloced_ssl_ctx = true; - } - if (ssl_ctx == NULL) { - closesocket(connfd); - return NABS(ERR_NEW_SSL_CTX); - } - cli->ssl = hssl_new(ssl_ctx, connfd); - if (cli->ssl == NULL) { - closesocket(connfd); - return NABS(ERR_NEW_SSL); - } - if (!is_ipaddr(host)) { - hssl_set_sni_hostname(cli->ssl, host); - } -#ifdef WITH_OPENSSL - // Offer ALPN "h2" only when HTTP/2 is intended, so an h2-capable server - // negotiates it (real https servers require ALPN, not prior-knowledge). - // Set it per-connection on the SSL object (not the shared ctx), so it - // works with any ctx source (user/global/allocated) and never leaks h2 - // into other clients or http/1.1 requests. - if (cli->http_version == 2) { - static unsigned char s_alpn_protos[] = "\x02h2\x08http/1.1"; - hssl_set_alpn_protos(cli->ssl, s_alpn_protos, sizeof(s_alpn_protos) - 1); - } -#endif - unsigned int elapsed = gettick_ms() - start_time; - int ssl_timeout = blocktime - (int)elapsed; - if (ssl_timeout <= 0) { - hssl_free(cli->ssl); - cli->ssl = NULL; + int ret = http_client_ssl_handshake(cli, connfd, host, blocktime, start_time); + if (ret != 0) { closesocket(connfd); - return NABS(ETIMEDOUT); + return ret; } - so_rcvtimeo(connfd, ssl_timeout); - int ret = hssl_connect(cli->ssl); + } + + cli->fd = connfd; + cli->keepalive_requests = 0; + return connfd; +} + +// Connect through an HTTP CONNECT tunnel: TCP-connect to the proxy, issue a +// blocking CONNECT to the origin, then (for https) TLS end-to-end with the +// origin. Used by the sync exec path for https-over-proxy. +static int http_client_connect_tunnel(http_client_t* cli, HttpRequest* req, int timeout) { + cli->Close(); + int blocktime = DEFAULT_CONNECT_TIMEOUT; + if (timeout > 0) { + blocktime = MIN(timeout*1000, blocktime); + } + unsigned int start_time = gettick_ms(); + int connfd = ConnectTimeout(req->tunnel_proxy_host.c_str(), req->tunnel_proxy_port, blocktime); + if (connfd < 0) { + hloge("connect proxy %s:%d failed!", req->tunnel_proxy_host.c_str(), req->tunnel_proxy_port); + return connfd; + } + tcp_nodelay(connfd, 1); + + int left = blocktime - (int)(gettick_ms() - start_time); + if (left <= 0) { closesocket(connfd); return NABS(ETIMEDOUT); } + int ret = http_client_http_connect(connfd, req->host.c_str(), req->port, + req->tunnel_proxy_username, req->tunnel_proxy_password, left); + if (ret != 0) { + closesocket(connfd); + return ret; + } + + if (req->IsHttps() && cli->ssl == NULL) { + ret = http_client_ssl_handshake(cli, connfd, req->host.c_str(), blocktime, start_time); if (ret != 0) { - fprintf(stderr, "* ssl handshake failed: %d\n", ret); - hloge("ssl handshake failed: %d", ret); - hssl_free(cli->ssl); - cli->ssl = NULL; closesocket(connfd); - return NABS(ret); + return ret; } } @@ -354,7 +503,13 @@ static int http_client_exec(http_client_t* cli, HttpRequest* req, HttpResponse* cli->port = req->port; cli->http_version = req->http_major; // gates the ALPN "h2" offer in connect connect: - connfd = http_client_connect(cli, req->host.c_str(), req->port, https, connect_timeout); + if (req->IsTunnelProxy()) { + // https over proxy: connect to the proxy, HTTP CONNECT to the + // origin, then TLS end-to-end with the origin. + connfd = http_client_connect_tunnel(cli, req, connect_timeout); + } else { + connfd = http_client_connect(cli, req->host.c_str(), req->port, https, connect_timeout); + } if (connfd < 0) { return connfd; } @@ -574,10 +729,23 @@ static int http_client_exec_curl(http_client_t* cli, HttpRequest* req, HttpRespo } CURL* curl = cli->curl; - // proxy + // proxy: plain-http forward proxy (req->host is the proxy) or CONNECT + // tunnel (tunnel_proxy_* for https). libcurl handles both, incl. CONNECT. + // cli->curl is reused across requests, so always reset proxy options first + // (a stale PROXY / PROXYUSERPWD would otherwise leak into a later request + // that uses a different or no proxy). + curl_easy_setopt(curl, CURLOPT_PROXY, ""); + curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, ""); if (req->IsProxy()) { curl_easy_setopt(curl, CURLOPT_PROXY, req->host.c_str()); curl_easy_setopt(curl, CURLOPT_PROXYPORT, req->port); + } else if (req->IsTunnelProxy()) { + curl_easy_setopt(curl, CURLOPT_PROXY, req->tunnel_proxy_host.c_str()); + curl_easy_setopt(curl, CURLOPT_PROXYPORT, req->tunnel_proxy_port); + if (!req->tunnel_proxy_username.empty()) { + std::string userpwd = req->tunnel_proxy_username + ":" + req->tunnel_proxy_password; + curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, userpwd.c_str()); + } } // SSL diff --git a/http/client/HttpClient.h b/http/client/HttpClient.h index 984096d59..74fcd8ed0 100644 --- a/http/client/HttpClient.h +++ b/http/client/HttpClient.h @@ -53,6 +53,8 @@ HV_EXPORT int http_client_set_http_proxy(http_client_t* cli, const char* host, i HV_EXPORT int http_client_set_https_proxy(http_client_t* cli, const char* host, int port); // no_proxy HV_EXPORT int http_client_add_no_proxy(http_client_t* cli, const char* host); +// proxy auth (Basic for http forward proxy / HTTP CONNECT tunnel) +HV_EXPORT int http_client_set_proxy_auth(http_client_t* cli, const char* username, const char* password); // sync HV_EXPORT int http_client_send(http_client_t* cli, HttpRequest* req, HttpResponse* resp); @@ -124,6 +126,10 @@ class HttpClient { int addNoProxy(const char* host) { return http_client_add_no_proxy(client_.get(), host); } + // proxy auth (Basic for http forward proxy / HTTP CONNECT tunnel) + int setProxyAuth(const char* username, const char* password) { + return http_client_set_proxy_auth(client_.get(), username, password); + } // sync int send(HttpRequest* req, HttpResponse* resp) {