Tunnel fetch through env proxies with CONNECT (#1237)

This commit is contained in:
Chris Tate
2026-10-17 19:59:21 -05:00
committed by GitHub
8 changed files with 909 additions and 33 deletions
+225 -23
View File
@@ -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);
+7
View File
@@ -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
View File
@@ -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);
}
}
+12
View File
@@ -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. */
+228
View File
@@ -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*/,
+3
View File
@@ -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()));
+25
View File
@@ -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;
}
}
+244
View File
@@ -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 () => {