Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/cn/HttpClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions event/hevent.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 7 additions & 5 deletions event/hloop.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
135 changes: 110 additions & 25 deletions event/nio.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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) {
Comment thread
Copilot marked this conversation as resolved.
int flag = 0;
#ifdef MSG_NOSIGNAL
flag |= MSG_NOSIGNAL;
Expand All @@ -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);
}
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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;
}
}
Expand All @@ -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;
Expand All @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions event/socks5.c
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
#include "socks5.h"

#include <string.h>
#include <stdio.h>

#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 |
Expand Down Expand Up @@ -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;
}
Loading
Loading