mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Tunnel fetch through env proxies with CONNECT (#1237)
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
6665, 6666, 6667, 6668, 6669, 6679, 6697, 10080
|
||||
|
||||
#include "scr_runtime.h"
|
||||
#include "scr_system_errors.h"
|
||||
#include "scr_url_internal.h"
|
||||
|
||||
#include <math.h>
|
||||
@@ -2888,14 +2889,21 @@ static int sf_hex_value(char c) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* EnvHttpProxyAgent sends Basic proxy authentication only when the proxy
|
||||
* URL has both a username and a password. */
|
||||
static bool sf_proxy_has_credentials(const ScrStr *userinfo) {
|
||||
const char *colon = memchr(userinfo->data, ':', userinfo->len);
|
||||
return colon != NULL && colon != userinfo->data &&
|
||||
colon + 1 < userinfo->data + userinfo->len;
|
||||
}
|
||||
|
||||
/* EnvHttpProxyAgent derives Basic proxy authentication from the proxy URL's
|
||||
* decoded username/password. ScrUrl keeps the encoded userinfo as one string,
|
||||
* so decode it here and add the empty-password separator when necessary. */
|
||||
* so decode it here. */
|
||||
static ScrStr *sf_proxy_authorization(const ScrStr *userinfo) {
|
||||
char *plain = malloc(userinfo->len + 1);
|
||||
if (!plain) sf_oom();
|
||||
size_t plain_len = 0;
|
||||
bool has_separator = false;
|
||||
for (size_t i = 0; i < userinfo->len;) {
|
||||
char c = userinfo->data[i];
|
||||
if (c == '%' && i + 2 < userinfo->len) {
|
||||
@@ -2907,11 +2915,9 @@ static ScrStr *sf_proxy_authorization(const ScrStr *userinfo) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == ':') has_separator = true;
|
||||
plain[plain_len++] = c;
|
||||
i++;
|
||||
}
|
||||
if (!has_separator) plain[plain_len++] = ':';
|
||||
|
||||
static const char b64[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
@@ -4058,15 +4064,154 @@ static void sf_on_response(ScrClosure *cb, ScrHttpReq *res) {
|
||||
sf_release(t);
|
||||
}
|
||||
|
||||
typedef struct { int value; const char *name; } SfUvError;
|
||||
#define SF_UV_ERROR(name, message) { SCR_NODE_UV__##name, #name },
|
||||
static const SfUvError sf_uv_errors[] = { SCR_NODE_UV_ERRNO_MAP(SF_UV_ERROR) };
|
||||
#undef SF_UV_ERROR
|
||||
|
||||
/* libuv's errno value for a code name; getaddrinfo's ENOTFOUND is
|
||||
* EAI_NONAME. 0 when the name is not a libuv code. */
|
||||
static int sf_uv_errno(const char *name) {
|
||||
if (strcmp(name, "ENOTFOUND") == 0) return SCR_NODE_UV__EAI_NONAME;
|
||||
for (size_t i = 0; i < sizeof sf_uv_errors / sizeof sf_uv_errors[0]; i++) {
|
||||
if (strcmp(sf_uv_errors[i].name, name) == 0) return sf_uv_errors[i].value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char sf_tunnel_prefix[] = "Proxy response (";
|
||||
static const char sf_not_http_prefix[] = "Response does not match the HTTP/1.1 protocol";
|
||||
|
||||
/* The cause Node's fetch gives a transport failure, built from the
|
||||
* runtime's failure message. "<syscall> <CODE>[ <detail>]" messages
|
||||
* become Node's system error with code, errno and syscall (plus address
|
||||
* and port for connect, hostname for getaddrinfo). A tunnel the proxy
|
||||
* refused becomes undici's DOMException "Request was cancelled." whose
|
||||
* cause is the AbortError carrying the proxy status, and a proxy reply
|
||||
* that is not HTTP becomes the HTTPParserError. NULL (no cause) for
|
||||
* every other message. +1. */
|
||||
static ScrDyn *sf_failure_cause(const ScrStr *message) {
|
||||
if (message->len > sizeof sf_tunnel_prefix - 1 &&
|
||||
memcmp(message->data, sf_tunnel_prefix, sizeof sf_tunnel_prefix - 1) == 0) {
|
||||
ScrError *aborted = scr_error_new(SCR_ERR_ERROR, (ScrStr *)message);
|
||||
scr_str_release(aborted->name);
|
||||
aborted->name = scr_str_new("AbortError", 10);
|
||||
aborted->name_present = true;
|
||||
scr_error_set_code(aborted, "UND_ERR_ABORTED");
|
||||
ScrDyn *inner = scr_dyn_from_error(aborted);
|
||||
scr_error_release(aborted);
|
||||
ScrDyn *options = scr_dyn_new_obj();
|
||||
ScrStr *name = scr_str_new("Error", 5);
|
||||
scr_dyn_obj_set(options, "name", 4, scr_dyn_new_str(name));
|
||||
scr_str_release(name);
|
||||
scr_dyn_obj_set(options, "cause", 5, inner);
|
||||
ScrStr *text = scr_str_new("Request was cancelled.", 22);
|
||||
ScrDyn *m = scr_dyn_new_str(text);
|
||||
scr_str_release(text);
|
||||
ScrError *dom = scr_domex_new(m, options);
|
||||
scr_dyn_release(m);
|
||||
scr_dyn_release(options);
|
||||
ScrDyn *cause = scr_dyn_from_error(dom);
|
||||
scr_error_release(dom);
|
||||
return cause;
|
||||
}
|
||||
if (message->len >= sizeof sf_not_http_prefix - 1 &&
|
||||
memcmp(message->data, sf_not_http_prefix, sizeof sf_not_http_prefix - 1) == 0) {
|
||||
ScrError *parser = scr_error_new(SCR_ERR_ERROR, (ScrStr *)message);
|
||||
scr_str_release(parser->name);
|
||||
parser->name = scr_str_new("HTTPParserError", 15);
|
||||
parser->name_present = true;
|
||||
ScrDyn *cause = scr_dyn_from_error(parser);
|
||||
scr_error_release(parser);
|
||||
return cause;
|
||||
}
|
||||
const char *data = message->data;
|
||||
const char *space = memchr(data, ' ', message->len);
|
||||
if (space == NULL || space == data) return NULL;
|
||||
size_t syscall_len = (size_t)(space - data);
|
||||
const char *code = space + 1;
|
||||
const char *end = data + message->len;
|
||||
const char *code_end = code;
|
||||
while (code_end < end && *code_end != ' ') code_end++;
|
||||
char code_buf[32];
|
||||
size_t code_len = (size_t)(code_end - code);
|
||||
if (code_len < 2 || code_len >= sizeof code_buf || code[0] != 'E') return NULL;
|
||||
for (size_t i = 0; i < code_len; i++) {
|
||||
char c = code[i];
|
||||
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) return NULL;
|
||||
}
|
||||
memcpy(code_buf, code, code_len);
|
||||
code_buf[code_len] = '\0';
|
||||
int uv = sf_uv_errno(code_buf);
|
||||
if (uv == 0) return NULL;
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, (ScrStr *)message);
|
||||
scr_error_set_code(e, code_buf);
|
||||
e->system_errno = uv;
|
||||
e->system_call = scr_str_new(data, syscall_len);
|
||||
ScrDyn *cause = scr_dyn_from_error(e);
|
||||
scr_error_release(e);
|
||||
const char *detail = code_end < end ? code_end + 1 : end;
|
||||
size_t detail_len = (size_t)(end - detail);
|
||||
if (syscall_len == 7 && memcmp(data, "connect", 7) == 0 && detail_len > 0) {
|
||||
const char *colon = NULL;
|
||||
for (const char *q = detail; q < end; q++) {
|
||||
if (*q == ':') colon = q;
|
||||
}
|
||||
if (colon != NULL && colon > detail && colon + 1 < end) {
|
||||
ScrStr *address = scr_str_new(detail, (size_t)(colon - detail));
|
||||
scr_dyn_obj_set(cause, "address", 7, scr_dyn_new_str(address));
|
||||
scr_str_release(address);
|
||||
scr_dyn_obj_set(cause, "port", 4, scr_dyn_new_num(strtod(colon + 1, NULL)));
|
||||
}
|
||||
} else if (syscall_len == 11 && memcmp(data, "getaddrinfo", 11) == 0 && detail_len > 0) {
|
||||
ScrStr *hostname = scr_str_new(detail, detail_len);
|
||||
scr_dyn_obj_set(cause, "hostname", 8, scr_dyn_new_str(hostname));
|
||||
scr_str_release(hostname);
|
||||
}
|
||||
return cause;
|
||||
}
|
||||
|
||||
/* A plain Error cause. +1. */
|
||||
static ScrDyn *sf_plain_cause(const char *message) {
|
||||
ScrStr *text = scr_str_new(message, strlen(message));
|
||||
ScrError *e = scr_error_new(SCR_ERR_ERROR, text);
|
||||
scr_str_release(text);
|
||||
ScrDyn *cause = scr_dyn_from_error(e);
|
||||
scr_error_release(e);
|
||||
return cause;
|
||||
}
|
||||
|
||||
/* Rejects with TypeError "fetch failed" whose cause is `cause` (moves;
|
||||
* NULL rejects without a cause). */
|
||||
static void sf_reject_cause(SfTransfer *t, ScrDyn *cause) {
|
||||
if (t->done || cause == NULL) {
|
||||
scr_dyn_release(cause);
|
||||
sf_reject(t, "fetch failed");
|
||||
return;
|
||||
}
|
||||
ScrError *e = NULL;
|
||||
ScrDyn *reason = sf_type_reason("fetch failed", &e);
|
||||
scr_error_set_cause(e, cause);
|
||||
scr_dyn_release(cause);
|
||||
scr_error_release(e);
|
||||
sf_reject_reason(t, reason);
|
||||
scr_dyn_release(reason);
|
||||
}
|
||||
|
||||
/* Rejects with TypeError "fetch failed", with Node's cause when the
|
||||
* failure message has one. */
|
||||
static void sf_reject_failure(SfTransfer *t, const ScrStr *message) {
|
||||
sf_reject_cause(t, message != NULL ? sf_failure_cause(message) : NULL);
|
||||
}
|
||||
|
||||
static void sf_on_client_error(ScrClosure *cb, ScrStr *message) {
|
||||
(void)message;
|
||||
SfTransfer *t = sf_from(cb);
|
||||
if (!t) return;
|
||||
if (!t->done) {
|
||||
if (t->response_sent && t->response_stream) {
|
||||
sf_response_terminate(t);
|
||||
} else {
|
||||
sf_reject(t, "fetch failed");
|
||||
sf_reject_failure(t, message);
|
||||
}
|
||||
}
|
||||
sf_release(t);
|
||||
@@ -4150,25 +4295,77 @@ static bool sf_redirect(SfTransfer *t, int status,
|
||||
return true;
|
||||
}
|
||||
|
||||
/* A CONNECT tunnel for one request: the head sent to the proxy, and the
|
||||
* target's TLS client context when the target is https. */
|
||||
typedef struct {
|
||||
char *head;
|
||||
size_t head_len;
|
||||
void *tls_ctx;
|
||||
} SfTunnel;
|
||||
|
||||
/* undici's CONNECT head: the target as host:port, its Host value, and
|
||||
* the proxy credentials from the proxy URL. Returns malloc'd storage. */
|
||||
static char *sf_tunnel_head(const ScrUrl *url, int port, const char *authority,
|
||||
size_t authority_len, const ScrUrl *proxy, size_t *len_out) {
|
||||
ScrStr *authorization = sf_proxy_has_credentials(proxy->userinfo)
|
||||
? sf_proxy_authorization(proxy->userinfo)
|
||||
: NULL;
|
||||
size_t cap = url->host->len + authority_len + 160 + (authorization ? authorization->len : 0);
|
||||
char *head = malloc(cap);
|
||||
if (!head) sf_oom();
|
||||
int n = snprintf(head, cap, "CONNECT %.*s:%d HTTP/1.1\r\nhost: %.*s\r\nconnection: close\r\n",
|
||||
(int)url->host->len, url->host->data, port, (int)authority_len, authority);
|
||||
if (authorization) {
|
||||
n += snprintf(head + n, cap - (size_t)n, "proxy-authorization: %.*s\r\n",
|
||||
(int)authorization->len, authorization->data);
|
||||
scr_str_release(authorization);
|
||||
}
|
||||
n += snprintf(head + n, cap - (size_t)n, "proxy-connection: keep-alive\r\n\r\n");
|
||||
*len_out = (size_t)n;
|
||||
return head;
|
||||
}
|
||||
|
||||
static SfTunnel *sf_tunnel_new(const ScrUrl *url, int port, const char *authority,
|
||||
size_t authority_len, const ScrUrl *proxy, void *tls_ctx) {
|
||||
SfTunnel *tunnel = malloc(sizeof *tunnel);
|
||||
if (!tunnel) sf_oom();
|
||||
tunnel->head = sf_tunnel_head(url, port, authority, authority_len, proxy, &tunnel->head_len);
|
||||
tunnel->tls_ctx = tls_ctx;
|
||||
return tunnel;
|
||||
}
|
||||
|
||||
/* The client's wrap hook for a tunneled request: the CONNECT exchange
|
||||
* runs on the proxy socket, then the target's TLS session (if any) runs
|
||||
* inside the tunnel. */
|
||||
static void sf_tunnel_wrap(ScrNetSocket *sock, void *ctx) {
|
||||
SfTunnel *tunnel = ctx;
|
||||
scr_net_sock_set_tunnel(sock, tunnel->head, tunnel->head_len);
|
||||
if (tunnel->tls_ctx) scr_tls_fetch_client_wrap(sock, tunnel->tls_ctx);
|
||||
free(tunnel->head);
|
||||
free(tunnel);
|
||||
}
|
||||
|
||||
static bool sf_start_hop(SfTransfer *t) {
|
||||
ScrUrl *url = t->url;
|
||||
bool https = sf_eq_ci(url->scheme, "https");
|
||||
int default_port = https ? 443 : 80;
|
||||
int port = sf_port(url, default_port);
|
||||
if (sf_bad_port(port)) {
|
||||
sf_reject(t, "fetch failed");
|
||||
sf_reject_cause(t, sf_plain_cause("bad port"));
|
||||
return false;
|
||||
}
|
||||
bool invalid_proxy = false;
|
||||
ScrUrl *proxy = sf_proxy_for(url, https, port, &invalid_proxy);
|
||||
bool proxy_http = proxy && sf_eq_ci(proxy->scheme, "http");
|
||||
bool proxy_https = proxy && sf_eq_ci(proxy->scheme, "https");
|
||||
if (invalid_proxy || (proxy && !proxy_http && !proxy_https) ||
|
||||
(https && proxy)) {
|
||||
if (invalid_proxy || (proxy && !proxy_http && !proxy_https)) {
|
||||
scr_url_release(proxy);
|
||||
sf_reject(t, "fetch failed");
|
||||
return false;
|
||||
}
|
||||
/* A proxy carries every request through a CONNECT tunnel, as undici's
|
||||
* ProxyAgent does; an https:// proxy runs the tunnel inside a TLS
|
||||
* session to the proxy. */
|
||||
size_t user_len = (size_t)scr_arr_len(t->headers);
|
||||
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, user_len + 16);
|
||||
|
||||
@@ -4214,13 +4411,6 @@ static bool sf_start_hop(SfTransfer *t) {
|
||||
scr_arr_push_ref(headers, name);
|
||||
scr_arr_push_ref(headers, value);
|
||||
}
|
||||
if (proxy && proxy->userinfo->len > 0 &&
|
||||
!sf_pairs_have(t->headers, "proxy-authorization")) {
|
||||
ScrStr *authorization = sf_proxy_authorization(proxy->userinfo);
|
||||
sf_push_header_text(headers, "proxy-authorization",
|
||||
authorization->data, authorization->len);
|
||||
scr_str_release(authorization);
|
||||
}
|
||||
if (!sf_pairs_have(t->headers, "accept")) {
|
||||
sf_push_header_text(headers, "accept", "*/*", 3);
|
||||
}
|
||||
@@ -4240,17 +4430,29 @@ static bool sf_start_hop(SfTransfer *t) {
|
||||
ScrStr *bare =
|
||||
sf_bare_host(proxy ? proxy->host : url->host);
|
||||
ScrStr *dial = scr_net_blocking_lookup(bare);
|
||||
ScrStr *path = proxy ? sf_url_serialize(url) : sf_path(url);
|
||||
ScrStr *path = sf_path(url);
|
||||
int dial_port =
|
||||
proxy ? sf_port(proxy, proxy_https ? 443 : 80) : port;
|
||||
ScrStr *sni =
|
||||
https ? sf_bare_host(url->host)
|
||||
: proxy_https ? sf_bare_host(proxy->host) : NULL;
|
||||
void *tls_ctx =
|
||||
sni ? scr_tls_fetch_client_ctx(sni, true) : NULL;
|
||||
ScrStr *sni = https ? sf_bare_host(url->host) : NULL;
|
||||
void (*wrap)(ScrNetSocket *, void *) = NULL;
|
||||
void *wrap_ctx = NULL;
|
||||
if (proxy_https) {
|
||||
size_t head_len = 0;
|
||||
char *head = sf_tunnel_head(url, port, authority, (size_t)authority_len, proxy, &head_len);
|
||||
wrap_ctx = scr_tls_fetch_tunnel_ctx(bare, sni, head, head_len);
|
||||
wrap = &scr_tls_fetch_tunnel_wrap;
|
||||
free(head);
|
||||
} else if (proxy_http) {
|
||||
void *tls_ctx = sni ? scr_tls_fetch_client_ctx(sni, true) : NULL;
|
||||
wrap_ctx = sf_tunnel_new(url, port, authority, (size_t)authority_len, proxy, tls_ctx);
|
||||
wrap = &sf_tunnel_wrap;
|
||||
} else if (sni) {
|
||||
wrap_ctx = scr_tls_fetch_client_ctx(sni, true);
|
||||
wrap = &scr_tls_fetch_client_wrap;
|
||||
}
|
||||
ScrHttpClientReq *client = scr_http_request_ex(
|
||||
dial, dial_port, path, t->method, 0, headers, false, NULL, NULL,
|
||||
default_port, sni ? &scr_tls_fetch_client_wrap : NULL, tls_ctx);
|
||||
default_port, wrap, wrap_ctx);
|
||||
scr_arr_release(headers);
|
||||
scr_str_release(path);
|
||||
scr_str_release(dial);
|
||||
|
||||
@@ -196,6 +196,13 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port
|
||||
* the fresh socket. The ctx moves into the transport at wrap time. */
|
||||
void *scr_tls_fetch_client_ctx(ScrStr *host /*borrowed*/, bool reject_unauthorized);
|
||||
void scr_tls_fetch_client_wrap(ScrNetSocket *sock, void *ctx);
|
||||
/* The fetch tunnel through an https:// proxy: TLS to the proxy, the
|
||||
* CONNECT head (copied) inside it, then TLS to target_host inside the
|
||||
* tunnel (NULL target_host: an http:// target's plaintext). The ctx moves
|
||||
* into the socket's transport at wrap time. */
|
||||
void *scr_tls_fetch_tunnel_ctx(ScrStr *proxy_host /*borrowed*/, ScrStr *target_host /*borrowed, nullable*/,
|
||||
const char *request, size_t len);
|
||||
void scr_tls_fetch_tunnel_wrap(ScrNetSocket *sock, void *ctx);
|
||||
/* The loop-side registration (scr_async.c, always linked): `pending`
|
||||
* joins the exhaustion test (a live handle keeps the process alive, like
|
||||
* Node's active TCP handles), `dispatch` runs at every turn top like the
|
||||
|
||||
+165
-10
@@ -593,8 +593,28 @@ struct ScrNetSocket {
|
||||
bool t_want_write; /* a handshake send hit EAGAIN: arm the write filter */
|
||||
bool in_registry;
|
||||
struct ScrNetSocket *next;
|
||||
/* an HTTP CONNECT tunnel (scr_net_sock_set_tunnel), set from the dial
|
||||
* until the proxy answers 200: writes buffer and the transport
|
||||
* handshake waits until the tunnel is up */
|
||||
struct ScrNetTunnel *tunnel;
|
||||
};
|
||||
|
||||
/* The CONNECT head still being sent from out_off, and the proxy's
|
||||
* response head as it arrives. */
|
||||
typedef struct ScrNetTunnel {
|
||||
char *out;
|
||||
size_t out_len, out_off;
|
||||
char *in;
|
||||
size_t in_len;
|
||||
} ScrNetTunnel;
|
||||
|
||||
static void scr_net_tunnel_free(ScrNetTunnel *t) {
|
||||
if (t == NULL) return;
|
||||
free(t->out);
|
||||
free(t->in);
|
||||
free(t);
|
||||
}
|
||||
|
||||
#ifdef SCR_RC_AUDIT
|
||||
static SCR_TL long scr_net_live = 0;
|
||||
long scr_net_live_count(void) { return scr_net_live; }
|
||||
@@ -665,6 +685,7 @@ void scr_net_sock_release(ScrNetSocket *s) {
|
||||
free(s->dial_ips);
|
||||
scr_str_release(s->dial_err);
|
||||
scr_str_release(s->dial_host);
|
||||
scr_net_tunnel_free(s->tunnel);
|
||||
free(s->wbuf);
|
||||
free(s->rbuf);
|
||||
if (s->pipe_dst) scr_net_sock_release(s->pipe_dst);
|
||||
@@ -910,6 +931,7 @@ static ScrNetSocket *scr_net_sock_new(void) {
|
||||
static void scr_net_sock_update_read(ScrNetSocket *s) {
|
||||
bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused &&
|
||||
(s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL ||
|
||||
s->tunnel != NULL /* the proxy's CONNECT answer */ ||
|
||||
s->flowing /* resume(): flow (and discard sans listeners) */ ||
|
||||
s->readable_ls.n > 0 /* paused-mode consumer: bytes buffer */ ||
|
||||
s->tops != NULL /* a transport is ALWAYS a consumer: the TLS
|
||||
@@ -935,7 +957,9 @@ static void scr_net_sock_update_read(ScrNetSocket *s) {
|
||||
}
|
||||
|
||||
static void scr_net_sock_update_write(ScrNetSocket *s) {
|
||||
bool want = s->fd >= 0 && (s->connecting || s->wlen > s->whead || s->t_want_write);
|
||||
bool want = s->fd >= 0 &&
|
||||
(s->connecting || (s->tunnel != NULL ? s->tunnel->out_off < s->tunnel->out_len
|
||||
: s->wlen > s->whead || s->t_want_write));
|
||||
if (want && !s->write_armed) {
|
||||
scr_net_watch_write(s->fd, s, true);
|
||||
s->write_armed = true;
|
||||
@@ -960,7 +984,7 @@ static void scr_net_sock_close_fd(ScrNetSocket *s) {
|
||||
/* The FIN half: called when the write half is ENDING and the buffer just
|
||||
* drained (or was empty). Full close once the read half is done too. */
|
||||
static void scr_net_sock_maybe_finish_write(ScrNetSocket *s) {
|
||||
if (s->fd < 0 || !s->wr_ending || s->wr_done || s->wlen > s->whead) return;
|
||||
if (s->fd < 0 || !s->wr_ending || s->wr_done || s->wlen > s->whead || s->tunnel != NULL) return;
|
||||
/* a transport says goodbye first (TLS close_notify), best-effort */
|
||||
if (s->tops && s->t_est) s->tops->shutdown_write(s->tctx);
|
||||
shutdown(s->fd, SHUT_WR);
|
||||
@@ -1009,7 +1033,8 @@ static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len
|
||||
if (!pre_dial && (s->fd < 0 || s->wr_ending || s->close_emitted)) return;
|
||||
if (len == 0) return;
|
||||
s->bytes_written += len; /* accepted bytes (Node counts buffered ones too) */
|
||||
if (pre_dial || s->connecting || s->wlen > s->whead || (s->tops && !s->t_est)) {
|
||||
if (pre_dial || s->connecting || s->tunnel != NULL || s->wlen > s->whead ||
|
||||
(s->tops && !s->t_est)) {
|
||||
/* mid-connect and mid-handshake bytes buffer; the flush after
|
||||
* 'connect'/establishment sends them */
|
||||
scr_net_sock_buffer(s, data, len);
|
||||
@@ -1053,7 +1078,7 @@ void scr_net_sock_writev_native(ScrNetSocket *s, const ScrNetSlice *slices, size
|
||||
int cnt = 0;
|
||||
size_t total = 0;
|
||||
bool immediate = n <= 8 && s->fd >= 0 && !s->wr_ending && !s->close_emitted &&
|
||||
!s->connecting && s->wlen == s->whead && !s->tops;
|
||||
!s->connecting && s->tunnel == NULL && s->wlen == s->whead && !s->tops;
|
||||
if (immediate) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (slices[i].len == 0) continue;
|
||||
@@ -1106,6 +1131,7 @@ void scr_net_sock_writev_native(ScrNetSocket *s, const ScrNetSlice *slices, size
|
||||
|
||||
/* Flush on write-readiness; drives the FIN when ENDING. */
|
||||
static void scr_net_sock_flush(ScrNetSocket *s) {
|
||||
if (s->tunnel != NULL) return; /* nothing moves until the proxy answers */
|
||||
if (s->tops && !s->t_est) return; /* nothing moves until the handshake ends */
|
||||
while (s->fd >= 0 && s->wlen > s->whead) {
|
||||
ssize_t sent = s->tops ? s->tops->xwrite(s->tctx, s->wbuf + s->whead, s->wlen - s->whead)
|
||||
@@ -1206,6 +1232,7 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s);
|
||||
|
||||
static void scr_net_sock_read(ScrNetSocket *s) {
|
||||
size_t reads_left = 32; /* at most 2 MiB with the 64 KiB read buffer */
|
||||
if (s->tunnel != NULL) return; /* readiness feeds the tunnel pump instead */
|
||||
if (s->tops && !s->t_est) return; /* readiness feeds the handshake pump instead */
|
||||
for (;;) {
|
||||
if (s->fd < 0 || s->user_paused) return;
|
||||
@@ -1298,6 +1325,8 @@ static void scr_net_sock_read(ScrNetSocket *s) {
|
||||
static void scr_net_sock_transport_pump(ScrNetSocket *s);
|
||||
|
||||
static void scr_net_sock_dial_next(ScrNetSocket *s);
|
||||
static void scr_net_sock_connected(ScrNetSocket *s);
|
||||
static void scr_net_sock_tunnel_pump(ScrNetSocket *s);
|
||||
|
||||
/* Connect completion (write-readiness while connecting). */
|
||||
static void scr_net_sock_connect_done(ScrNetSocket *s) {
|
||||
@@ -1342,6 +1371,19 @@ static void scr_net_sock_connect_done(ScrNetSocket *s) {
|
||||
scr_str_release(s->dial_err);
|
||||
s->dial_err = NULL;
|
||||
}
|
||||
if (s->tunnel != NULL) {
|
||||
/* TCP to the proxy is up: the CONNECT exchange runs first */
|
||||
scr_net_sock_touch(s);
|
||||
scr_net_sock_tunnel_pump(s);
|
||||
return;
|
||||
}
|
||||
scr_net_sock_connected(s);
|
||||
}
|
||||
|
||||
/* The connection is usable (TCP up, and the proxy tunnel when there is
|
||||
* one): the transport handshake starts, or 'connect' fires and buffered
|
||||
* writes flush. */
|
||||
static void scr_net_sock_connected(ScrNetSocket *s) {
|
||||
scr_net_sock_update_write(s); /* keeps the filter iff bytes are buffered */
|
||||
scr_net_sock_update_read(s);
|
||||
scr_net_sock_touch(s);
|
||||
@@ -1363,6 +1405,106 @@ static void scr_net_sock_connect_done(ScrNetSocket *s) {
|
||||
scr_net_sock_flush(s);
|
||||
}
|
||||
|
||||
/* Fails a tunnel being set up: the message is the socket's 'error'. */
|
||||
static void scr_net_sock_tunnel_fail(ScrNetSocket *s, const char *msg) {
|
||||
if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg));
|
||||
s->had_error = true;
|
||||
scr_net_sock_close_fd(s);
|
||||
}
|
||||
|
||||
int scr_net_tunnel_status(const char *head, size_t len) {
|
||||
if (len >= 12 && memcmp(head, "HTTP/1.", 7) == 0 && head[8] == ' ' &&
|
||||
isdigit((unsigned char)head[9]) && isdigit((unsigned char)head[10]) &&
|
||||
isdigit((unsigned char)head[11])) {
|
||||
return (head[9] - '0') * 100 + (head[10] - '0') * 10 + (head[11] - '0');
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void scr_net_tunnel_failure(int status, char *msg, size_t cap) {
|
||||
if (status < 0) {
|
||||
/* llhttp's message for a response that does not start with a status line */
|
||||
snprintf(msg, cap, "Response does not match the HTTP/1.1 protocol (Expected HTTP/, RTSP/ or ICE/)");
|
||||
} else {
|
||||
snprintf(msg, cap, "Proxy response (%d) !== 200 when HTTP Tunneling", status);
|
||||
}
|
||||
}
|
||||
|
||||
/* The proxy's response head is at most this long. */
|
||||
#define SCR_NET_TUNNEL_HEAD_MAX 16384u
|
||||
|
||||
/* Drives the CONNECT exchange off readiness: sends the request head,
|
||||
* then reads the proxy's response head. A 200 status makes the
|
||||
* connection usable (bytes after the head belong to the tunnel); any
|
||||
* other status fails the socket with undici's message. */
|
||||
static void scr_net_sock_tunnel_pump(ScrNetSocket *s) {
|
||||
ScrNetTunnel *t = s->tunnel;
|
||||
while (s->fd >= 0 && t->out_off < t->out_len) {
|
||||
ssize_t sent =
|
||||
scr_net_send(s->fd, t->out + t->out_off, t->out_len - t->out_off);
|
||||
if (sent < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof msg, "write %s", scr_net_errname(errno));
|
||||
scr_net_sock_tunnel_fail(s, msg);
|
||||
return;
|
||||
}
|
||||
t->out_off += (size_t)sent;
|
||||
}
|
||||
scr_net_sock_update_write(s);
|
||||
if (s->fd < 0 || t->out_off < t->out_len) return;
|
||||
for (;;) {
|
||||
char buf[4096];
|
||||
ssize_t n = read(s->fd, buf, sizeof buf);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
scr_net_sock_update_read(s);
|
||||
return;
|
||||
}
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof msg, "read %s", scr_net_errname(errno));
|
||||
scr_net_sock_tunnel_fail(s, msg);
|
||||
return;
|
||||
}
|
||||
if (n == 0) {
|
||||
scr_net_sock_tunnel_fail(s, "socket hang up");
|
||||
return;
|
||||
}
|
||||
if (t->in_len + (size_t)n > SCR_NET_TUNNEL_HEAD_MAX) {
|
||||
scr_net_sock_tunnel_fail(s, "Proxy response head is too large");
|
||||
return;
|
||||
}
|
||||
char *grown = realloc(t->in, t->in_len + (size_t)n);
|
||||
if (!grown) scr_net_oom();
|
||||
t->in = grown;
|
||||
memcpy(t->in + t->in_len, buf, (size_t)n);
|
||||
t->in_len += (size_t)n;
|
||||
size_t end = 0;
|
||||
for (size_t i = 3; i < t->in_len; i++) {
|
||||
if (memcmp(t->in + i - 3, "\r\n\r\n", 4) == 0) {
|
||||
end = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (end == 0) continue;
|
||||
int status = scr_net_tunnel_status(t->in, end);
|
||||
if (status != 200) {
|
||||
char msg[128];
|
||||
scr_net_tunnel_failure(status, msg, sizeof msg);
|
||||
scr_net_sock_tunnel_fail(s, msg);
|
||||
return;
|
||||
}
|
||||
scr_net_sock_append_rbuf(s, t->in + end, t->in_len - end);
|
||||
scr_net_tunnel_free(t);
|
||||
s->tunnel = NULL;
|
||||
scr_net_sock_touch(s);
|
||||
scr_net_sock_connected(s);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Drive the transport handshake off readiness until it settles. On
|
||||
* establishment: the protocol layer attaches (https' parser), a
|
||||
* deferred server 'connection' fires ('secureConnection' timing), the
|
||||
@@ -1371,7 +1513,7 @@ static void scr_net_sock_connect_done(ScrNetSocket *s) {
|
||||
* handshake flight delivers. Failure already tore the socket down via
|
||||
* scr_net_sock_transport_error. */
|
||||
static void scr_net_sock_transport_pump(ScrNetSocket *s) {
|
||||
if (!s->tops || s->t_est || s->fd < 0 || s->connecting) return;
|
||||
if (!s->tops || s->t_est || s->fd < 0 || s->connecting || s->tunnel != NULL) return;
|
||||
int r = s->tops->handshake(s->tctx);
|
||||
if (r == 0) {
|
||||
scr_net_sock_update_read(s);
|
||||
@@ -3907,9 +4049,9 @@ ScrDyn *scr_net_sock_addr_field(ScrNetSocket *s, double field) {
|
||||
double scr_net_sock_bytes_read(ScrNetSocket *s) { return (double)s->bytes_read; }
|
||||
|
||||
/* socket.connecting: a dial in progress (deferred, looking up, or
|
||||
* awaiting the TCP handshake). */
|
||||
* awaiting the TCP handshake or the proxy tunnel). */
|
||||
bool scr_net_sock_connecting(ScrNetSocket *s) {
|
||||
return s->fd >= 0 ? s->connecting : (s->dial_deferred || s->lookup_wait);
|
||||
return s->fd >= 0 ? s->connecting || s->tunnel != NULL : (s->dial_deferred || s->lookup_wait);
|
||||
}
|
||||
|
||||
/* socket.pending: no open handle yet (or anymore), or still connecting. */
|
||||
@@ -4253,7 +4395,7 @@ void scr_net_sock_on_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool onc
|
||||
scr_closure_release(cb);
|
||||
return;
|
||||
}
|
||||
if (!s->connecting) {
|
||||
if (!s->connecting && s->tunnel == NULL) {
|
||||
/* already connected: Node fires a late once('connect')… never — the
|
||||
* event is past. Release, like listeners after 'exit'. */
|
||||
scr_closure_release(cb);
|
||||
@@ -4365,7 +4507,7 @@ bool scr_net_sock_writable(ScrNetSocket *s) {
|
||||
}
|
||||
|
||||
bool scr_net_sock_established(ScrNetSocket *s) {
|
||||
return s->fd >= 0 && !s->connecting && (!s->tops || s->t_est);
|
||||
return s->fd >= 0 && !s->connecting && s->tunnel == NULL && (!s->tops || s->t_est);
|
||||
}
|
||||
|
||||
/* The protocol layer's timeout/error hooks (scr_http.c's client and the
|
||||
@@ -4393,6 +4535,17 @@ void scr_net_sock_write_native(ScrNetSocket *s, const char *buf, size_t n) {
|
||||
|
||||
/* ── the transport hooks (scr_tls.c) ─────────────────────────────────── */
|
||||
|
||||
void scr_net_sock_set_tunnel(ScrNetSocket *s, const char *request, size_t len) {
|
||||
if (s->fd < 0 || !s->connecting || s->tunnel != NULL) return;
|
||||
ScrNetTunnel *t = calloc(1, sizeof *t);
|
||||
if (!t) scr_net_oom();
|
||||
t->out = malloc(len);
|
||||
if (!t->out) scr_net_oom();
|
||||
memcpy(t->out, request, len);
|
||||
t->out_len = len;
|
||||
s->tunnel = t;
|
||||
}
|
||||
|
||||
void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx) {
|
||||
s->tops = ops;
|
||||
s->tctx = tctx;
|
||||
@@ -4811,6 +4964,7 @@ static void scr_net_dispatch(void) {
|
||||
* failure may have closed. */
|
||||
if (evs[i].events & SCRP_WRITABLE) {
|
||||
if (s->connecting) scr_net_sock_connect_done(s);
|
||||
else if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s);
|
||||
else if (s->tops && !s->t_est) {
|
||||
s->t_want_write = false;
|
||||
scr_net_sock_transport_pump(s);
|
||||
@@ -4821,7 +4975,8 @@ static void scr_net_dispatch(void) {
|
||||
}
|
||||
}
|
||||
if ((evs[i].events & SCRP_READABLE) && !scr_exc_pending() && s->fd >= 0) {
|
||||
if (s->tops && !s->t_est) scr_net_sock_transport_pump(s);
|
||||
if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s);
|
||||
else if (s->tops && !s->t_est) scr_net_sock_transport_pump(s);
|
||||
else scr_net_sock_read(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +341,18 @@ typedef struct ScrNetTransportOps {
|
||||
void (*free)(void *tctx);
|
||||
} ScrNetTransportOps;
|
||||
void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx);
|
||||
/* Routes a client socket through an HTTP CONNECT proxy. Call it right
|
||||
* after the dial (scr_net_connect to the proxy) and before any transport
|
||||
* is set: once TCP is up, `request` (the CONNECT head, copied) is sent
|
||||
* and the proxy's response head is read. A 200 status starts the
|
||||
* transport handshake, or fires 'connect'; any other status fails the
|
||||
* socket with "Proxy response (<status>) !== 200 when HTTP Tunneling". */
|
||||
void scr_net_sock_set_tunnel(ScrNetSocket *s, const char *request, size_t len);
|
||||
/* The status code of a CONNECT response head (head[0..len) ends with the
|
||||
* blank line), or -1 when it does not start with an HTTP/1.x status
|
||||
* line; and undici's message for a status other than 200 (or -1). */
|
||||
int scr_net_tunnel_status(const char *head, size_t len);
|
||||
void scr_net_tunnel_failure(int status, char *msg, size_t cap);
|
||||
int scr_net_sock_fd(ScrNetSocket *s);
|
||||
/* Drain up to n bytes of the socket's receive buffer (peeked/unshifted
|
||||
* bytes — the demux path); consumers call it before the fd. */
|
||||
|
||||
@@ -1089,6 +1089,234 @@ void scr_tls_fetch_client_wrap(ScrNetSocket *sock, void *ctx) {
|
||||
scr_tls_cli_wrap(sock, ctx);
|
||||
}
|
||||
|
||||
/* ── the fetch tunnel through an https:// proxy ───────────────────────
|
||||
* One transport carries three stages over the proxy socket: the TLS
|
||||
* session to the proxy (verified against the proxy host), the CONNECT
|
||||
* exchange inside it, then the target's TLS session (verified against
|
||||
* the target host) inside the tunnel. An http:// target uses the
|
||||
* tunnel's plaintext directly. */
|
||||
|
||||
enum { SCR_TUN_OUTER = 0, SCR_TUN_SEND, SCR_TUN_HEAD, SCR_TUN_INNER, SCR_TUN_UP };
|
||||
|
||||
typedef struct {
|
||||
ScrTlsSock *outer; /* the session to the proxy */
|
||||
ScrTlsSock *inner; /* the session to the target; NULL for http:// */
|
||||
int stage;
|
||||
char *req; /* the CONNECT head */
|
||||
size_t req_len, req_off;
|
||||
char *head; /* the proxy's response head as it arrives */
|
||||
size_t head_len;
|
||||
char *extra; /* tunnel bytes that arrived with the response head */
|
||||
size_t extra_len, extra_off;
|
||||
} ScrTlsTunnel;
|
||||
|
||||
static ScrTlsSock *scr_tls_cli_sock(ScrNetSocket *sock, ScrTlsCli *cli) {
|
||||
ScrTlsSock *t = calloc(1, sizeof *t);
|
||||
if (!t) scr_tls_oom();
|
||||
mbedtls_ssl_init(&t->ssl);
|
||||
if (mbedtls_ssl_setup(&t->ssl, &cli->conf) != 0) scr_tls_oom();
|
||||
(void)mbedtls_ssl_set_hostname(&t->ssl, cli->hostname);
|
||||
mbedtls_ssl_set_verify(&t->ssl, &scr_tls_vrfy_cb, t);
|
||||
t->sock = sock;
|
||||
t->cli = cli;
|
||||
return t;
|
||||
}
|
||||
|
||||
/* Reads tunnel plaintext: bytes that arrived with the proxy's head
|
||||
* first, then the proxy session. mbedTLS return conventions. */
|
||||
static int scr_tls_tunnel_read(ScrTlsTunnel *tun, unsigned char *buf, size_t len) {
|
||||
if (tun->extra_off < tun->extra_len) {
|
||||
size_t n = tun->extra_len - tun->extra_off;
|
||||
if (n > len) n = len;
|
||||
memcpy(buf, tun->extra + tun->extra_off, n);
|
||||
tun->extra_off += n;
|
||||
return (int)n;
|
||||
}
|
||||
int r = mbedtls_ssl_read(&tun->outer->ssl, buf, len);
|
||||
if (r == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) return 0;
|
||||
#if defined(MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET)
|
||||
if (r == MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET) return MBEDTLS_ERR_SSL_WANT_READ;
|
||||
#endif
|
||||
return r;
|
||||
}
|
||||
|
||||
/* The target session's BIOs: records travel inside the proxy session. */
|
||||
static int scr_tls_tunnel_bio_send(void *ctx, const unsigned char *buf, size_t len) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)ctx;
|
||||
return mbedtls_ssl_write(&tun->outer->ssl, buf, len);
|
||||
}
|
||||
|
||||
static int scr_tls_tunnel_bio_recv(void *ctx, unsigned char *buf, size_t len) {
|
||||
return scr_tls_tunnel_read((ScrTlsTunnel *)ctx, buf, len);
|
||||
}
|
||||
|
||||
static int scr_tls_tunnel_handshake(void *tctx) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx;
|
||||
ScrNetSocket *sock = tun->outer->sock;
|
||||
if (tun->stage == SCR_TUN_OUTER) {
|
||||
int r = scr_tls_handshake(tun->outer);
|
||||
if (r <= 0) return r;
|
||||
tun->outer->established = true;
|
||||
tun->stage = SCR_TUN_SEND;
|
||||
}
|
||||
if (tun->stage == SCR_TUN_SEND) {
|
||||
while (tun->req_off < tun->req_len) {
|
||||
int r = mbedtls_ssl_write(&tun->outer->ssl, (const unsigned char *)tun->req + tun->req_off,
|
||||
tun->req_len - tun->req_off);
|
||||
if (r == MBEDTLS_ERR_SSL_WANT_READ || r == MBEDTLS_ERR_SSL_WANT_WRITE) return 0;
|
||||
if (r < 0) {
|
||||
scr_net_sock_transport_error(sock, "write EPIPE");
|
||||
return -1;
|
||||
}
|
||||
tun->req_off += (size_t)r;
|
||||
}
|
||||
tun->stage = SCR_TUN_HEAD;
|
||||
}
|
||||
if (tun->stage == SCR_TUN_HEAD) {
|
||||
for (;;) {
|
||||
unsigned char buf[4096];
|
||||
int r = scr_tls_tunnel_read(tun, buf, sizeof buf);
|
||||
if (r == MBEDTLS_ERR_SSL_WANT_READ || r == MBEDTLS_ERR_SSL_WANT_WRITE) return 0;
|
||||
if (r < 0 && r != MBEDTLS_ERR_SSL_CONN_EOF) {
|
||||
scr_net_sock_transport_error(sock, "read ECONNRESET");
|
||||
return -1;
|
||||
}
|
||||
if (r <= 0) {
|
||||
scr_net_sock_transport_error(sock, "socket hang up");
|
||||
return -1;
|
||||
}
|
||||
if (tun->head_len + (size_t)r > 16384) {
|
||||
scr_net_sock_transport_error(sock, "Proxy response head is too large");
|
||||
return -1;
|
||||
}
|
||||
char *grown = realloc(tun->head, tun->head_len + (size_t)r);
|
||||
if (!grown) scr_tls_oom();
|
||||
tun->head = grown;
|
||||
memcpy(tun->head + tun->head_len, buf, (size_t)r);
|
||||
tun->head_len += (size_t)r;
|
||||
size_t end = 0;
|
||||
for (size_t i = 3; i < tun->head_len; i++) {
|
||||
if (memcmp(tun->head + i - 3, "\r\n\r\n", 4) == 0) {
|
||||
end = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (end == 0) continue;
|
||||
int status = scr_net_tunnel_status(tun->head, end);
|
||||
if (status != 200) {
|
||||
char msg[128];
|
||||
scr_net_tunnel_failure(status, msg, sizeof msg);
|
||||
scr_net_sock_transport_error(sock, msg);
|
||||
return -1;
|
||||
}
|
||||
tun->extra = malloc(tun->head_len - end + 1);
|
||||
if (!tun->extra) scr_tls_oom();
|
||||
memcpy(tun->extra, tun->head + end, tun->head_len - end);
|
||||
tun->extra_len = tun->head_len - end;
|
||||
tun->extra_off = 0;
|
||||
free(tun->head);
|
||||
tun->head = NULL;
|
||||
tun->head_len = 0;
|
||||
tun->stage = tun->inner != NULL ? SCR_TUN_INNER : SCR_TUN_UP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tun->stage == SCR_TUN_INNER) {
|
||||
int r = scr_tls_handshake(tun->inner);
|
||||
if (r <= 0) return r;
|
||||
tun->stage = SCR_TUN_UP;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static ssize_t scr_tls_tunnel_xread(void *tctx, char *buf, size_t n) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx;
|
||||
if (tun->inner != NULL) return scr_tls_xread(tun->inner, buf, n);
|
||||
if (tun->extra_off < tun->extra_len) {
|
||||
return scr_tls_tunnel_read(tun, (unsigned char *)buf, n);
|
||||
}
|
||||
return scr_tls_xread(tun->outer, buf, n);
|
||||
}
|
||||
|
||||
static ssize_t scr_tls_tunnel_xwrite(void *tctx, const char *buf, size_t n) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx;
|
||||
return scr_tls_xwrite(tun->inner != NULL ? tun->inner : tun->outer, buf, n);
|
||||
}
|
||||
|
||||
static void scr_tls_tunnel_on_established(void *tctx) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx;
|
||||
if (tun->inner != NULL) tun->inner->established = true;
|
||||
}
|
||||
|
||||
static bool scr_tls_tunnel_pending(void *tctx) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx;
|
||||
return tun->extra_off < tun->extra_len || (tun->inner != NULL && scr_tls_pending(tun->inner)) ||
|
||||
scr_tls_pending(tun->outer);
|
||||
}
|
||||
|
||||
static void scr_tls_tunnel_shutdown_write(void *tctx) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx;
|
||||
if (tun->inner != NULL) scr_tls_shutdown_write(tun->inner);
|
||||
scr_tls_shutdown_write(tun->outer);
|
||||
}
|
||||
|
||||
static void scr_tls_tunnel_free(void *tctx) {
|
||||
ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx;
|
||||
if (tun->inner != NULL) scr_tls_free(tun->inner);
|
||||
scr_tls_free(tun->outer);
|
||||
free(tun->req);
|
||||
free(tun->head);
|
||||
free(tun->extra);
|
||||
free(tun);
|
||||
}
|
||||
|
||||
static const ScrNetTransportOps SCR_TLS_TUNNEL_OPS = {
|
||||
.xread = scr_tls_tunnel_xread,
|
||||
.xwrite = scr_tls_tunnel_xwrite,
|
||||
.handshake = scr_tls_tunnel_handshake,
|
||||
.on_established = scr_tls_tunnel_on_established,
|
||||
.pending = scr_tls_tunnel_pending,
|
||||
.shutdown_write = scr_tls_tunnel_shutdown_write,
|
||||
.free = scr_tls_tunnel_free,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
ScrTlsCli *proxy; /* moves into the transport */
|
||||
ScrTlsCli *target; /* NULL for an http:// target */
|
||||
char *req;
|
||||
size_t req_len;
|
||||
} ScrTlsTunnelCtx;
|
||||
|
||||
void *scr_tls_fetch_tunnel_ctx(ScrStr *proxy_host /*borrowed*/, ScrStr *target_host /*borrowed, nullable*/,
|
||||
const char *request, size_t len) {
|
||||
ScrTlsTunnelCtx *ctx = calloc(1, sizeof *ctx);
|
||||
if (!ctx) scr_tls_oom();
|
||||
ctx->proxy = scr_tls_cli_new(proxy_host, true, NULL, 0);
|
||||
ctx->target = target_host != NULL ? scr_tls_cli_new(target_host, true, NULL, 0) : NULL;
|
||||
ctx->req = malloc(len);
|
||||
if (!ctx->req) scr_tls_oom();
|
||||
memcpy(ctx->req, request, len);
|
||||
ctx->req_len = len;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void scr_tls_fetch_tunnel_wrap(ScrNetSocket *sock, void *vctx) {
|
||||
ScrTlsTunnelCtx *ctx = (ScrTlsTunnelCtx *)vctx;
|
||||
ScrTlsTunnel *tun = calloc(1, sizeof *tun);
|
||||
if (!tun) scr_tls_oom();
|
||||
tun->outer = scr_tls_cli_sock(sock, ctx->proxy);
|
||||
mbedtls_ssl_set_bio(&tun->outer->ssl, tun->outer, scr_tls_bio_send, scr_tls_bio_recv, NULL);
|
||||
if (ctx->target != NULL) {
|
||||
tun->inner = scr_tls_cli_sock(sock, ctx->target);
|
||||
mbedtls_ssl_set_bio(&tun->inner->ssl, tun, scr_tls_tunnel_bio_send, scr_tls_tunnel_bio_recv,
|
||||
NULL);
|
||||
}
|
||||
tun->req = ctx->req;
|
||||
tun->req_len = ctx->req_len;
|
||||
free(ctx);
|
||||
scr_net_sock_set_transport(sock, &SCR_TLS_TUNNEL_OPS, tun);
|
||||
}
|
||||
|
||||
ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
|
||||
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
|
||||
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
|
||||
|
||||
Vendored
+3
@@ -370,6 +370,7 @@ export async function startFetchServers() {
|
||||
// CONNECT tunnels (undici's ProxyAgent shape — it tunnels even plain
|
||||
// http) splice raw sockets. One count per proxied request either way.
|
||||
let proxiedRequests = 0;
|
||||
let tunneledRequests = 0;
|
||||
const proxyAuthorizations = [];
|
||||
const proxyRequest = (req, res) => {
|
||||
// Relative-path requests are never proxy traffic — the standalone
|
||||
@@ -410,6 +411,7 @@ export async function startFetchServers() {
|
||||
};
|
||||
const proxyConnect = (req, clientSocket, head) => {
|
||||
proxiedRequests++;
|
||||
tunneledRequests++;
|
||||
proxyAuthorizations.push(req.headers["proxy-authorization"] ?? "");
|
||||
const [host, portStr] = (req.url ?? "").split(":");
|
||||
const upstream = connect(Number(portStr ?? "80"), host ?? "127.0.0.1", () => {
|
||||
@@ -450,6 +452,7 @@ export async function startFetchServers() {
|
||||
proxyUrl,
|
||||
secureProxyUrl,
|
||||
proxiedRequests: () => proxiedRequests,
|
||||
tunneledRequests: () => tunneledRequests,
|
||||
proxyAuthorizations: () => proxyAuthorizations.slice(),
|
||||
close: async () => {
|
||||
await new Promise((resolve) => server.close(() => resolve()));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Prints a failed fetch's error and its cause chain: name, message and
|
||||
// code at each level, plus the system error fields Node sets.
|
||||
function describe(value: unknown): string {
|
||||
const error = value as {
|
||||
name?: unknown;
|
||||
message?: unknown;
|
||||
code?: unknown;
|
||||
errno?: unknown;
|
||||
syscall?: unknown;
|
||||
address?: unknown;
|
||||
port?: unknown;
|
||||
};
|
||||
return `${String(error.name)} | ${String(error.message)} | ${String(error.code)} | ${String(error.errno)} | ${String(error.syscall)} | ${String(error.address)} | ${String(error.port)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(process.argv[2]);
|
||||
console.log(response.status, await response.text());
|
||||
} catch (error) {
|
||||
let level: unknown = error;
|
||||
for (let depth = 0; level !== undefined && depth < 4; depth++) {
|
||||
console.log(depth, describe(level));
|
||||
level = (level as { cause?: unknown }).cause;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import { globSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { createServer as createHttpServer } from "node:http";
|
||||
import { createServer as createHttpsServer } from "node:https";
|
||||
import { createServer as createNetServer, type AddressInfo } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||
@@ -461,6 +462,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})`
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const before = servers.proxiedRequests();
|
||||
const tunneledBefore = servers.tunneledRequests();
|
||||
const authBefore = servers.proxyAuthorizations().length;
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, ...argv], env),
|
||||
@@ -470,6 +472,8 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})`
|
||||
expect(nativeRes.exitCode, nativeRes.stderr.toString("utf8")).toBe(nodeRes.exitCode);
|
||||
expectCleanStderr(nativeRes, nodeRes);
|
||||
expect(servers.proxiedRequests() - before).toBe(2);
|
||||
// Both lanes reach the http:// target through a CONNECT tunnel.
|
||||
expect(servers.tunneledRequests() - tunneledBefore).toBe(2);
|
||||
expect(servers.proxyAuthorizations().slice(authBefore)).toEqual([
|
||||
expectedProxyAuthorization,
|
||||
expectedProxyAuthorization,
|
||||
@@ -522,6 +526,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})`
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const before = servers.proxiedRequests();
|
||||
const tunneledBefore = servers.tunneledRequests();
|
||||
const authBefore = servers.proxyAuthorizations().length;
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, baseUrl], env),
|
||||
@@ -531,6 +536,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})`
|
||||
expect(nativeRes.exitCode, nativeRes.stderr.toString("utf8")).toBe(nodeRes.exitCode);
|
||||
expectCleanStderr(nativeRes, nodeRes);
|
||||
expect(servers.proxiedRequests() - before).toBe(2);
|
||||
expect(servers.tunneledRequests() - tunneledBefore).toBe(2);
|
||||
expect(servers.proxyAuthorizations().slice(authBefore)).toEqual([
|
||||
expectedProxyAuthorization,
|
||||
expectedProxyAuthorization,
|
||||
@@ -593,6 +599,244 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})`
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
/** An HTTPS target on localhost that counts the requests it serves,
|
||||
* with the localhost certificate or another fixture certificate. */
|
||||
async function secureTarget(cert = "localhost"): Promise<{
|
||||
url: string;
|
||||
requests: () => number;
|
||||
close: () => Promise<void>;
|
||||
}> {
|
||||
const certs = join(fixturesRoot, "../server/certs");
|
||||
let requests = 0;
|
||||
const server = createHttpsServer(
|
||||
{
|
||||
key: readFileSync(join(certs, `${cert}-key.pem`)),
|
||||
cert: readFileSync(join(certs, `${cert}.pem`)),
|
||||
},
|
||||
(request, response) => {
|
||||
requests++;
|
||||
response.end(`secure ${request.url} ${request.headers.host}`);
|
||||
},
|
||||
);
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
return {
|
||||
url: `https://localhost:${port}`,
|
||||
requests: () => requests,
|
||||
close: () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
test.for(["llvm"] as const)(
|
||||
"%s fetch tunnels HTTPS through an http_proxy with CONNECT",
|
||||
async () => {
|
||||
const target = await secureTarget();
|
||||
try {
|
||||
const entry = join(fixturesRoot, "static-proxy/main.mts");
|
||||
const binary = await build(entry, "proxy-connect");
|
||||
const env = {
|
||||
...process.env,
|
||||
NODE_EXTRA_CA_CERTS: join(fixturesRoot, "../server/certs/ca.pem"),
|
||||
NODE_USE_ENV_PROXY: "1",
|
||||
https_proxy: authenticatedProxyUrl,
|
||||
HTTPS_PROXY: authenticatedProxyUrl,
|
||||
no_proxy: "",
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const before = servers.proxiedRequests();
|
||||
const authBefore = servers.proxyAuthorizations().length;
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, target.url], env),
|
||||
runBinary(binary, [target.url], env),
|
||||
]);
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
|
||||
expect(nativeRes.stdout.toString("utf8")).toContain("200 secure /text localhost:");
|
||||
expect(nativeRes.exitCode, nativeRes.stderr.toString("utf8")).toBe(nodeRes.exitCode);
|
||||
expectCleanStderr(nativeRes, nodeRes);
|
||||
expect(servers.proxiedRequests() - before).toBe(2);
|
||||
expect(target.requests()).toBe(2);
|
||||
expect(servers.proxyAuthorizations().slice(authBefore)).toEqual([
|
||||
expectedProxyAuthorization,
|
||||
expectedProxyAuthorization,
|
||||
]);
|
||||
} finally {
|
||||
await target.close();
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test.for(["llvm"] as const)(
|
||||
"%s fetch verifies the target host through the CONNECT tunnel",
|
||||
async () => {
|
||||
// The alt certificate names alt.localhost only.
|
||||
const target = await secureTarget("alt");
|
||||
try {
|
||||
const entry = join(fixturesRoot, "static-network-error/main.mts");
|
||||
const binary = await build(entry, "proxy-connect-verify");
|
||||
const url = target.url;
|
||||
const env = {
|
||||
...process.env,
|
||||
NODE_EXTRA_CA_CERTS: join(fixturesRoot, "../server/certs/ca.pem"),
|
||||
NODE_USE_ENV_PROXY: "1",
|
||||
https_proxy: proxyUrl,
|
||||
HTTPS_PROXY: proxyUrl,
|
||||
no_proxy: "",
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, url], env),
|
||||
runBinary(binary, [url], env),
|
||||
]);
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe("TypeError fetch failed true\n");
|
||||
expect(target.requests()).toBe(0);
|
||||
} finally {
|
||||
await target.close();
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test.for(["llvm"] as const)(
|
||||
"%s fetch sends proxy credentials only with a username and a password",
|
||||
async () => {
|
||||
const entry = join(fixturesRoot, "static-proxy/main.mts");
|
||||
const binary = await build(entry, "proxy-user-only");
|
||||
const userOnly = proxyUrl.replace("http://", "http://proxy-user@");
|
||||
const env = {
|
||||
...process.env,
|
||||
NODE_USE_ENV_PROXY: "1",
|
||||
http_proxy: userOnly,
|
||||
HTTP_PROXY: userOnly,
|
||||
no_proxy: "",
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const authBefore = servers.proxyAuthorizations().length;
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, baseUrl], env),
|
||||
runBinary(binary, [baseUrl], env),
|
||||
]);
|
||||
expect(nativeRes.stdout.equals(nodeRes.stdout)).toBe(true);
|
||||
expect(nativeRes.exitCode, nativeRes.stderr.toString("utf8")).toBe(nodeRes.exitCode);
|
||||
expect(servers.proxyAuthorizations().slice(authBefore)).toEqual(["", ""]);
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test.for(["llvm"] as const)(
|
||||
"%s fetch failures through a proxy carry Node's cause",
|
||||
async () => {
|
||||
// Answers every CONNECT with one fixed status line.
|
||||
const statusProxy = createNetServer((socket) => {
|
||||
socket.once("data", () => {
|
||||
socket.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||
});
|
||||
socket.on("error", () => {});
|
||||
});
|
||||
await new Promise<void>((resolve) => statusProxy.listen(0, "127.0.0.1", resolve));
|
||||
const statusProxyUrl = `http://127.0.0.1:${(statusProxy.address() as AddressInfo).port}`;
|
||||
try {
|
||||
const entry = join(fixturesRoot, "static-proxy-error/main.mts");
|
||||
const binary = await build(entry, "proxy-error-cause");
|
||||
for (const proxy of [statusProxyUrl, refusedUrl]) {
|
||||
const env = {
|
||||
...process.env,
|
||||
NODE_USE_ENV_PROXY: "1",
|
||||
https_proxy: proxy,
|
||||
HTTPS_PROXY: proxy,
|
||||
no_proxy: "",
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, "https://localhost/"], env),
|
||||
runBinary(binary, ["https://localhost/"], env),
|
||||
]);
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
|
||||
expect(nativeRes.exitCode, nativeRes.stderr.toString("utf8")).toBe(nodeRes.exitCode);
|
||||
}
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => statusProxy.close(() => resolve()));
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test.for(["llvm"] as const)(
|
||||
"%s fetch tunnels HTTPS through an HTTPS proxy with CONNECT",
|
||||
async () => {
|
||||
const target = await secureTarget();
|
||||
try {
|
||||
const entry = join(fixturesRoot, "static-proxy/main.mts");
|
||||
const binary = await build(entry, "proxy-https-https");
|
||||
const env = {
|
||||
...process.env,
|
||||
NODE_EXTRA_CA_CERTS: join(fixturesRoot, "../server/certs/ca.pem"),
|
||||
NODE_USE_ENV_PROXY: "1",
|
||||
https_proxy: secureAuthenticatedProxyUrl,
|
||||
HTTPS_PROXY: secureAuthenticatedProxyUrl,
|
||||
no_proxy: "",
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const before = servers.proxiedRequests();
|
||||
const tunneledBefore = servers.tunneledRequests();
|
||||
const authBefore = servers.proxyAuthorizations().length;
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, target.url], env),
|
||||
runBinary(binary, [target.url], env),
|
||||
]);
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
|
||||
expect(nativeRes.stdout.toString("utf8")).toContain("200 secure /text localhost:");
|
||||
expect(nativeRes.exitCode, nativeRes.stderr.toString("utf8")).toBe(nodeRes.exitCode);
|
||||
expectCleanStderr(nativeRes, nodeRes);
|
||||
expect(servers.proxiedRequests() - before).toBe(2);
|
||||
expect(servers.tunneledRequests() - tunneledBefore).toBe(2);
|
||||
expect(target.requests()).toBe(2);
|
||||
expect(servers.proxyAuthorizations().slice(authBefore)).toEqual([
|
||||
expectedProxyAuthorization,
|
||||
expectedProxyAuthorization,
|
||||
]);
|
||||
} finally {
|
||||
await target.close();
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test.for(["llvm"] as const)(
|
||||
"%s fetch verifies the target host through an HTTPS proxy tunnel",
|
||||
async () => {
|
||||
// The alt certificate names alt.localhost only.
|
||||
const target = await secureTarget("alt");
|
||||
try {
|
||||
const entry = join(fixturesRoot, "static-network-error/main.mts");
|
||||
const binary = await build(entry, "proxy-https-verify");
|
||||
const env = {
|
||||
...process.env,
|
||||
NODE_EXTRA_CA_CERTS: join(fixturesRoot, "../server/certs/ca.pem"),
|
||||
NODE_USE_ENV_PROXY: "1",
|
||||
https_proxy: secureProxyUrl,
|
||||
HTTPS_PROXY: secureProxyUrl,
|
||||
no_proxy: "",
|
||||
NO_PROXY: "",
|
||||
};
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry, target.url], env),
|
||||
runBinary(binary, [target.url], env),
|
||||
]);
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe("TypeError fetch failed true\n");
|
||||
expect(target.requests()).toBe(0);
|
||||
} finally {
|
||||
await target.close();
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test.for(["llvm"] as const)(
|
||||
"%s fetch rejects an invalid opted-in proxy instead of dialing directly",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user