/*************************************************************************** * _ _ ____ _ * Project ___| | | | _ \| | * / __| | | | |_) | | * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * * Copyright (C) Daniel Stenberg, , 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 */