Skip to content

Commit e02e42b

Browse files
authored
feat(http): support https over proxy via HTTP CONNECT (#886)
1 parent 0db9c63 commit e02e42b

13 files changed

Lines changed: 772 additions & 202 deletions

File tree

‎docs/cn/HttpClient.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,32 @@ class HttpClient {
2121
// 获取请求头部
2222
const char* getHeader(const char* key);
2323

24-
// 设置http代理
24+
// 设置http代理(明文HTTP走绝对URI转发)
2525
int setHttpProxy(const char* host, int port);
26-
// 设置https代理
26+
// 设置https代理(HTTPS走HTTP CONNECT隧道,与目标端到端TLS)
2727
int setHttpsProxy(const char* host, int port);
2828
// 添加不走代理
2929
int addNoProxy(const char* host);
30+
// 设置代理认证(http转发用Basic头,CONNECT隧道用Proxy-Authorization)
31+
int setProxyAuth(const char* username, const char* password);
3032

3133
// 同步发送
3234
int send(HttpRequest* req, HttpResponse* resp);
3335
// 异步发送
3436
int sendAsync(HttpRequestPtr req, HttpResponseCallback resp_cb = NULL);
3537

38+
// 按请求中的目标及代理配置建立连接;成功返回时CONNECT隧道和TLS握手均已完成
39+
int connect(HttpRequest& req);
40+
// 直接连接指定主机
41+
int connect(const char* host, int port = DEFAULT_HTTP_PORT, int https = 0, int timeout = DEFAULT_HTTP_CONNECT_TIMEOUT);
42+
3643
// 关闭连接 (HttpClient对象析构时会自动调用)
3744
int close();
3845

3946
};
4047

48+
当 `HttpRequest` 已通过 `SetProxy` 或 `SetTunnelProxy` 配置代理时,以请求级配置为准;否则使用 `HttpClient` 的全局代理配置。重定向后的连接目标未变化时复用当前连接,目标或代理变化时使用独立连接完成重定向,不会关闭调用方原有的 keep-alive 连接。
49+
4150
namespace requests {
4251

4352
// 同步请求

‎docs/cn/HttpMessage.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ class HttpRequest : public HttpMessage {
8080

8181
// 设置代理
8282
void SetProxy(const char* host, int port);
83+
// 设置HTTP正向代理Basic认证,username为空时删除Proxy-Authorization
84+
void SetProxyAuth(const char* username, const char* password = NULL);
85+
// 设置HTTP CONNECT隧道代理及握手认证
86+
void SetTunnelProxy(const char* host, int port, const char* username = NULL, const char* password = NULL);
8387

8488
// 设置认证
8589
void SetAuth(const std::string& auth);

‎event/hevent.c‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,11 @@ const char* hio_get_hostname(hio_t* io) {
499499

500500
int hio_set_proxy(hio_t* io, proxy_setting_t* setting) {
501501
if (io == NULL || setting == NULL) return -1;
502-
// only SOCKS5 is implemented so far
503-
if (setting->protocol != PROXY_PROTOCOL_SOCKS5) return -1;
502+
// implemented: SOCKS5, HTTP CONNECT
503+
if (setting->protocol != PROXY_PROTOCOL_SOCKS5 &&
504+
setting->protocol != PROXY_PROTOCOL_HTTP_CONNECT) {
505+
return -1;
506+
}
504507
if (io->proxy == NULL) {
505508
HV_ALLOC_SIZEOF(io->proxy);
506509
if (io->proxy == NULL) return -1;

‎event/hloop.h‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,17 +359,19 @@ HV_EXPORT const char* hio_get_hostname(hio_t* io);
359359
// already the proxy connection).
360360
//
361361
// The setting is copied. Leave username empty for no auth, or set
362-
// username/password for auth (SOCKS5 => RFC 1929). Only PROXY_PROTOCOL_SOCKS5
363-
// is implemented so far.
362+
// username/password for auth (SOCKS5 => RFC 1929, HTTP CONNECT => Basic).
363+
// Implemented protocols: PROXY_PROTOCOL_SOCKS5, PROXY_PROTOCOL_HTTP_CONNECT.
364364
// NOTE: set before hio_connect().
365365
typedef enum {
366-
PROXY_PROTOCOL_NONE = 0,
367-
PROXY_PROTOCOL_SOCKS5 = 1,
366+
PROXY_PROTOCOL_NONE = 0,
367+
PROXY_PROTOCOL_SOCKS5 = 1,
368+
PROXY_PROTOCOL_HTTP_CONNECT = 2, // HTTP CONNECT tunnel (RFC 7231 4.3.6)
368369
} proxy_protocol_e;
369370

370371
typedef struct proxy_setting_s {
371372
int protocol; // proxy_protocol_e
372-
char proxy_host[256]; // proxy host (SOCKS5: unused, socket is the proxy)
373+
char proxy_host[256]; // proxy host (unused by the io layer: the socket
374+
// is already the proxy connection; kept for ref)
373375
int proxy_port;
374376
char target_host[256]; // final target the proxy should CONNECT to
375377
int target_port;

‎event/nio.c‎

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,9 @@ enum socks5_state_e {
263263

264264
static void socks5_handshake(hio_t* io);
265265

266-
static void socks5_fail(hio_t* io) {
266+
static void proxy_fail(hio_t* io) {
267267
if (io->error == 0) io->error = ERR_CONNECT;
268-
hlogw("connfd=%d socks5 handshake error", io->fd);
268+
hlogw("connfd=%d proxy handshake error", io->fd);
269269
hio_close(io);
270270
}
271271

@@ -277,16 +277,18 @@ static void socks5_expect(hio_t* io, int state, int want) {
277277
s5->want = want;
278278
}
279279

280-
// Raw handshake send. The SOCKS5 handshake runs immediately after the TCP
280+
// Raw handshake send. The proxy handshake runs immediately after the TCP
281281
// connection to the proxy is established, when the socket send buffer is empty
282-
// and the messages are tiny (<= 513 bytes), so a short write is not expected.
282+
// and the message is tiny (SOCKS5 <= 513 bytes; HTTP CONNECT < ~1.3KB), far
283+
// smaller than the default send buffer, so a single send() transfers it all.
283284
// We deliberately do NOT use hio_write() here: it would invoke the upper-layer
284285
// write_cb (leaking handshake bytes, including credentials, to the application
285286
// before onConnection), dispatch to hssl_write() with a not-yet-created SSL
286287
// handle for a TLS target, and enqueue on EAGAIN via hio_add() which would
287-
// clobber the handshake read handler. A short write or error is treated as
288-
// fatal and closes the connection.
289-
static int socks5_send(hio_t* io, const void* buf, int len) {
288+
// clobber the handshake read handler (io has a single cb slot). A short write
289+
// cannot happen here in practice; if it somehow does, it is treated as fatal
290+
// (return -1) rather than blocking the event loop.
291+
static int proxy_send(hio_t* io, const void* buf, int len) {
290292
int flag = 0;
291293
#ifdef MSG_NOSIGNAL
292294
flag |= MSG_NOSIGNAL;
@@ -300,15 +302,15 @@ static void socks5_send_connect(hio_t* io) {
300302
proxy_conn_t* s5 = io->proxy;
301303
unsigned char buf[300];
302304
int n = socks5_build_connect_request(s5, buf);
303-
if (n < 0) { socks5_fail(io); return; }
304-
if (socks5_send(io, buf, n) != 0) { socks5_fail(io); return; }
305+
if (n < 0) { proxy_fail(io); return; }
306+
if (proxy_send(io, buf, n) != 0) { proxy_fail(io); return; }
305307
socks5_expect(io, S5_RECV_REPLY_HEAD, 4);
306308
}
307309

308310
// hand off the established proxy tunnel to the upper layer: stop the handshake
309311
// read handler, then run the SSL handshake / connect_cb. io->read_cb was never
310312
// touched, so the upper-layer Channel read callback stays intact.
311-
static void socks5_established(hio_t* io) {
313+
static void proxy_established(hio_t* io) {
312314
hio_del(io, HV_READ);
313315
nio_connect_established(io);
314316
}
@@ -321,29 +323,29 @@ static void socks5_dispatch(hio_t* io) {
321323
switch (s5->state) {
322324
case S5_RECV_METHOD:
323325
// VER METHOD
324-
if (buf[0] != SOCKS5_VERSION) { socks5_fail(io); return; }
326+
if (buf[0] != SOCKS5_VERSION) { proxy_fail(io); return; }
325327
if (buf[1] == SOCKS5_AUTH_NONE) {
326328
socks5_send_connect(io);
327329
} else if (buf[1] == SOCKS5_AUTH_USERPASS && s5->setting.username[0]) {
328330
unsigned char req[640];
329331
int n = socks5_build_auth_request(s5, req);
330-
if (socks5_send(io, req, n) != 0) { socks5_fail(io); return; }
332+
if (proxy_send(io, req, n) != 0) { proxy_fail(io); return; }
331333
socks5_expect(io, S5_RECV_AUTH, 2);
332334
} else {
333-
socks5_fail(io); // no acceptable method
335+
proxy_fail(io); // no acceptable method
334336
}
335337
return;
336338

337339
case S5_RECV_AUTH:
338340
// VER STATUS (0 == success)
339-
if (buf[1] != 0x00) { socks5_fail(io); return; }
341+
if (buf[1] != 0x00) { proxy_fail(io); return; }
340342
socks5_send_connect(io);
341343
return;
342344

343345
case S5_RECV_REPLY_HEAD: {
344346
// VER REP RSV ATYP
345-
if (buf[0] != SOCKS5_VERSION) { socks5_fail(io); return; }
346-
if (buf[1] != SOCKS5_REP_SUCCESS) { io->error = ERR_CONNECT; socks5_fail(io); return; }
347+
if (buf[0] != SOCKS5_VERSION) { proxy_fail(io); return; }
348+
if (buf[1] != SOCKS5_REP_SUCCESS) { io->error = ERR_CONNECT; proxy_fail(io); return; }
347349
unsigned char atyp = buf[3];
348350
if (atyp == SOCKS5_ATYP_IPV4) {
349351
socks5_expect(io, S5_RECV_REPLY_ADDR, 4 + 2); // addr + port
@@ -354,14 +356,14 @@ static void socks5_dispatch(hio_t* io) {
354356
// first requiring the length byte.
355357
socks5_expect(io, S5_RECV_REPLY_DADDR, 1);
356358
} else {
357-
socks5_fail(io);
359+
proxy_fail(io);
358360
}
359361
return;
360362
}
361363

362364
case S5_RECV_REPLY_ADDR:
363365
// bound addr+port consumed; tunnel is up
364-
socks5_established(io);
366+
proxy_established(io);
365367
return;
366368

367369
case S5_RECV_REPLY_DADDR:
@@ -372,11 +374,11 @@ static void socks5_dispatch(hio_t* io) {
372374
socks5_expect(io, S5_RECV_REPLY_DADDR, dlen + 2);
373375
return;
374376
}
375-
socks5_established(io);
377+
proxy_established(io);
376378
return;
377379

378380
default:
379-
socks5_fail(io);
381+
proxy_fail(io);
380382
return;
381383
}
382384
}
@@ -387,14 +389,14 @@ static void socks5_handshake(hio_t* io) {
387389
proxy_conn_t* s5 = io->proxy;
388390
while (s5->rlen < s5->want) {
389391
int need = s5->want - s5->rlen;
390-
if (s5->want > (int)sizeof(s5->rbuf)) { socks5_fail(io); return; }
392+
if (s5->want > (int)sizeof(s5->rbuf)) { proxy_fail(io); return; }
391393
int n = recv(io->fd, (char*)s5->rbuf + s5->rlen, need, 0);
392-
if (n == 0) { socks5_fail(io); return; } // peer closed
394+
if (n == 0) { proxy_fail(io); return; } // peer closed
393395
if (n < 0) {
394396
int err = socket_errno();
395397
if (err == EAGAIN || err == EINTR) return; // wait for more
396398
io->error = err;
397-
socks5_fail(io);
399+
proxy_fail(io);
398400
return;
399401
}
400402
s5->rlen += n;
@@ -407,17 +409,100 @@ static void socks5_handshake_start(hio_t* io) {
407409
proxy_conn_t* s5 = io->proxy;
408410
unsigned char buf[8];
409411
int n = socks5_build_method_request(s5, buf);
410-
if (socks5_send(io, buf, n) != 0) { socks5_fail(io); return; }
412+
if (proxy_send(io, buf, n) != 0) { proxy_fail(io); return; }
411413
socks5_expect(io, S5_RECV_METHOD, 2);
412414
hio_add(io, socks5_handshake, HV_READ);
413415
}
414416

415-
// Dispatch the proxy handshake by protocol (only SOCKS5 implemented so far).
417+
// HTTP CONNECT handshake (RFC 7231 4.3.6): send a CONNECT request, then read
418+
// response headers until the blank line "\r\n\r\n". A 2xx status establishes
419+
// the tunnel. Like the SOCKS5 handshake this uses a dedicated recv() via
420+
// hio_add (never touches io->read_cb) and is robust to fragmentation.
421+
//
422+
// CONNECT responses carry no body, but a server-first origin protocol (SMTP,
423+
// IMAP, FTP...) may send its greeting immediately after the tunnel opens, so
424+
// those bytes can arrive in the same segment as the response headers. To avoid
425+
// swallowing them, we MSG_PEEK to locate the header terminator, then drain
426+
// EXACTLY the header bytes with a real recv(); anything after "\r\n\r\n" stays
427+
// in the socket for the upper-layer read path.
428+
static void http_connect_handshake(hio_t* io) {
429+
proxy_conn_t* p = io->proxy;
430+
for (;;) {
431+
int cap = (int)sizeof(p->rbuf) - p->rlen;
432+
if (cap <= 0) { proxy_fail(io); return; } // headers too large
433+
// peek (non-destructive): inspect what is available without consuming.
434+
int n = recv(io->fd, (char*)p->rbuf + p->rlen, cap, MSG_PEEK);
435+
if (n == 0) { proxy_fail(io); return; } // peer closed
436+
if (n < 0) {
437+
int err = socket_errno();
438+
if (err == EAGAIN || err == EINTR) return; // wait for more
439+
io->error = err;
440+
proxy_fail(io);
441+
return;
442+
}
443+
int have = p->rlen + n;
444+
// search for "\r\n\r\n" in the peeked window (rescan from a safe offset)
445+
int start = p->rlen >= 3 ? p->rlen - 3 : 0;
446+
int term = -1;
447+
for (int i = start + 3; i < have; ++i) {
448+
if (p->rbuf[i-3]=='\r' && p->rbuf[i-2]=='\n' &&
449+
p->rbuf[i-1]=='\r' && p->rbuf[i]=='\n') { term = i; break; }
450+
}
451+
if (term < 0) {
452+
// no full header yet: consume the peeked bytes into the accumulator
453+
// (they are all header bytes) and keep reading.
454+
int got = recv(io->fd, (char*)p->rbuf + p->rlen, n, 0);
455+
if (got <= 0) { proxy_fail(io); return; }
456+
p->rlen += got;
457+
continue;
458+
}
459+
// full header present. Drain exactly up to and including the terminator,
460+
// leaving any trailing tunnel/greeting bytes in the socket.
461+
int header_len = term + 1; // bytes from socket start
462+
int to_drain = header_len - p->rlen; // not yet consumed
463+
if (to_drain > 0) {
464+
int got = recv(io->fd, (char*)p->rbuf + p->rlen, to_drain, 0);
465+
if (got != to_drain) { proxy_fail(io); return; }
466+
p->rlen += got;
467+
}
468+
// parse status line: "HTTP/1.x SP CODE SP ..."
469+
int code = 0;
470+
char* sp = (char*)memchr(p->rbuf, ' ', p->rlen);
471+
if (sp) code = atoi(sp + 1);
472+
if (code >= 200 && code < 300) {
473+
proxy_established(io);
474+
} else {
475+
hlogw("connfd=%d http proxy CONNECT failed: %d", io->fd, code);
476+
io->error = ERR_CONNECT;
477+
proxy_fail(io);
478+
}
479+
return;
480+
}
481+
}
482+
483+
// Kick off the HTTP CONNECT handshake once the TCP connection to the proxy is up.
484+
static void http_connect_start(hio_t* io) {
485+
proxy_conn_t* p = io->proxy;
486+
// Max request: "CONNECT " + authority(<=262) + " HTTP/1.1\r\nHost: " +
487+
// authority + "\r\nProxy-Authorization: Basic " + base64(255:255)=~684 +
488+
// "\r\n\r\n" ~= 1.3KB. 2048 leaves headroom.
489+
char buf[2048];
490+
int n = http_connect_build_request(p, buf, (int)sizeof(buf));
491+
if (n < 0) { proxy_fail(io); return; }
492+
if (proxy_send(io, buf, n) != 0) { proxy_fail(io); return; }
493+
p->rlen = 0;
494+
hio_add(io, http_connect_handshake, HV_READ);
495+
}
496+
497+
// Dispatch the proxy handshake by protocol.
416498
static void proxy_handshake_start(hio_t* io) {
417499
switch (io->proxy->setting.protocol) {
418500
case PROXY_PROTOCOL_SOCKS5:
419501
socks5_handshake_start(io);
420502
return;
503+
case PROXY_PROTOCOL_HTTP_CONNECT:
504+
http_connect_start(io);
505+
return;
421506
default:
422507
io->error = ERR_INVALID_PARAM;
423508
hio_close(io);

0 commit comments

Comments
 (0)