curl/lib/http_proxy.c
Daniel Stenberg 7bc2bf7917
http_proxy: make two proxy_create functions static
And drop their `Curl_` prefixes. They are only used within this file.

Closes #21775
2026-05-27 23:15:29 +02:00

787 lines
23 KiB
C

/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#include "http_proxy.h"
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY)
#include "curl_trc.h"
#include "http.h"
#include "url.h"
#include "cfilters.h"
#include "cf-h1-proxy.h"
#include "cf-h2-proxy.h"
#include "cf-h3-proxy.h"
#include "cf-capsule.h"
#include "connect.h"
#include "vauth/vauth.h"
#include "curlx/strparse.h"
static CURLcode dynhds_add_custom(struct Curl_easy *data,
bool is_connect, int httpversion,
bool is_udp, struct dynhds *hds)
{
struct connectdata *conn = data->conn;
struct curl_slist *h[2];
struct curl_slist *headers;
int numlists = 1; /* by default */
int i;
enum Curl_proxy_use proxy;
if(is_connect && !is_udp)
proxy = HEADER_CONNECT;
else if(is_connect && is_udp)
proxy = HEADER_CONNECT_UDP;
else
proxy = (conn->bits.httpproxy && !conn->bits.tunnel_proxy) ?
HEADER_PROXY : HEADER_SERVER;
switch(proxy) {
case HEADER_SERVER:
h[0] = data->set.headers;
break;
case HEADER_PROXY:
h[0] = data->set.headers;
if(data->set.sep_headers) {
h[1] = data->set.proxyheaders;
numlists++;
}
break;
case HEADER_CONNECT:
if(data->set.sep_headers)
h[0] = data->set.proxyheaders;
else
h[0] = data->set.headers;
break;
case HEADER_CONNECT_UDP:
if(data->set.sep_headers)
h[0] = data->set.proxyheaders;
else
h[0] = data->set.headers;
break;
}
/* loop through one or two lists */
for(i = 0; i < numlists; i++) {
for(headers = h[i]; headers; headers = headers->next) {
struct Curl_str name;
const char *value = NULL;
size_t valuelen = 0;
const char *ptr = headers->data;
/* There are 2 quirks in place for custom headers:
* 1. setting only 'name:' to suppress a header from being sent
* 2. setting only 'name;' to send an empty (illegal) header
*/
if(!curlx_str_cspn(&ptr, &name, ";:")) {
if(!curlx_str_single(&ptr, ':')) {
curlx_str_passblanks(&ptr);
if(*ptr) {
value = ptr;
valuelen = strlen(value);
}
else {
/* quirk #1, suppress this header */
continue;
}
}
else if(!curlx_str_single(&ptr, ';')) {
curlx_str_passblanks(&ptr);
if(!*ptr) {
/* quirk #2, send an empty header */
value = "";
valuelen = 0;
}
else {
/* this may be used for something else in the future,
* ignore this for now */
continue;
}
}
else
/* neither : nor ; in provided header value. We ignore this
* silently */
continue;
}
else
/* no name, move on */
continue;
DEBUGASSERT(curlx_strlen(&name) && value);
if(data->state.aptr.host &&
/* a Host: header was sent already, do not pass on any custom Host:
header as that will produce *two* in the same request! */
curlx_str_casecompare(&name, "Host"))
;
else if(data->state.httpreq == HTTPREQ_POST_FORM &&
/* this header (extended by formdata.c) is sent later */
curlx_str_casecompare(&name, "Content-Type"))
;
else if(data->state.httpreq == HTTPREQ_POST_MIME &&
/* this header is sent later */
curlx_str_casecompare(&name, "Content-Type"))
;
else if(data->req.authneg &&
/* while doing auth neg, do not allow the custom length since
we will force length zero then */
curlx_str_casecompare(&name, "Content-Length"))
;
else if((httpversion >= 20) &&
curlx_str_casecompare(&name, "Transfer-Encoding"))
;
/* HTTP/2 and HTTP/3 do not support chunked requests */
else if((curlx_str_casecompare(&name, "Authorization") ||
curlx_str_casecompare(&name, "Cookie")) &&
/* be careful of sending this potentially sensitive header to
other hosts */
!Curl_auth_allowed_to_host(data))
;
else {
CURLcode result =
Curl_dynhds_add(hds, curlx_str(&name), curlx_strlen(&name),
value, valuelen);
if(result)
return result;
}
}
}
return CURLE_OK;
}
struct cf_proxy_ctx {
struct Curl_peer *dest; /* tunnel destination */
uint8_t proxytype;
BIT(sub_filter_installed);
BIT(udp_tunnel);
};
static int proxy_http_ver_major(proxy_http_ver ver)
{
switch(ver) {
case PROXY_HTTP_V1:
return 11;
case PROXY_HTTP_V2:
return 20;
case PROXY_HTTP_V3:
return 30;
}
return 0;
}
static CURLcode http_proxy_create_CONNECT(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
proxy_http_ver ver)
{
char *authority = NULL;
int httpversion = proxy_http_ver_major(ver);
CURLcode result;
struct httpreq *req = NULL;
authority = curl_maprintf("%s%s%s:%u",
dest->ipv6 ? "[" : "",
dest->hostname,
dest->ipv6 ? "]" : "",
dest->port);
if(!authority) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
result = Curl_http_req_make(&req, "CONNECT", sizeof("CONNECT") - 1,
NULL, 0, authority, strlen(authority),
NULL, 0);
if(result)
goto out;
/* Setup the proxy-authorization header, if any */
result = Curl_http_output_auth(data, cf->conn, req->method, HTTPREQ_GET,
req->authority, NULL, TRUE);
if(result)
goto out;
/* If user is not overriding Host: header, we add for HTTP/1.x */
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) {
result = Curl_dynhds_cadd(&req->headers, "Host", authority);
if(result)
goto out;
}
if(data->req.hd_proxy_auth) {
result = Curl_dynhds_h1_cadd_line(&req->headers,
data->req.hd_proxy_auth);
if(result)
goto out;
}
if(!Curl_checkProxyheaders(data, cf->conn, STRCONST("User-Agent")) &&
data->set.str[STRING_USERAGENT] && *data->set.str[STRING_USERAGENT]) {
result = Curl_dynhds_cadd(&req->headers, "User-Agent",
data->set.str[STRING_USERAGENT]);
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) {
result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive");
if(result)
goto out;
}
result = dynhds_add_custom(data, TRUE, httpversion,
FALSE, &req->headers);
out:
if(result && req) {
Curl_http_req_free(req);
req = NULL;
}
curlx_free(authority);
*preq = req;
return result;
}
static CURLcode http_proxy_create_CONNECTUDP(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
proxy_http_ver ver)
{
const char *proxy_scheme = "http";
const char *proxy_host = cf->conn->http_proxy.peer->hostname;
int httpversion = proxy_http_ver_major(ver);
char *authority = NULL;
char *path = NULL;
char *encoded_host = NULL;
struct httpreq *req = NULL;
bool proxy_ipv6_ip;
CURLcode result;
if(cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS ||
cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS2 ||
cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS3)
proxy_scheme = "https";
proxy_ipv6_ip = cf->conn->http_proxy.peer->ipv6 != 0;
authority = curl_maprintf("%s%s%s:%d",
proxy_ipv6_ip ? "[" : "",
proxy_host,
proxy_ipv6_ip ? "]" : "",
cf->conn->http_proxy.peer->port);
if(!authority) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
if(dest->ipv6) {
/* RFC 9298: colons in IPv6 addresses MUST be percent-encoded
* in the URI template (e.g. "2001:db8::1" -> "2001%3Adb8%3A%3A1") */
const char *s = dest->hostname;
char *d;
size_t hlen = strlen(s);
encoded_host = curlx_malloc(hlen * 3 + 1);
if(!encoded_host) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
d = encoded_host;
while(*s) {
if(*s == ':') {
*d++ = '%';
*d++ = '3';
*d++ = 'A';
}
else
*d++ = *s;
s++;
}
*d = '\0';
path = curl_maprintf("/.well-known/masque/udp/%s/%u/",
encoded_host, (unsigned int)dest->port);
}
else {
path = curl_maprintf("/.well-known/masque/udp/%s/%u/",
dest->hostname, (unsigned int)dest->port);
}
if(!path) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
if(ver == PROXY_HTTP_V1) {
result = Curl_http_req_make(&req, "GET", sizeof("GET")-1,
proxy_scheme, strlen(proxy_scheme),
authority, strlen(authority),
path, strlen(path));
if(result)
goto out;
}
else if(ver == PROXY_HTTP_V2 || ver == PROXY_HTTP_V3) {
result = Curl_http_req_make(&req, "CONNECT", sizeof("CONNECT") - 1,
proxy_scheme, strlen(proxy_scheme),
authority, strlen(authority),
path, strlen(path));
if(result)
goto out;
}
else {
result = CURLE_FAILED_INIT;
goto out;
}
/* Setup the proxy-authorization header, if any */
result = Curl_http_output_auth(data, cf->conn, req->method, HTTPREQ_GET,
req->authority, NULL, TRUE);
if(result)
goto out;
/* If user is not overriding Host: header, we add for HTTP/1.x */
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) {
result = Curl_dynhds_cadd(&req->headers, "Host", authority);
if(result)
goto out;
}
if(data->req.hd_proxy_auth) {
result = Curl_dynhds_h1_cadd_line(&req->headers,
data->req.hd_proxy_auth);
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("User-Agent")) &&
data->set.str[STRING_USERAGENT] && *data->set.str[STRING_USERAGENT]) {
result = Curl_dynhds_cadd(&req->headers, "User-Agent",
data->set.str[STRING_USERAGENT]);
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) {
result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive");
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1) {
result = Curl_dynhds_cadd(&req->headers, "Connection", "Upgrade");
if(result)
goto out;
result = Curl_dynhds_cadd(&req->headers, "Upgrade", "connect-udp");
if(result)
goto out;
result = Curl_dynhds_cadd(&req->headers, "Capsule-Protocol", "?1");
if(result)
goto out;
}
else {
result = Curl_dynhds_cadd(&req->headers, ":Protocol", "connect-udp");
if(result)
goto out;
if(ver >= PROXY_HTTP_V2) {
result = Curl_dynhds_cadd(&req->headers, "Capsule-Protocol", "?1");
if(result)
goto out;
}
}
result = dynhds_add_custom(data, TRUE, httpversion,
TRUE, &req->headers);
out:
if(result && req) {
Curl_http_req_free(req);
req = NULL;
}
curlx_free(authority);
curlx_free(path);
curlx_free(encoded_host);
*preq = req;
return result;
}
CURLcode Curl_http_proxy_create_tunnel_request(
struct httpreq **preq, struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_peer *dest,
proxy_http_ver ver, bool udp_tunnel)
{
CURLcode result;
if(udp_tunnel)
result = http_proxy_create_CONNECTUDP(preq, cf, data, dest, ver);
else
result = http_proxy_create_CONNECT(preq, cf, data, dest, ver);
if(result)
return result;
if(udp_tunnel)
infof(data, "Establishing %s proxy UDP tunnel to %s:%s",
(ver == PROXY_HTTP_V2) ? "HTTP/2" :
(ver == PROXY_HTTP_V3) ? "HTTP/3" : "HTTP",
data->state.up.hostname, data->state.up.port);
else
infof(data, "Establishing %s proxy tunnel to %s",
(ver == PROXY_HTTP_V2) ? "HTTP/2" :
(ver == PROXY_HTTP_V3) ? "HTTP/3" : "HTTP",
(*preq)->authority);
return CURLE_OK;
}
CURLcode Curl_http_proxy_inspect_tunnel_response(
struct Curl_cfilter *cf, struct Curl_easy *data,
struct http_resp *resp, bool udp_tunnel,
proxy_inspect_result *presult)
{
struct dynhds_entry *capsule_protocol = NULL;
struct dynhds_entry *auth_reply = NULL;
size_t i, header_count;
CURLcode result = CURLE_OK;
DEBUGASSERT(resp);
header_count = Curl_dynhds_count(&resp->headers);
if(udp_tunnel)
infof(data, "CONNECT-UDP Response Status %d", resp->status);
else
infof(data, "CONNECT Response Status %d", resp->status);
infof(data, "Response Headers (%zu total):", header_count);
for(i = 0; i < header_count; i++) {
struct dynhds_entry *entry = Curl_dynhds_getn(&resp->headers, i);
if(entry)
infof(data, " %s: %s", entry->name, entry->value);
}
if(resp->status == 401) {
auth_reply = Curl_dynhds_cget(&resp->headers, "WWW-Authenticate");
}
else if(resp->status == 407) {
auth_reply = Curl_dynhds_cget(&resp->headers, "Proxy-Authenticate");
}
if(auth_reply) {
CURL_TRC_CF(data, cf, "[0] CONNECT%s: fwd auth header '%s'",
udp_tunnel ? "-UDP" : "", auth_reply->value);
result = Curl_http_input_auth(data, resp->status == 407,
auth_reply->value);
if(result)
return result;
if(data->req.newurl) {
curlx_safefree(data->req.newurl);
*presult = PROXY_INSPECT_AUTH_RETRY;
return CURLE_OK;
}
}
if(udp_tunnel) {
if(resp->status / 100 == 2) {
capsule_protocol = Curl_dynhds_cget(&resp->headers,
"capsule-protocol");
if(capsule_protocol) {
if(strncmp(capsule_protocol->value, "?1", 2) == 0 &&
!capsule_protocol->value[2]) {
infof(data, "CONNECT-UDP tunnel established, response %d",
resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
failf(data, "Failed to establish CONNECT-UDP tunnel, response %d, "
"unsupported capsule-protocol value '%s'",
resp->status, capsule_protocol->value);
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
else {
/* NOTE proxies may not set capsule protocol in the headers */
infof(data, "CONNECT-UDP tunnel established, response %d "
"but no capsule-protocol header found", resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
}
else {
failf(data, "Failed to establish CONNECT-UDP tunnel, "
"response %d", resp->status);
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
}
if(resp->status / 100 == 2) {
infof(data, "CONNECT tunnel established, response %d", resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
static CURLcode http_proxy_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
struct cf_proxy_ctx *ctx = cf->ctx;
CURLcode result;
const char *tunnel_type; /* Determine tunnel type once and reuse */
tunnel_type = ctx->udp_tunnel ? "CONNECT-UDP" : "CONNECT";
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
CURL_TRC_CF(data, cf, "%s", tunnel_type);
connect_sub:
/* in case of h3_proxy, cf->next will be NULL initially */
if(cf->next) {
result = cf->next->cft->do_connect(cf->next, data, done);
if(result || !*done)
return result;
}
*done = FALSE;
if(!ctx->sub_filter_installed) {
const char *alpn = NULL;
/* in case of h3_proxy, cf->next will be NULL initially */
if(cf->next) {
alpn = Curl_conn_cf_get_alpn_negotiated(cf->next, data);
}
if(alpn)
infof(data, "%s: '%s' negotiated", tunnel_type, alpn);
else if(!alpn) {
/* No ALPN, proxytype rules. Fake ALPN */
infof(data, "%s: no ALPN negotiated", tunnel_type);
switch(ctx->proxytype) {
case CURLPROXY_HTTP_1_0:
alpn = "http/1.0";
break;
case CURLPROXY_HTTPS2:
alpn = "h2";
break;
case CURLPROXY_HTTPS3:
alpn = "h3";
break;
default:
alpn = "http/1.1";
break;
}
}
if(!strcmp(alpn, "http/1.0")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.0");
result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, 10,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
else if(!strcmp(alpn, "http/1.1")) {
int httpversion = (ctx->proxytype == CURLPROXY_HTTP_1_0) ? 10 : 11;
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.%d",
httpversion % 10);
result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, httpversion,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
#ifdef USE_NGHTTP2
else if(!strcmp(alpn, "h2")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/2");
result = Curl_cf_h2_proxy_insert_after(cf, data, ctx->dest,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
#endif /* USE_NGHTTP2 */
#if defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) && \
defined(USE_NGTCP2) && defined(USE_OPENSSL)
else if(!strcmp(alpn, "h3")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/3");
result = Curl_cf_h3_proxy_insert_after(cf, data, ctx->dest,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
#endif /* USE_PROXY_HTTP3 && USE_NGHTTP3 && USE_NGTCP2 && USE_OPENSSL */
else {
failf(data, "%s: negotiated ALPN '%s' not supported", tunnel_type, alpn);
result = CURLE_COULDNT_CONNECT;
goto out;
}
ctx->sub_filter_installed = TRUE;
/* after we installed the filter "below" us, we call connect
* on out sub-chain again.
*/
goto connect_sub;
}
else {
/* subchain connected and we had already installed the protocol filter.
* This means the protocol tunnel is established, we are done.
*/
DEBUGASSERT(ctx->sub_filter_installed);
if(ctx->udp_tunnel) {
#ifdef USE_PROXY_HTTP3
/* Insert capsule filter between us and the protocol sub-filter.
* This handles encap/decap of UDP datagrams in capsule format. */
result = Curl_cf_capsule_insert_after(cf, data);
if(result)
goto out;
CURL_TRC_CF(data, cf, "installed capsule filter for UDP tunnel");
#else
result = CURLE_NOT_BUILT_IN;
goto out;
#endif /* USE_PROXY_HTTP3 */
}
result = CURLE_OK;
}
out:
if(!result) {
cf->connected = TRUE;
*done = TRUE;
}
return result;
}
static CURLcode cf_http_proxy_query(struct Curl_cfilter *cf,
struct Curl_easy *data,
int query, int *pres1, void *pres2)
{
struct cf_proxy_ctx *ctx = cf->ctx;
switch(query) {
case CF_QUERY_HOST_PORT:
*pres1 = (int)ctx->dest->port;
*((const char **)pres2) = ctx->dest->hostname;
return CURLE_OK;
case CF_QUERY_ALPN_NEGOTIATED: {
const char **palpn = pres2;
DEBUGASSERT(palpn);
*palpn = NULL;
return CURLE_OK;
}
default:
break;
}
return cf->next ?
cf->next->cft->query(cf->next, data, query, pres1, pres2) :
CURLE_UNKNOWN_OPTION;
}
static void cf_https_proxy_ctx_free(struct cf_proxy_ctx *ctx)
{
if(ctx) {
Curl_peer_unlink(&ctx->dest);
curlx_free(ctx);
}
}
static void http_proxy_cf_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_proxy_ctx *ctx = cf->ctx;
if(ctx) {
CURL_TRC_CF(data, cf, "destroy");
cf_https_proxy_ctx_free(ctx);
}
}
static void http_proxy_cf_close(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
CURL_TRC_CF(data, cf, "close");
cf->connected = FALSE;
if(cf->next)
cf->next->cft->do_close(cf->next, data);
}
struct Curl_cftype Curl_cft_http_proxy = {
"HTTP-PROXY",
CF_TYPE_IP_CONNECT | CF_TYPE_PROXY | CF_TYPE_SETUP,
0,
http_proxy_cf_destroy,
http_proxy_cf_connect,
http_proxy_cf_close,
Curl_cf_def_shutdown,
Curl_cf_def_adjust_pollset,
Curl_cf_def_data_pending,
Curl_cf_def_send,
Curl_cf_def_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
cf_http_proxy_query,
};
CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
struct Curl_peer *dest,
uint8_t proxytype,
bool udp_tunnel)
{
struct Curl_cfilter *cf;
struct cf_proxy_ctx *ctx = NULL;
CURLcode result;
(void)data;
if(!dest)
return CURLE_FAILED_INIT;
ctx = curlx_calloc(1, sizeof(*ctx));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
Curl_peer_link(&ctx->dest, dest);
ctx->proxytype = proxytype;
ctx->udp_tunnel = udp_tunnel;
result = Curl_cf_create(&cf, &Curl_cft_http_proxy, ctx);
if(result)
goto out;
ctx = NULL;
Curl_conn_cf_insert_after(cf_at, cf);
out:
cf_https_proxy_ctx_free(ctx);
return result;
}
#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_PROXY */