diff --git a/.github/scripts/pyspelling.words b/.github/scripts/pyspelling.words index 7d9f6ffb36..63e5143191 100644 --- a/.github/scripts/pyspelling.words +++ b/.github/scripts/pyspelling.words @@ -167,8 +167,10 @@ CWE cyassl Cygwin daniel +datagrams datatracker dbg +decapsulation Debian DEBUGBUILD decrypt @@ -234,6 +236,7 @@ EGD EHLO EINTR else's +encapsulation encodings enctype endianness diff --git a/CMakeLists.txt b/CMakeLists.txt index 331c22dc4f..4a34f8524e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1118,6 +1118,8 @@ if(USE_SSLS_EXPORT) endif() endif() +option(USE_PROXY_HTTP3 "Enable experimental HTTP/3 proxy support" OFF) + option(USE_NGHTTP2 "Use nghttp2 library" ON) if(USE_NGHTTP2) find_package(NGHTTP2 MODULE) @@ -1186,6 +1188,20 @@ if(USE_QUICHE) endif() endif() +if(USE_PROXY_HTTP3) + if(CURL_DISABLE_PROXY) + message(FATAL_ERROR "USE_PROXY_HTTP3 requires proxy support") + elseif(CURL_DISABLE_HTTP) + message(FATAL_ERROR "USE_PROXY_HTTP3 requires HTTP support") + elseif(NOT USE_NGTCP2 OR NOT USE_NGHTTP3) + message(FATAL_ERROR "USE_PROXY_HTTP3 requires ngtcp2 + nghttp3") + elseif(NOT USE_OPENSSL) + message(FATAL_ERROR "USE_PROXY_HTTP3 currently requires OpenSSL") + else() + message(STATUS "HTTP/3 proxy support enabled (experimental)") + endif() +endif() + if(NOT CURL_DISABLE_SRP AND (HAVE_GNUTLS_SRP OR HAVE_OPENSSL_SRP)) set(USE_TLS_SRP 1) endif() @@ -2046,6 +2062,7 @@ curl_add_if("NTLM" CURL_ENABLE_NTLM AND curl_add_if("TLS-SRP" USE_TLS_SRP) curl_add_if("HTTP2" USE_NGHTTP2) curl_add_if("HTTP3" USE_NGTCP2 OR USE_QUICHE) +curl_add_if("PROXY-HTTP3" USE_PROXY_HTTP3) curl_add_if("MultiSSL" CURL_WITH_MULTI_SSL) curl_add_if("HTTPS-proxy" NOT CURL_DISABLE_PROXY AND _ssl_enabled AND (USE_OPENSSL OR USE_GNUTLS OR USE_SCHANNEL OR USE_RUSTLS OR USE_MBEDTLS OR diff --git a/configure.ac b/configure.ac index 31a29cd601..0601371baa 100644 --- a/configure.ac +++ b/configure.ac @@ -54,6 +54,30 @@ CURL_CHECK_OPTION_RT CURL_CHECK_OPTION_HTTPSRR CURL_CHECK_OPTION_ECH CURL_CHECK_OPTION_SSLS_EXPORT +AC_MSG_CHECKING([whether to enable HTTP/3 proxy support]) +OPT_PROXY_HTTP3="default" +AC_ARG_ENABLE(proxy-http3, +AS_HELP_STRING([--enable-proxy-http3],[Enable experimental HTTP/3 proxy support]) +AS_HELP_STRING([--disable-proxy-http3],[Disable experimental HTTP/3 proxy support]), + OPT_PROXY_HTTP3=$enableval) +case "$OPT_PROXY_HTTP3" in + no) + want_proxy_http3="no" + curl_proxy_http3_msg="no (--enable-proxy-http3)" + AC_MSG_RESULT([no]) + ;; + default) + want_proxy_http3="no" + curl_proxy_http3_msg="no (--enable-proxy-http3)" + AC_MSG_RESULT([no]) + ;; + *) + want_proxy_http3="yes" + curl_proxy_http3_msg="enabled (--disable-proxy-http3)" + AC_MSG_RESULT([yes]) + ;; +esac +USE_PROXY_HTTP3=0 XC_CHECK_PATH_SEPARATOR @@ -318,6 +342,22 @@ AS_HELP_STRING([--with-test-caddy=PATH],[where to find caddy for testing]), ) AC_SUBST(CADDY) +if test -x /usr/local/bin/h2o; then + H2O=/usr/local/bin/h2o +elif test -x /usr/bin/h2o; then + H2O=/usr/bin/h2o +elif test -x "`brew --prefix 2>/dev/null`/bin/h2o"; then + H2O=`brew --prefix`/bin/h2o +fi +AC_ARG_WITH(test-h2o,dnl +AS_HELP_STRING([--with-test-h2o=PATH],[where to find h2o for testing]), + H2O=$withval + if test "x$H2O" = "xno"; then + H2O="" + fi +) +AC_SUBST(H2O) + if test -x /usr/sbin/vsftpd; then VSFTPD=/usr/sbin/vsftpd elif test -x /usr/local/sbin/vsftpd; then @@ -5028,6 +5068,28 @@ if test "$want_ssls_export" != "no"; then fi fi +dnl ************************************************************* +dnl check whether experimental HTTP/3 proxy support is enabled +dnl +if test "$want_proxy_http3" = "yes"; then + AC_MSG_CHECKING([whether HTTP/3 proxy support is available]) + + if test "$CURL_DISABLE_PROXY" = "1"; then + AC_MSG_ERROR([--enable-proxy-http3 requires proxy support]) + elif test "$CURL_DISABLE_HTTP" = "1"; then + AC_MSG_ERROR([--enable-proxy-http3 requires HTTP support]) + elif test "$USE_NGTCP2_H3" != "1"; then + AC_MSG_ERROR([--enable-proxy-http3 requires ngtcp2 + nghttp3]) + elif test "x$OPENSSL_ENABLED" != "x1"; then + AC_MSG_ERROR([--enable-proxy-http3 currently requires OpenSSL]) + else + AC_DEFINE(USE_PROXY_HTTP3, 1, [if HTTP/3 proxy support is available]) + USE_PROXY_HTTP3=1 + AC_MSG_RESULT([yes]) + experimental="$experimental PROXY-HTTP3" + fi +fi + dnl ************************************************************ dnl hiding of library internal symbols dnl @@ -5141,6 +5203,10 @@ if test "$curl_psl_msg" = "enabled"; then SUPPORT_FEATURES="$SUPPORT_FEATURES PSL" fi +if test "$USE_PROXY_HTTP3" = "1"; then + SUPPORT_FEATURES="$SUPPORT_FEATURES PROXY-HTTP3" +fi + if test "$curl_gsasl_msg" = "enabled"; then SUPPORT_FEATURES="$SUPPORT_FEATURES gsasl" fi @@ -5485,6 +5551,7 @@ AC_MSG_NOTICE([Configured to build curl/libcurl: HTTP1: ${curl_h1_msg} HTTP2: ${curl_h2_msg} HTTP3: ${curl_h3_msg} + Proxy-HTTP3: ${curl_proxy_http3_msg} ECH: ${curl_ech_msg} HTTPS RR: ${curl_httpsrr_msg} SSLS-EXPORT: ${curl_ssls_export_msg} diff --git a/docs/EXPERIMENTAL.md b/docs/EXPERIMENTAL.md index 43fc0fdeed..ca8277fa14 100644 --- a/docs/EXPERIMENTAL.md +++ b/docs/EXPERIMENTAL.md @@ -43,6 +43,16 @@ Graduation requirements: - Using HTTP/3 with the given build should perform without risking busy-loops +### HTTP/3 proxy and CONNECT-UDP support + +Support for HTTP/3 proxy and CONNECT-UDP tunneling is experimental and +requires an explicit build-time opt-in (`--enable-proxy-http3` for +autotools, `-DUSE_PROXY_HTTP3=ON` for CMake). + +Graduation requirements: + +- implementation stability over time with no known severe regressions + ### The Rustls backend Graduation requirements: diff --git a/docs/INSTALL-CMAKE.md b/docs/INSTALL-CMAKE.md index e07ec455da..83eb9df68e 100644 --- a/docs/INSTALL-CMAKE.md +++ b/docs/INSTALL-CMAKE.md @@ -254,6 +254,7 @@ target_link_libraries(my_target PRIVATE CURL::libcurl) - `USE_SSLS_EXPORT`: Enable experimental SSL session import/export. Default: `OFF` - `USE_WIN32_IDN`: Use WinIDN for IDN support. Default: `OFF` - `USE_WIN32_LDAP`: Use Windows LDAP implementation. Default: `ON` +- `USE_PROXY_HTTP3`: Enable experimental HTTP/3 proxy support. Default: `OFF` ## Disabling features diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc index f7236af1b1..f8fd01ccd6 100644 --- a/docs/cmdline-opts/Makefile.inc +++ b/docs/cmdline-opts/Makefile.inc @@ -212,6 +212,7 @@ DPAGES = \ proxy-digest.md \ proxy-header.md \ proxy-http2.md \ + proxy-http3.md \ proxy-insecure.md \ proxy-key-type.md \ proxy-key.md \ diff --git a/docs/cmdline-opts/proxy-http2.md b/docs/cmdline-opts/proxy-http2.md index ca6a091f32..a38da9e87e 100644 --- a/docs/cmdline-opts/proxy-http2.md +++ b/docs/cmdline-opts/proxy-http2.md @@ -5,7 +5,7 @@ Long: proxy-http2 Tags: Versions HTTP/2 Protocols: HTTP Added: 8.1.0 -Mutexed: +Mutexed: proxy-http3 Requires: HTTP/2 Help: Use HTTP/2 with HTTPS proxy Category: http proxy @@ -22,3 +22,5 @@ Negotiate HTTP/2 with an HTTPS proxy. The proxy might still only offer HTTP/1 and then curl sticks to using that version. This has no effect for any other kinds of proxies. + +This option is mutually exclusive with `--proxy-http3`. diff --git a/docs/cmdline-opts/proxy-http3.md b/docs/cmdline-opts/proxy-http3.md new file mode 100644 index 0000000000..6533b980b6 --- /dev/null +++ b/docs/cmdline-opts/proxy-http3.md @@ -0,0 +1,31 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: proxy-http3 +Tags: Versions HTTP/3 +Protocols: HTTP +Added: 8.21.0 +Mutexed: proxy-http2 +Requires: HTTP/3 +Help: Use HTTP/3 with HTTPS proxy +Category: http proxy +Multi: boolean +See-also: + - proxy + - proxy-http2 +Example: + - --proxy-http3 -x proxy $URL +--- + +# `--proxy-http3` + +Negotiate HTTP/3 with an HTTPS proxy. +Fails to perform the transfer if the given proxy does not support HTTP/3. + +This has no effect for any other kinds of proxies. + +This option is mutually exclusive with `--proxy-http2`. + +This feature is experimental and requires a build with HTTP/3 proxy support +enabled. For autotools builds, use `--enable-proxy-http3`. For CMake builds, +use `-DUSE_PROXY_HTTP3=ON`. diff --git a/docs/internals/CONNECTION-FILTERS.md b/docs/internals/CONNECTION-FILTERS.md index 619ca0e340..1a817a1567 100644 --- a/docs/internals/CONNECTION-FILTERS.md +++ b/docs/internals/CONNECTION-FILTERS.md @@ -156,9 +156,9 @@ The currently existing filter types (curl 8.5.0) are: `accept()`ed in a `listen()` * `SSL`: filter that applies TLS en-/decryption and handshake. Manages the underlying TLS backend implementation. -* `HTTP-PROXY`, `H1-PROXY`, `H2-PROXY`: the first manages the connection to an - HTTP proxy server and uses the other depending on which ALPN protocol has - been negotiated. +* `HTTP-PROXY`, `H1-PROXY`, `H2-PROXY`, `H3-PROXY`: the first manages the + connection to an HTTP proxy server and uses the other depending on which + ALPN protocol has been negotiated. * `SOCKS-PROXY`: filter for the various SOCKS proxy protocol variations * `HAPROXY`: filter for the protocol of the same name, providing client IP information to a server. @@ -166,7 +166,7 @@ The currently existing filter types (curl 8.5.0) are: connection * `HTTP/3`: filter for handling multiplexed transfers over an HTTP/3+QUIC connection -* `HAPPY-EYEBALLS`: meta filter that implements IPv4/IPv6 "happy eyeballing". +* `HAPPY-EYEBALLS`: meta filter that implements IPv4/IPv6 "happy eyeballs". It creates up to 2 sub-filters that race each other for a connection. * `SETUP`: meta filter that manages the creation of sub-filter chains for a specific transport (e.g. TCP or QUIC). @@ -220,6 +220,37 @@ as an `SSL` flagged filter is seen first. `conn3` is also encrypted as the Similar checks can determine if a connection is multiplexed or not. +## Adding CONNECT-UDP support +HTTP/3 on top of HTTP/1.1 (MASQUE CONNECT-UDP): +``` +conn --> HTTP/3 --> CAPSULE --> HTTP-PROXY --> H1-PROXY --> SSL --> HAPPY-EYEBALLS --> TCP +``` + +HTTP/3 on top of HTTP/2 (MASQUE CONNECT-UDP): +``` +conn --> HTTP/3 --> CAPSULE --> HTTP-PROXY --> H2-PROXY --> SSL --> HAPPY-EYEBALLS --> TCP +``` + +The CAPSULE filter handles RFC 9297 capsule protocol encapsulation and +decapsulation of UDP datagrams. It is inserted automatically when the +HTTP-PROXY filter completes a successful CONNECT-UDP tunnel. + +## Adding H3-PROXY support +HTTP/1.1 on top of HTTP/3 (CONNECT over QUIC): +``` +conn --> HTTP/1.1 --> SSL --> HTTP-PROXY --> H3-PROXY --> HAPPY-EYEBALLS --> UDP +``` + +HTTP/2 on top of HTTP/3 (CONNECT over QUIC): +``` +conn --> HTTP/2 --> SSL --> HTTP-PROXY --> H3-PROXY --> HAPPY-EYEBALLS --> UDP +``` + +HTTP/3 on top of HTTP/3 (MASQUE CONNECT-UDP over QUIC): +``` +conn --> HTTP/3 --> CAPSULE --> HTTP-PROXY --> H3-PROXY --> HAPPY-EYEBALLS --> UDP +``` + ## Filter Tracing Filters may make use of special trace macros like `CURL_TRC_CF(data, cf, msg, diff --git a/docs/libcurl/curl_version_info.md b/docs/libcurl/curl_version_info.md index fd589a834c..ec29fa66e7 100644 --- a/docs/libcurl/curl_version_info.md +++ b/docs/libcurl/curl_version_info.md @@ -298,6 +298,13 @@ supports HTTP NTLM libcurl was built with support for NTLM delegation to a winbind helper. This feature was removed from curl in 8.8.0. +## `PROXY-HTTP3` + +*features* mask bit: non-existent + +libcurl was built with EXPERIMENTAL support for HTTP/3 proxy tunneling +(Added in 8.21.0) + ## `PSL` *features* mask bit: CURL_VERSION_PSL diff --git a/docs/libcurl/opts/CURLOPT_PROXY.md b/docs/libcurl/opts/CURLOPT_PROXY.md index 7be874d733..072dfd4809 100644 --- a/docs/libcurl/opts/CURLOPT_PROXY.md +++ b/docs/libcurl/opts/CURLOPT_PROXY.md @@ -58,7 +58,11 @@ HTTPS Proxy. (with OpenSSL, GnuTLS, mbedTLS, Rustls, Schannel or wolfSSL.) This uses HTTP/1 by default. Setting CURLOPT_PROXYTYPE(3) to **CURLPROXY_HTTPS2** allows libcurl to negotiate using HTTP/2 with proxy. -## `socks4://` +Setting CURLOPT_PROXYTYPE(3) to **CURLPROXY_HTTPS3** allows libcurl to +negotiate using HTTP/3 with proxy. This feature is experimental and requires +a build with HTTP/3 proxy support enabled. + +## socks4:// SOCKS4 Proxy. diff --git a/docs/libcurl/opts/CURLOPT_PROXYTYPE.md b/docs/libcurl/opts/CURLOPT_PROXYTYPE.md index 6000d10b00..1dc1a1328a 100644 --- a/docs/libcurl/opts/CURLOPT_PROXYTYPE.md +++ b/docs/libcurl/opts/CURLOPT_PROXYTYPE.md @@ -41,6 +41,12 @@ HTTPS Proxy using HTTP/1. (Added in 7.52.0 for OpenSSL and GnuTLS. Since HTTPS Proxy and attempt to speak HTTP/2 over it. (Added in 8.1.0) +## CURLPROXY_HTTPS3 + +HTTPS Proxy and attempt to speak HTTP/3 over it. (Added in 8.21.0) +This feature is experimental and requires a build with HTTP/3 proxy support +enabled. + ## CURLPROXY_HTTP_1_0 HTTP 1.0 Proxy. This is similar to CURLPROXY_HTTP except it uses HTTP/1.0 for diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index 4dc670da6e..5bad9a9842 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -993,6 +993,7 @@ CURLPROXY_HTTP 7.10 CURLPROXY_HTTP_1_0 7.19.4 CURLPROXY_HTTPS 7.52.0 CURLPROXY_HTTPS2 8.1.0 +CURLPROXY_HTTPS3 8.21.0 CURLPROXY_SOCKS4 7.10 CURLPROXY_SOCKS4A 7.18.0 CURLPROXY_SOCKS5 7.10 diff --git a/docs/options-in-versions b/docs/options-in-versions index 95d84a4bfe..fa20b2dd29 100644 --- a/docs/options-in-versions +++ b/docs/options-in-versions @@ -177,6 +177,7 @@ --proxy-digest 7.12.0 --proxy-header 7.37.0 --proxy-http2 8.1.0 +--proxy-http3 8.21.0 --proxy-insecure 7.52.0 --proxy-key 7.52.0 --proxy-key-type 7.52.0 diff --git a/docs/tests/HTTP.md b/docs/tests/HTTP.md index 88fb9a0c4b..79f3fac200 100644 --- a/docs/tests/HTTP.md +++ b/docs/tests/HTTP.md @@ -62,6 +62,9 @@ Via curl's `configure` script you may specify: * `--with-test-nghttpx=` if you have nghttpx to use somewhere outside your `$PATH`. + * `--with-test-h2o=` if you have h2o to use somewhere + outside your `$PATH`. + * `--with-test-httpd=` if you have an Apache httpd installed somewhere else. On Debian/Ubuntu it otherwise looks into `/usr/bin` and `/usr/sbin` to find those. diff --git a/include/curl/curl.h b/include/curl/curl.h index cb36eefad4..c790760b88 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -802,9 +802,11 @@ typedef CURLcode (*curl_ssl_ctx_callback)(CURL *curl, /* easy handle */ #define CURLPROXY_SOCKS5_HOSTNAME 7L /* Use the SOCKS5 protocol but pass along the hostname rather than the IP address. added in 7.18.0 */ +#define CURLPROXY_HTTPS3 8L /* HTTPS and attempt HTTP/3 + added in 8.21.0 */ typedef enum { - CURLPROXY_LAST = 8 /* never use */ + CURLPROXY_LAST = 9 /* never use */ } curl_proxytype; /* this enum was added in 7.10 */ /* @@ -1494,8 +1496,8 @@ typedef enum { CURLOPT(CURLOPT_SHARE, CURLOPTTYPE_OBJECTPOINT, 100), /* indicates type of proxy. accepted values are CURLPROXY_HTTP (default), - CURLPROXY_HTTPS, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A and - CURLPROXY_SOCKS5. */ + CURLPROXY_HTTPS, CURLPROXY_HTTPS2, CURLPROXY_HTTPS3, CURLPROXY_SOCKS4, + CURLPROXY_SOCKS4A and CURLPROXY_SOCKS5. */ CURLOPT(CURLOPT_PROXYTYPE, CURLOPTTYPE_VALUES, 101), /* Set the Accept-Encoding string. Use this to tell a server you would like diff --git a/lib/Makefile.inc b/lib/Makefile.inc index 2c7259af0d..0a9e6ce311 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -150,8 +150,11 @@ LIB_CFILES = \ bufq.c \ bufref.c \ cf-dns.c \ + capsule.c \ + cf-capsule.c \ cf-h1-proxy.c \ cf-h2-proxy.c \ + cf-h3-proxy.c \ cf-haproxy.c \ cf-https-connect.c \ cf-ip-happy.c \ @@ -282,8 +285,11 @@ LIB_HFILES = \ bufq.h \ bufref.h \ cf-dns.h \ + capsule.h \ + cf-capsule.h \ cf-h1-proxy.h \ cf-h2-proxy.h \ + cf-h3-proxy.h \ cf-haproxy.h \ cf-https-connect.h \ cf-ip-happy.h \ diff --git a/lib/capsule.c b/lib/capsule.c new file mode 100644 index 0000000000..698cdcdb56 --- /dev/null +++ b/lib/capsule.c @@ -0,0 +1,281 @@ +/*************************************************************************** + * _ _ ____ _ + * 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" + +#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + +#include +#include "urldata.h" +#include "curlx/dynbuf.h" +#include "cfilters.h" +#include "curl_trc.h" +#include "bufq.h" +#include "capsule.h" + + +/** + * Convert 64-bit value from network byte order to host byte order + */ +static uint64_t capsule_ntohll(uint64_t value) +{ +#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + return value; +#elif (defined(__GNUC__) || defined(__clang__)) && \ + defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + return __builtin_bswap64(value); +#else + union { + uint64_t u64; + uint32_t u32[2]; + } src, dst; + + src.u64 = value; + dst.u32[0] = ntohl(src.u32[1]); + dst.u32[1] = ntohl(src.u32[0]); + return dst.u64; +#endif +} + +/** + * Encode a variable-length integer into a plain buffer. + * @param buf Output buffer (must have at least 8 bytes) + * @param value Value to encode (must be <= 0x3FFFFFFFFFFFFFFF) + * @return Number of bytes written + */ +static size_t capsule_encode_varint_buf(uint8_t *buf, uint64_t value) +{ + DEBUGASSERT(value <= 0x3FFFFFFFFFFFFFFF); + + if(value <= 0x3F) { + buf[0] = (uint8_t)value; + return 1; + } + else if(value <= 0x3FFF) { + uint16_t encoded = (uint16_t)value & 0x3FFF; + encoded = ntohs(encoded | 0x4000); + memcpy(buf, &encoded, 2); + return 2; + } + else if(value <= 0x3FFFFFFF) { + uint32_t encoded = (uint32_t)value & 0x3FFFFFFF; + encoded = ntohl(encoded | 0x80000000); + memcpy(buf, &encoded, 4); + return 4; + } + else { + uint64_t encoded = (uint64_t)value & 0x3FFFFFFFFFFFFFFF; + encoded = capsule_ntohll(encoded | 0xC000000000000000); + memcpy(buf, &encoded, 8); + return 8; + } +} + +static CURLcode capsule_peek_u8(struct bufq *recvbufq, + size_t offset, + uint8_t *pbyte) +{ + const unsigned char *peek = NULL; + size_t peeklen = 0; + + if(!Curl_bufq_peek_at(recvbufq, offset, &peek, &peeklen) || !peeklen) + return CURLE_AGAIN; + *pbyte = peek[0]; + return CURLE_OK; +} + +static CURLcode capsule_decode_varint_at(struct bufq *recvbufq, + size_t offset, + uint64_t *pvalue, + size_t *pconsumed) +{ + uint8_t first_byte, byte; + uint64_t value; + size_t nbytes; + size_t i; + CURLcode result; + + result = capsule_peek_u8(recvbufq, offset, &first_byte); + if(result) + return result; + + nbytes = (size_t)1 << (first_byte >> 6); /* 1, 2, 4 or 8 bytes */ + value = first_byte & 0x3F; + + for(i = 1; i < nbytes; ++i) { + result = capsule_peek_u8(recvbufq, offset + i, &byte); + if(result) + return result; + value = (value << 8) | byte; + } + + *pvalue = value; + *pconsumed = nbytes; + return CURLE_OK; +} + +size_t Curl_capsule_encap_udp_hdr(uint8_t *hdr, size_t hdrlen, + size_t payload_len) +{ + size_t off = 0; + DEBUGASSERT(hdrlen >= HTTP_CAPSULE_HEADER_MAX_SIZE); + if(hdrlen < HTTP_CAPSULE_HEADER_MAX_SIZE) + return 0; + hdr[off++] = 0; /* capsule type: HTTP Datagram */ + off += capsule_encode_varint_buf(hdr + off, (uint64_t)payload_len + 1); + hdr[off++] = 0; /* context ID */ + return off; +} + +CURLcode Curl_capsule_encap_udp_datagram(struct dynbuf *dyn, + const void *buf, size_t blen) +{ + CURLcode result; + uint8_t hdr[HTTP_CAPSULE_HEADER_MAX_SIZE]; + size_t hdr_len; + + curlx_dyn_init(dyn, HTTP_CAPSULE_HEADER_MAX_SIZE + blen); + hdr_len = Curl_capsule_encap_udp_hdr(hdr, sizeof(hdr), blen); + DEBUGASSERT(hdr_len); + if(!hdr_len) + return CURLE_FAILED_INIT; + + result = curlx_dyn_addn(dyn, hdr, hdr_len); + if(result) + return result; + + return curlx_dyn_addn(dyn, buf, blen); +} + +size_t Curl_capsule_process_udp_raw(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct bufq *recvbufq, + unsigned char *buf, size_t len, + CURLcode *err) +{ + const unsigned char *context_id, *capsule_type; + size_t read_size, varint_len; + uint64_t capsule_length; + size_t offset, payload_len; + size_t bytes_read = 0; + CURLcode result = CURLE_OK; + + if(!len) { + *err = CURLE_BAD_FUNCTION_ARGUMENT; + return 0; + } + + if(Curl_bufq_is_empty(recvbufq)) { + *err = CURLE_AGAIN; + return 0; + } + + if(!Curl_bufq_peek(recvbufq, &capsule_type, &read_size) || !read_size) { + *err = CURLE_AGAIN; + return 0; + } + + if(capsule_type[0]) { + infof(data, "Error! Invalid capsule type: %d", capsule_type[0]); + Curl_bufq_skip(recvbufq, 1); + *err = CURLE_RECV_ERROR; + return 0; + } + + offset = 1; + result = capsule_decode_varint_at(recvbufq, offset, &capsule_length, + &varint_len); + if(result == CURLE_AGAIN) { + *err = CURLE_AGAIN; + return 0; + } + else if(result) { + *err = CURLE_RECV_ERROR; + return 0; + } + offset += varint_len; + + if(!Curl_bufq_peek_at(recvbufq, offset, &context_id, &read_size) || + !read_size) { + *err = CURLE_AGAIN; + return 0; + } + + if(*context_id) { + infof(data, "Error! Invalid context ID: %02x", *context_id); + Curl_bufq_skip(recvbufq, offset + 1); + *err = CURLE_RECV_ERROR; + return 0; + } + offset += 1; + + if(!capsule_length) { + infof(data, "Error! Invalid capsule length: 0"); + Curl_bufq_skip(recvbufq, offset); + *err = CURLE_RECV_ERROR; + return 0; + } + if(capsule_length - 1 >= (uint64_t)SIZE_MAX) { + infof(data, "Error! Capsule length too large: %" CURL_FORMAT_CURL_OFF_T, + (curl_off_t)capsule_length); + *err = CURLE_RECV_ERROR; + return 0; + } + payload_len = (size_t)(capsule_length - 1); + + if(Curl_bufq_len(recvbufq) < offset + payload_len) { + *err = CURLE_AGAIN; + return 0; + } + + if(payload_len > len) { + infof(data, "UDP payload does not fit destination buffer: %zu > %zu", + payload_len, len); + Curl_bufq_skip(recvbufq, offset + payload_len); + *err = CURLE_RECV_ERROR; + return 0; + } + + Curl_bufq_skip(recvbufq, offset); + if(!payload_len) { + *err = CURLE_OK; + return 0; + } + result = Curl_bufq_read(recvbufq, buf, payload_len, &bytes_read); + if(result || (bytes_read != payload_len)) { + infof(data, "Error! Read less than expected %zu %zu", + payload_len, bytes_read); + *err = CURLE_RECV_ERROR; + return 0; + } + + if(cf && data) { + CURL_TRC_CF(data, cf, "Processed UDP capsule raw: size=%zu " + "length_left %zu", payload_len, Curl_bufq_len(recvbufq)); + } + *err = CURLE_OK; + return bytes_read; +} + +#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ diff --git a/lib/capsule.h b/lib/capsule.h new file mode 100644 index 0000000000..fa7dec19cb --- /dev/null +++ b/lib/capsule.h @@ -0,0 +1,77 @@ +#ifndef HEADER_CURL_CAPSULE_H +#define HEADER_CURL_CAPSULE_H +/*************************************************************************** + * _ _ ____ _ + * 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" + +#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + +#include "curlx/dynbuf.h" +#include "bufq.h" + +/* HTTP Capsule constants */ +#define HTTP_CAPSULE_HEADER_MAX_SIZE 10 + +/* HTTP Capsule function prototypes */ + +/** + * Write the capsule header (type + varint length + context ID) into `hdr`. + * @param hdr Output buffer (must be >= HTTP_CAPSULE_HEADER_MAX_SIZE) + * @param hdrlen Size of `hdr` in bytes + * @param payload_len Length of the UDP payload that follows + * @return Number of header bytes written, or 0 on error + */ +size_t Curl_capsule_encap_udp_hdr(uint8_t *hdr, size_t hdrlen, + size_t payload_len); + +/** + * Encapsulate UDP payload into HTTP Datagram capsule format + * @param dyn Dynamic buffer to write capsule to + * @param buf Payload buffer + * @param blen Payload buffer length + * @return CURLE_OK on success, error code on failure + */ +CURLcode Curl_capsule_encap_udp_datagram(struct dynbuf *dyn, + const void *buf, size_t blen); + +/** + * Process one UDP capsule from buffer into raw datagram payload bytes. + * @param cf Connection filter + * @param data Easy handle + * @param recvbufq Buffer queue containing capsule data + * @param buf Output buffer for one datagram payload + * @param len Size of output buffer in bytes + * @param err Error code output + * @return Number of payload bytes written. Check `err` for status. + */ +size_t Curl_capsule_process_udp_raw(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct bufq *recvbufq, + unsigned char *buf, size_t len, + CURLcode *err); + +#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ + +#endif /* HEADER_CURL_CAPSULE_H */ diff --git a/lib/cf-capsule.c b/lib/cf-capsule.c new file mode 100644 index 0000000000..dd740c0f15 --- /dev/null +++ b/lib/cf-capsule.c @@ -0,0 +1,253 @@ +/*************************************************************************** + * _ _ ____ _ + * 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" + +#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + +#include +#include "urldata.h" +#include "cfilters.h" +#include "curl_trc.h" +#include "curlx/dynbuf.h" +#include "bufq.h" +#include "capsule.h" +#include "cf-capsule.h" + +/* recv buffer: 4 chunks of 16KB = 64KB, enough for large datagrams */ +#define CAPSULE_RECV_CHUNKS 4 +#define CAPSULE_CHUNK_SIZE (16 * 1024) + +struct cf_capsule_ctx { + struct bufq recvbuf; + struct cf_call_data call_data; + unsigned char *pending; /* unsent capsule bytes from partial write */ + size_t pending_len; /* total length of pending buffer */ + size_t pending_offset; /* bytes already sent from pending */ + size_t pending_payload; /* original payload len for pending capsule */ +}; + +static void capsule_cf_destroy(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_capsule_ctx *ctx = cf->ctx; + (void)data; + if(ctx) { + Curl_bufq_free(&ctx->recvbuf); + curlx_free(ctx->pending); + curlx_safefree(ctx); + } +} + +static void capsule_cf_close(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_capsule_ctx *ctx = cf->ctx; + + CURL_TRC_CF(data, cf, "close"); + cf->connected = FALSE; + if(ctx) { + Curl_bufq_reset(&ctx->recvbuf); + curlx_safefree(ctx->pending); + ctx->pending_len = 0; + ctx->pending_offset = 0; + ctx->pending_payload = 0; + } + if(cf->next) + cf->next->cft->do_close(cf->next, data); +} + +static CURLcode capsule_cf_connect(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool *done) +{ + if(cf->connected) { + *done = TRUE; + return CURLE_OK; + } + if(cf->next) { + CURLcode result = cf->next->cft->do_connect(cf->next, data, done); + if(!result && *done) + cf->connected = TRUE; + return result; + } + *done = FALSE; + return CURLE_OK; +} + +static CURLcode capsule_cf_send(struct Curl_cfilter *cf, + struct Curl_easy *data, + const uint8_t *buf, size_t len, + bool eos, size_t *pnwritten) +{ + struct cf_capsule_ctx *ctx = cf->ctx; + struct dynbuf dyn; + size_t nwritten = 0; + size_t capsule_len; + size_t remaining; + CURLcode result; + + (void)eos; + *pnwritten = 0; + + if(ctx->pending) { + /* flush remaining bytes from a partially sent capsule */ + remaining = ctx->pending_len - ctx->pending_offset; + result = Curl_conn_cf_send(cf->next, data, + ctx->pending + ctx->pending_offset, + remaining, FALSE, &nwritten); + if(result && result != CURLE_AGAIN) { + curlx_safefree(ctx->pending); + return result; + } + ctx->pending_offset += nwritten; + if(ctx->pending_offset < ctx->pending_len) + return CURLE_AGAIN; + /* pending capsule has been fully flusehd */ + *pnwritten = ctx->pending_payload; + curlx_safefree(ctx->pending); + return CURLE_OK; + } + + /* encapsulate new payload into a capsule */ + result = Curl_capsule_encap_udp_datagram(&dyn, buf, len); + if(result) { + curlx_dyn_free(&dyn); + return result; + } + capsule_len = curlx_dyn_len(&dyn); + + result = Curl_conn_cf_send(cf->next, data, + (const uint8_t *)curlx_dyn_ptr(&dyn), + capsule_len, FALSE, &nwritten); + if(result && result != CURLE_AGAIN) { + curlx_dyn_free(&dyn); + return result; + } + + if(nwritten < capsule_len) { + /* partial or zero write - save unsent capsule bytes as pending */ + remaining = capsule_len - nwritten; + ctx->pending = curlx_malloc(remaining); + if(!ctx->pending) { + curlx_dyn_free(&dyn); + return CURLE_OUT_OF_MEMORY; + } + memcpy(ctx->pending, + curlx_dyn_ptr(&dyn) + nwritten, remaining); + ctx->pending_len = remaining; + ctx->pending_offset = 0; + ctx->pending_payload = len; + curlx_dyn_free(&dyn); + return CURLE_AGAIN; + } + + /* entire capsule sent */ + curlx_dyn_free(&dyn); + *pnwritten = len; + return CURLE_OK; +} + +static CURLcode capsule_cf_recv(struct Curl_cfilter *cf, + struct Curl_easy *data, + char *buf, size_t len, + size_t *pnread) +{ + struct cf_capsule_ctx *ctx = cf->ctx; + CURLcode result; + size_t nread; + + *pnread = 0; + + /* fill our receive buffer from the filter below */ + while(!Curl_bufq_is_full(&ctx->recvbuf)) { + result = Curl_cf_recv_bufq(cf->next, data, &ctx->recvbuf, 0, &nread); + if(result == CURLE_AGAIN) + break; + if(result) + return result; + if(!nread) + break; + } + + /* try to extract a complete capsule datagram */ + *pnread = Curl_capsule_process_udp_raw(cf, data, &ctx->recvbuf, + (unsigned char *)buf, len, + &result); + return result; +} + +static bool capsule_cf_data_pending(struct Curl_cfilter *cf, + const struct Curl_easy *data) +{ + struct cf_capsule_ctx *ctx = cf->ctx; + + if(ctx && !Curl_bufq_is_empty(&ctx->recvbuf)) + return TRUE; + return cf->next ? cf->next->cft->has_data_pending(cf->next, data) : FALSE; +} + +struct Curl_cftype Curl_cft_capsule = { + "CAPSULE", + 0, + 0, + capsule_cf_destroy, + capsule_cf_connect, + capsule_cf_close, + Curl_cf_def_shutdown, + Curl_cf_def_adjust_pollset, + capsule_cf_data_pending, + capsule_cf_send, + capsule_cf_recv, + Curl_cf_def_cntrl, + Curl_cf_def_conn_is_alive, + Curl_cf_def_conn_keep_alive, + Curl_cf_def_query, +}; + +CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at, + struct Curl_easy *data) +{ + struct Curl_cfilter *cf; + struct cf_capsule_ctx *ctx; + CURLcode result; + + (void)data; + ctx = curlx_calloc(1, sizeof(*ctx)); + if(!ctx) + return CURLE_OUT_OF_MEMORY; + + Curl_bufq_init2(&ctx->recvbuf, CAPSULE_CHUNK_SIZE, CAPSULE_RECV_CHUNKS, + BUFQ_OPT_SOFT_LIMIT); + + result = Curl_cf_create(&cf, &Curl_cft_capsule, ctx); + if(result) { + Curl_bufq_free(&ctx->recvbuf); + curlx_free(ctx); + return result; + } + Curl_conn_cf_insert_after(cf_at, cf); + return CURLE_OK; +} + +#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ diff --git a/lib/cf-capsule.h b/lib/cf-capsule.h new file mode 100644 index 0000000000..437c9681b6 --- /dev/null +++ b/lib/cf-capsule.h @@ -0,0 +1,40 @@ +#ifndef HEADER_CURL_CF_CAPSULE_H +#define HEADER_CURL_CF_CAPSULE_H +/*************************************************************************** + * _ _ ____ _ + * 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" + +#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + +/* Insert a capsule protocol filter after `cf_at` in the filter chain. + * The capsule filter encapsulates/decapsulates UDP datagrams using + * the HTTP Datagram capsule format (RFC 9297). */ +CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at, + struct Curl_easy *data); + +extern struct Curl_cftype Curl_cft_capsule; + +#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ + +#endif /* HEADER_CURL_CF_CAPSULE_H */ diff --git a/lib/cf-h1-proxy.c b/lib/cf-h1-proxy.c index 0f1c392d48..5dd02b2b0a 100644 --- a/lib/cf-h1-proxy.c +++ b/lib/cf-h1-proxy.c @@ -25,6 +25,8 @@ #if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + +#include #include "urldata.h" #include "curlx/dynbuf.h" #include "sendf.h" @@ -33,6 +35,7 @@ #include "http_proxy.h" #include "select.h" #include "progress.h" +#include "multiif.h" #include "cfilters.h" #include "cf-h1-proxy.h" #include "connect.h" @@ -40,7 +43,6 @@ #include "strcase.h" #include "curlx/strparse.h" - typedef enum { H1_TUNNEL_INIT, /* init/default/no tunnel state */ H1_TUNNEL_CONNECT, /* CONNECT request is being send */ @@ -72,6 +74,12 @@ struct h1_tunnel_state { BIT(leading_unfold); }; +/* Persistent context for the H1-PROXY filter */ +struct cf_h1_proxy_ctx { + struct h1_tunnel_state *ts; + BIT(udp_tunnel); +}; + static bool tunnel_is_established(struct h1_tunnel_state *ts) { return ts && (ts->tunnel_state == H1_TUNNEL_ESTABLISHED); @@ -82,6 +90,12 @@ static bool tunnel_is_failed(struct h1_tunnel_state *ts) return ts && (ts->tunnel_state == H1_TUNNEL_FAILED); } +static bool h1_proxy_is_udp(struct Curl_cfilter *cf) +{ + struct cf_h1_proxy_ctx *pctx = cf->ctx; + return (pctx->udp_tunnel ? TRUE : FALSE); +} + static CURLcode tunnel_reinit(struct Curl_cfilter *cf, struct Curl_easy *data, struct h1_tunnel_state *ts) @@ -97,6 +111,8 @@ static CURLcode tunnel_reinit(struct Curl_cfilter *cf, ts->close_connection = FALSE; ts->maybe_folded = FALSE; ts->leading_unfold = FALSE; + ts->nsent = 0; + ts->headerlines = 0; return CURLE_OK; } @@ -158,7 +174,9 @@ static void h1_tunnel_go_state(struct Curl_cfilter *cf, case H1_TUNNEL_ESTABLISHED: CURL_TRC_CF(data, cf, "new tunnel state 'established'"); - infof(data, "CONNECT phase completed"); + infof(data, "CONNECT%s phase completed for HTTP proxy", + h1_proxy_is_udp(cf) ? "-UDP" : ""); + data->state.authproxy.done = TRUE; data->state.authproxy.multipass = FALSE; FALLTHROUGH(); @@ -195,11 +213,12 @@ static void cf_tunnel_free(struct Curl_cfilter *cf, struct Curl_easy *data) { if(cf) { - struct h1_tunnel_state *ts = cf->ctx; + struct cf_h1_proxy_ctx *pctx = cf->ctx; + struct h1_tunnel_state *ts = pctx ? pctx->ts : NULL; if(ts) { h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data); tunnel_free(ts, data); - cf->ctx = NULL; + pctx->ts = NULL; } } } @@ -217,17 +236,17 @@ static CURLcode start_CONNECT(struct Curl_cfilter *cf, int http_minor; CURLcode result; + DEBUGASSERT(data); /* This only happens if we have looped here due to authentication reasons, and we do not really use the newly cloned URL here then. Free it. */ curlx_safefree(data->req.newurl); - result = Curl_http_proxy_create_CONNECT(&req, cf, data, - ts->dest, ts->httpversion); + result = Curl_http_proxy_create_tunnel_request(&req, cf, data, ts->dest, + PROXY_HTTP_V1, + h1_proxy_is_udp(cf)); if(result) goto out; - infof(data, "Establish HTTP proxy tunnel to %s", req->authority); - curlx_dyn_reset(&ts->request_data); ts->nsent = 0; ts->headerlines = 0; @@ -280,6 +299,92 @@ out: return result; } +static CURLcode on_resp_header_udp(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h1_tunnel_state *ts, + const char *header) +{ + CURLcode result = CURLE_OK; + struct SingleRequest *k = &data->req; + + if((checkprefix("WWW-Authenticate:", header) && (401 == k->httpcode)) || + (checkprefix("Proxy-authenticate:", header) && (407 == k->httpcode))) { + + bool proxy = (k->httpcode == 407); + char *auth = Curl_copy_header_value(header); + if(!auth) + return CURLE_OUT_OF_MEMORY; + + CURL_TRC_CF(data, cf, "CONNECT-UDP: fwd auth header '%s'", header); + result = Curl_http_input_auth(data, proxy, auth); + + curlx_free(auth); + + if(result) + return result; + } + else if(checkprefix("Content-Length:", header)) { + if(k->httpcode / 100 == 2 || k->httpcode == 101) { + infof(data, "Ignoring Content-Length in CONNECT-UDP %03d response", + k->httpcode); + } + else { + const char *p = header + strlen("Content-Length:"); + if(curlx_str_numblanks(&p, &ts->cl)) { + failf(data, "Unsupported Content-Length value"); + return CURLE_WEIRD_SERVER_REPLY; + } + } + } + else if(checkprefix("Transfer-Encoding:", header)) { + if(k->httpcode / 100 == 2 || k->httpcode == 101) { + infof(data, "Ignoring Transfer-Encoding in " + "CONNECT-UDP %03d response", k->httpcode); + } + else if(Curl_compareheader(header, + STRCONST("Transfer-Encoding:"), + STRCONST("chunked"))) { + CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> " + "Transfer-Encoding: chunked"); + ts->chunked_encoding = TRUE; + /* reset our chunky engine */ + Curl_httpchunk_reset(data, &ts->ch, TRUE); + } + } + else if(checkprefix("Capsule-protocol:", header)) { + if(Curl_compareheader(header, + STRCONST("Capsule-protocol:"), + STRCONST("?1"))) { + CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> Capsule-protocol: ?1"); + } + } + else if(Curl_compareheader(header, + STRCONST("Connection:"), STRCONST("close"))) { + ts->close_connection = TRUE; + CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> Connection: close"); + } + else if(Curl_compareheader(header, + STRCONST("Proxy-Connection:"), + STRCONST("close"))) { + ts->close_connection = TRUE; + CURL_TRC_CF(data, cf, + "CONNECT-UDP Response --> Proxy-Connection: close"); + } + else if(!strncmp(header, "HTTP/1.", 7) && + ((header[7] == '0') || (header[7] == '1')) && + (header[8] == ' ') && + ISDIGIT(header[9]) && ISDIGIT(header[10]) && ISDIGIT(header[11]) && + !ISDIGIT(header[12])) { + /* store the HTTP code from the proxy */ + data->info.httpproxycode = k->httpcode = + ((header[9] - '0') * 100) + + ((header[10] - '0') * 10) + + (header[11] - '0'); + CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> %d", k->httpcode); + } + return result; +} + static CURLcode on_resp_header(struct Curl_cfilter *cf, struct Curl_easy *data, struct h1_tunnel_state *ts, @@ -418,7 +523,13 @@ static CURLcode single_header(struct Curl_cfilter *cf, return result; } - result = on_resp_header(cf, data, ts, linep); + if(h1_proxy_is_udp(cf)) { + result = on_resp_header_udp(cf, data, ts, linep); + } + else { + result = on_resp_header(cf, data, ts, linep); + } + if(result) return result; @@ -460,6 +571,13 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf, } if(!nread) { + if(ts->maybe_folded) { + /* EOF right after LF: finalize the pending header line. */ + result = single_header(cf, data, ts); + if(result) + return result; + ts->maybe_folded = FALSE; + } if(data->set.proxyauth && data->state.authproxy.avail && data->req.hd_proxy_auth) { /* proxy auth was requested and there was proxy auth available, @@ -551,12 +669,16 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf, ts->maybe_folded = TRUE; } + if(result) + return result; } /* while there is buffer left and loop is requested */ if(error) result = CURLE_RECV_ERROR; *done = (ts->keepon == KEEPON_DONE); - if(!result && *done && data->info.httpproxycode / 100 != 2) { + if(!result && *done && + data->info.httpproxycode / 100 != 2 && + !(h1_proxy_is_udp(cf) && data->info.httpproxycode == 101)) { /* Deal with the possibly already received authenticate headers. 'newurl' is set to a new URL if we must loop. */ result = Curl_http_auth_act(data); @@ -637,7 +759,7 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf, infof(data, "Connect me again please"); Curl_conn_cf_close(cf, data); result = Curl_conn_cf_connect(cf->next, data, &done); - goto out; + return result; } else { /* staying on this connection, reset state */ @@ -653,17 +775,36 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf, } while(data->req.newurl); DEBUGASSERT(ts->tunnel_state == H1_TUNNEL_RESPONSE); - if(data->info.httpproxycode / 100 != 2) { - /* a non-2xx response and we have no next URL to try. */ - curlx_safefree(data->req.newurl); - h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data); - failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode); - return CURLE_COULDNT_CONNECT; + if(h1_proxy_is_udp(cf)) { + /* RFC 9298: Accept 101 Upgrade for HTTP/1.1 and + * 2xx responses for HTTP/2 and HTTP/3 proxies. */ + if(data->info.httpproxycode / 100 != 2 && + data->info.httpproxycode != 101) { + curlx_safefree(data->req.newurl); + h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data); + failf(data, "CONNECT-UDP tunnel failed, response %d", + data->req.httpcode); + return CURLE_COULDNT_CONNECT; + } + } + else { + if(data->info.httpproxycode / 100 != 2) { + /* a non-2xx response and we have no next URL to try. */ + curlx_safefree(data->req.newurl); + h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data); + failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode); + return CURLE_COULDNT_CONNECT; + } } /* 2xx response, SUCCESS! */ + /* 101 Switching Protocol for CONNECT-UDP */ h1_tunnel_go_state(cf, ts, H1_TUNNEL_ESTABLISHED, data); - infof(data, "CONNECT tunnel established, response %d", - data->info.httpproxycode); + if(h1_proxy_is_udp(cf)) + infof(data, "CONNECT-UDP tunnel established, response %d", + data->info.httpproxycode); + else + infof(data, "CONNECT tunnel established, response %d", + data->info.httpproxycode); result = CURLE_OK; out: @@ -677,7 +818,8 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf, bool *done) { CURLcode result; - struct h1_tunnel_state *ts = cf->ctx; + struct cf_h1_proxy_ctx *pctx = cf->ctx; + struct h1_tunnel_state *ts = pctx->ts; if(cf->connected) { *done = TRUE; @@ -694,7 +836,7 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf, result = tunnel_init(cf, data, &ts); if(result) return result; - cf->ctx = ts; + pctx->ts = ts; } /* We want "seamless" operations through HTTP proxy tunnel */ @@ -705,14 +847,13 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf, curlx_safefree(data->req.hd_proxy_auth); out: - *done = (result == CURLE_OK) && tunnel_is_established(cf->ctx); + *done = (result == CURLE_OK) && tunnel_is_established(pctx->ts); if(*done) { cf->connected = TRUE; /* The real request will follow the CONNECT, reset request partially */ Curl_req_soft_reset(&data->req, data); Curl_client_reset(data); Curl_pgrsReset(data); - cf_tunnel_free(cf, data); } return result; @@ -722,7 +863,8 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf, struct Curl_easy *data, struct easy_pollset *ps) { - struct h1_tunnel_state *ts = cf->ctx; + struct cf_h1_proxy_ctx *pctx = cf->ctx; + struct h1_tunnel_state *ts = pctx->ts; CURLcode result = CURLE_OK; if(!cf->connected) { @@ -742,37 +884,49 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf, else result = Curl_pollset_set_out_only(data, ps, sock); } + else { + if(cf->next) + result = cf->next->cft->adjust_pollset(cf->next, data, ps); + } return result; } +static bool cf_h1_proxy_data_pending(struct Curl_cfilter *cf, + const struct Curl_easy *data) +{ + return cf->next ? cf->next->cft->has_data_pending(cf->next, data) : FALSE; +} + static void cf_h1_proxy_destroy(struct Curl_cfilter *cf, struct Curl_easy *data) { CURL_TRC_CF(data, cf, "destroy"); cf_tunnel_free(cf, data); + curlx_safefree(cf->ctx); } static void cf_h1_proxy_close(struct Curl_cfilter *cf, struct Curl_easy *data) { + struct cf_h1_proxy_ctx *pctx = cf->ctx; CURL_TRC_CF(data, cf, "close"); - if(cf) { - cf->connected = FALSE; - if(cf->ctx) { - h1_tunnel_go_state(cf, cf->ctx, H1_TUNNEL_INIT, data); - } - if(cf->next) - cf->next->cft->do_close(cf->next, data); - } + cf->connected = FALSE; + if(pctx && pctx->ts) + h1_tunnel_go_state(cf, pctx->ts, H1_TUNNEL_INIT, data); + if(cf->next) + cf->next->cft->do_close(cf->next, data); } static CURLcode cf_h1_proxy_query(struct Curl_cfilter *cf, struct Curl_easy *data, int query, int *pres1, void *pres2) { - struct h1_tunnel_state *ts = cf->ctx; + struct cf_h1_proxy_ctx *pctx = cf->ctx; + struct h1_tunnel_state *ts = pctx ? pctx->ts : NULL; switch(query) { case CF_QUERY_HOST_PORT: + if(!ts || !ts->dest) + break; *pres1 = (int)ts->dest->port; *((const char **)pres2) = ts->dest->hostname; return CURLE_OK; @@ -799,7 +953,7 @@ struct Curl_cftype Curl_cft_h1_proxy = { cf_h1_proxy_close, Curl_cf_def_shutdown, cf_h1_proxy_adjust_pollset, - Curl_cf_def_data_pending, + cf_h1_proxy_data_pending, Curl_cf_def_send, Curl_cf_def_recv, Curl_cf_def_cntrl, @@ -811,9 +965,11 @@ struct Curl_cftype Curl_cft_h1_proxy = { CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, struct Curl_peer *dest, - int httpversion) + int httpversion, + bool udp_tunnel) { struct Curl_cfilter *cf; + struct cf_h1_proxy_ctx *pctx; struct h1_tunnel_state *ts; CURLcode result; @@ -834,9 +990,18 @@ CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at, curlx_dyn_init(&ts->request_data, DYN_HTTP_REQUEST); Curl_httpchunk_init(data, &ts->ch, TRUE); - result = Curl_cf_create(&cf, &Curl_cft_h1_proxy, ts); - if(result) + pctx = curlx_calloc(1, sizeof(*pctx)); + if(!pctx) { + result = CURLE_OUT_OF_MEMORY; goto out; + } + pctx->udp_tunnel = udp_tunnel; + pctx->ts = ts; + result = Curl_cf_create(&cf, &Curl_cft_h1_proxy, pctx); + if(result) { + curlx_free(pctx); + goto out; + } ts = NULL; Curl_conn_cf_insert_after(cf_at, cf); diff --git a/lib/cf-h1-proxy.h b/lib/cf-h1-proxy.h index 10adcdfb4f..3255bf79f2 100644 --- a/lib/cf-h1-proxy.h +++ b/lib/cf-h1-proxy.h @@ -32,7 +32,8 @@ struct Curl_peer; CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, struct Curl_peer *dest, - int httpversion); + int httpversion, + bool udp_tunnel); extern struct Curl_cftype Curl_cft_h1_proxy; diff --git a/lib/cf-h2-proxy.c b/lib/cf-h2-proxy.c index 297afb3c8c..b2cc49896f 100644 --- a/lib/cf-h2-proxy.c +++ b/lib/cf-h2-proxy.c @@ -42,6 +42,7 @@ #include "sendf.h" #include "select.h" #include "cf-h2-proxy.h" +#include "capsule.h" #define PROXY_H2_CHUNK_SIZE (16 * 1024) @@ -96,6 +97,20 @@ static CURLcode tunnel_stream_init(struct tunnel_stream *ts, return CURLE_OK; } +static void tunnel_stream_reset(struct tunnel_stream *ts) +{ + Curl_http_resp_free(ts->resp); + ts->resp = NULL; + Curl_bufq_reset(&ts->recvbuf); + Curl_bufq_reset(&ts->sendbuf); + ts->stream_id = -1; + ts->error = 0; + ts->has_final_response = FALSE; + ts->closed = FALSE; + ts->reset = FALSE; + ts->state = H2_TUNNEL_INIT; +} + static void tunnel_stream_clear(struct tunnel_stream *ts) { Curl_http_resp_free(ts->resp); @@ -109,9 +124,11 @@ static void tunnel_stream_clear(struct tunnel_stream *ts) static void h2_tunnel_go_state(struct Curl_cfilter *cf, struct tunnel_stream *ts, h2_tunnel_state new_state, - struct Curl_easy *data) + struct Curl_easy *data, + bool udp_tunnel) { (void)cf; + (void)udp_tunnel; if(ts->state == new_state) return; @@ -127,7 +144,7 @@ static void h2_tunnel_go_state(struct Curl_cfilter *cf, switch(new_state) { case H2_TUNNEL_INIT: CURL_TRC_CF(data, cf, "[%d] new tunnel state 'init'", ts->stream_id); - tunnel_stream_clear(ts); + tunnel_stream_reset(ts); break; case H2_TUNNEL_CONNECT: @@ -143,7 +160,8 @@ static void h2_tunnel_go_state(struct Curl_cfilter *cf, case H2_TUNNEL_ESTABLISHED: CURL_TRC_CF(data, cf, "[%d] new tunnel state 'established'", ts->stream_id); - infof(data, "CONNECT phase completed"); + infof(data, "CONNECT%s phase completed for HTTP/2 proxy", + udp_tunnel ? "-UDP" : ""); data->state.authproxy.done = TRUE; data->state.authproxy.multipass = FALSE; FALLTHROUGH(); @@ -175,6 +193,7 @@ struct cf_h2_proxy_ctx { BIT(rcvd_goaway); BIT(sent_goaway); BIT(nw_out_blocked); + BIT(udp_tunnel); }; /* How to access `call_data` from a cf_h2 filter */ @@ -211,7 +230,8 @@ static void drain_tunnel(struct Curl_cfilter *cf, struct cf_h2_proxy_ctx *ctx = cf->ctx; (void)cf; if(!tunnel->closed && !tunnel->reset && - !Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) + (!Curl_bufq_is_empty(&ctx->tunnel.sendbuf) || + !Curl_bufq_is_empty(&ctx->tunnel.recvbuf))) Curl_multi_mark_dirty(data); } @@ -749,15 +769,15 @@ static CURLcode submit_CONNECT(struct Curl_cfilter *cf, CURLcode result; struct httpreq *req = NULL; - result = Curl_http_proxy_create_CONNECT(&req, cf, data, ctx->dest, 20); + result = Curl_http_proxy_create_tunnel_request(&req, cf, data, ctx->dest, + PROXY_HTTP_V2, + (bool)ctx->udp_tunnel); if(result) goto out; result = Curl_creader_set_null(data); if(result) goto out; - infof(data, "Establish HTTP/2 proxy tunnel to %s", req->authority); - result = proxy_h2_submit(&ts->stream_id, cf, data, ctx->h2, req, NULL, ts, tunnel_send_callback, cf); if(result) { @@ -777,41 +797,30 @@ static CURLcode inspect_response(struct Curl_cfilter *cf, struct Curl_easy *data, struct tunnel_stream *ts) { - CURLcode result = CURLE_OK; - struct dynhds_entry *auth_reply = NULL; - (void)cf; + struct cf_h2_proxy_ctx *ctx = cf->ctx; + proxy_inspect_result res; + CURLcode result; - DEBUGASSERT(ts->resp); - if(ts->resp->status / 100 == 2) { - infof(data, "CONNECT tunnel established, response %d", ts->resp->status); - h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data); - return CURLE_OK; + result = Curl_http_proxy_inspect_tunnel_response( + cf, data, ts->resp, (bool)ctx->udp_tunnel, &res); + if(result) + return result; + switch(res) { + case PROXY_INSPECT_OK: + h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data, + (bool)ctx->udp_tunnel); + break; + case PROXY_INSPECT_FAILED: + h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data, + (bool)ctx->udp_tunnel); + result = CURLE_COULDNT_CONNECT; + break; + case PROXY_INSPECT_AUTH_RETRY: + h2_tunnel_go_state(cf, ts, H2_TUNNEL_INIT, data, + (bool)ctx->udp_tunnel); + break; } - - if(ts->resp->status == 401) { - auth_reply = Curl_dynhds_cget(&ts->resp->headers, "WWW-Authenticate"); - } - else if(ts->resp->status == 407) { - auth_reply = Curl_dynhds_cget(&ts->resp->headers, "Proxy-Authenticate"); - } - - if(auth_reply) { - CURL_TRC_CF(data, cf, "[0] CONNECT: fwd auth header '%s'", - auth_reply->value); - result = Curl_http_input_auth(data, ts->resp->status == 407, - auth_reply->value); - if(result) - return result; - if(data->req.newurl) { - /* Indicator that we should try again */ - curlx_safefree(data->req.newurl); - h2_tunnel_go_state(cf, ts, H2_TUNNEL_INIT, data); - return CURLE_OK; - } - } - - /* Seems to have failed */ - return CURLE_COULDNT_CONNECT; + return result; } static CURLcode H2_CONNECT(struct Curl_cfilter *cf, @@ -831,7 +840,8 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf, result = submit_CONNECT(cf, data, ts); if(result) goto out; - h2_tunnel_go_state(cf, ts, H2_TUNNEL_CONNECT, data); + h2_tunnel_go_state(cf, ts, H2_TUNNEL_CONNECT, data, + (bool)ctx->udp_tunnel); FALLTHROUGH(); case H2_TUNNEL_CONNECT: @@ -840,12 +850,14 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf, if(!result) result = proxy_h2_progress_egress(cf, data); if(result && result != CURLE_AGAIN) { - h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data); + h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data, + (bool)ctx->udp_tunnel); break; } if(ts->has_final_response) { - h2_tunnel_go_state(cf, ts, H2_TUNNEL_RESPONSE, data); + h2_tunnel_go_state(cf, ts, H2_TUNNEL_RESPONSE, data, + (bool)ctx->udp_tunnel); } else { result = CURLE_OK; @@ -874,7 +886,8 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf, out: if((result && (result != CURLE_AGAIN)) || ctx->tunnel.closed) - h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data); + h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data, + (bool)ctx->udp_tunnel); return result; } @@ -1231,7 +1244,8 @@ static CURLcode cf_h2_proxy_recv(struct Curl_cfilter *cf, result = Curl_1st_fatal(result, proxy_h2_progress_egress(cf, data)); out: - if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) && + if((!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) || + !Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) && (!result || (result == CURLE_AGAIN))) { /* data pending and no fatal error to report. Need to trigger * draining to avoid stalling when no socket events happen. */ @@ -1297,7 +1311,8 @@ static CURLcode cf_h2_proxy_send(struct Curl_cfilter *cf, } out: - if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) && + if((!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) || + !Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) && (!result || (result == CURLE_AGAIN))) { /* data pending and no fatal error to report. Need to trigger * draining to avoid stalling when no socket events happen. */ @@ -1477,7 +1492,8 @@ struct Curl_cftype Curl_cft_h2_proxy = { CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf, struct Curl_easy *data, - struct Curl_peer *dest) + struct Curl_peer *dest, + bool udp_tunnel) { struct Curl_cfilter *cf_h2_proxy = NULL; struct cf_h2_proxy_ctx *ctx; @@ -1488,6 +1504,7 @@ CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf, if(!ctx) goto out; Curl_peer_link(&ctx->dest, dest); + ctx->udp_tunnel = udp_tunnel; result = Curl_cf_create(&cf_h2_proxy, &Curl_cft_h2_proxy, ctx); if(result) @@ -1501,3 +1518,6 @@ out: } #endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_PROXY && USE_NGHTTP2 */ + +/* Do not leak this filter's call_data accessor in unity builds. */ +#undef CF_CTX_CALL_DATA diff --git a/lib/cf-h2-proxy.h b/lib/cf-h2-proxy.h index 1056a32907..07e3c9aedf 100644 --- a/lib/cf-h2-proxy.h +++ b/lib/cf-h2-proxy.h @@ -29,7 +29,8 @@ CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf, struct Curl_easy *data, - struct Curl_peer *dest); + struct Curl_peer *dest, + bool udp_tunnel); extern struct Curl_cftype Curl_cft_h2_proxy; diff --git a/lib/cf-h3-proxy.c b/lib/cf-h3-proxy.c new file mode 100644 index 0000000000..1896ba6302 --- /dev/null +++ b/lib/cf-h3-proxy.c @@ -0,0 +1,3478 @@ +/*************************************************************************** + * _ _ ____ _ + * 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" + +#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY) && \ + defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) && \ + defined(USE_NGTCP2) && defined(USE_OPENSSL) + +#include +#include +#ifdef USE_OPENSSL +#include +#if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) +#include +#elif defined(OPENSSL_QUIC_API2) +#include +#else +#include +#endif +#include "vtls/openssl.h" +#endif /* USE_OPENSSL */ + +#include + +#include "urldata.h" +#include "hash.h" +#include "sendf.h" +#include "multiif.h" +#include "cfilters.h" +#include "cf-socket.h" +#include "connect.h" +#include "progress.h" +#include "curlx/fopen.h" +#include "curlx/dynbuf.h" +#include "dynhds.h" +#include "http_proxy.h" +#include "select.h" +#include "uint-hash.h" +#include "vquic/vquic.h" +#include "vquic/vquic_int.h" +#include "vquic/vquic-tls.h" +#include "vtls/vtls.h" +#include "vtls/vtls_scache.h" +#include "curl_trc.h" +#include "cf-h3-proxy.h" +#include "url.h" +#include "capsule.h" +#include "rand.h" + +/* A stream window is the maximum amount we need to buffer for + * each active transfer. We use HTTP/3 flow control and only ACK + * when we take things out of the buffer. + * Chunk size is large enough to take a full DATA frame */ +#define PROXY_H3_STREAM_WINDOW_SIZE (128 * 1024) +#define PROXY_H3_STREAM_WINDOW_SIZE_MAX (10 * 1024 * 1024) +#define PROXY_H3_STREAM_CHUNK_SIZE (16 * 1024) + +/* The pool keeps spares around and half of a full stream window + * seems good. More does not seem to improve performance. + * The benefit of the pool is that stream buffer to not keep + * spares. Memory consumption goes down when streams run empty, + * have a large upload done, etc. */ +#define PROXY_H3_STREAM_POOL_SPARES \ + ((PROXY_H3_STREAM_WINDOW_SIZE / PROXY_H3_STREAM_CHUNK_SIZE) / 2) + +#define PROXY_H3_STREAM_RECV_CHUNKS \ + (PROXY_H3_STREAM_WINDOW_SIZE / PROXY_H3_STREAM_CHUNK_SIZE) +#define PROXY_H3_STREAM_SEND_CHUNKS \ + (PROXY_H3_STREAM_WINDOW_SIZE / PROXY_H3_STREAM_CHUNK_SIZE) + +#define PROXY_QUIC_MAX_STREAMS (256*1024) +#define PROXY_QUIC_HANDSHAKE_TIMEOUT (10*NGTCP2_SECONDS) + +typedef enum +{ + H3_TUNNEL_INIT, /* init/default/no tunnel state */ + H3_TUNNEL_CONNECT, /* CONNECT request is being sent */ + H3_TUNNEL_RESPONSE, /* CONNECT response received completely */ + H3_TUNNEL_ESTABLISHED, + H3_TUNNEL_FAILED +} h3_tunnel_state; + +struct h3_proxy_stream_ctx; + +struct h3_tunnel_stream +{ + struct http_resp *resp; + char *authority; + struct h3_proxy_stream_ctx *stream; + int64_t stream_id; + h3_tunnel_state state; + BIT(has_final_response); + BIT(closed); +}; + +static CURLcode h3_tunnel_stream_init(struct h3_tunnel_stream *ts, + struct Curl_peer *dest) +{ + ts->state = H3_TUNNEL_INIT; + ts->stream_id = -1; + ts->has_final_response = FALSE; + + /* host:port with IPv6 support */ + ts->authority = curl_maprintf("%s%s%s:%u", dest->ipv6 ? "[" : "", + dest->hostname, + dest->ipv6 ? "]" : "", + dest->port); + if(!ts->authority) + return CURLE_OUT_OF_MEMORY; + + return CURLE_OK; +} + +static void h3_tunnel_stream_reset(struct h3_tunnel_stream *ts) +{ + Curl_http_resp_free(ts->resp); + ts->resp = NULL; + ts->stream = NULL; + ts->stream_id = -1; + ts->has_final_response = FALSE; + ts->closed = FALSE; + ts->state = H3_TUNNEL_INIT; +} + +static void h3_tunnel_stream_clear(struct h3_tunnel_stream *ts) +{ + Curl_http_resp_free(ts->resp); + curlx_safefree(ts->authority); + memset(ts, 0, sizeof(*ts)); + ts->state = H3_TUNNEL_INIT; +} + +static void h3_tunnel_go_state(struct Curl_cfilter *cf, + struct h3_tunnel_stream *ts, + h3_tunnel_state new_state, + struct Curl_easy *data, + bool udp_tunnel) +{ + (void)cf; + (void)udp_tunnel; + + if(ts->state == new_state) + return; + /* leaving this one */ + switch(ts->state) { + case H3_TUNNEL_CONNECT: + data->req.ignorebody = FALSE; + break; + default: + break; + } + /* entering this one */ + switch(new_state) { + case H3_TUNNEL_INIT: + CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'init'", + ts->stream_id); + h3_tunnel_stream_reset(ts); + break; + + case H3_TUNNEL_CONNECT: + CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'connect'", + ts->stream_id); + ts->state = H3_TUNNEL_CONNECT; + break; + + case H3_TUNNEL_RESPONSE: + CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'response'", + ts->stream_id); + ts->state = H3_TUNNEL_RESPONSE; + break; + + case H3_TUNNEL_ESTABLISHED: + CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'established'", + ts->stream_id); + infof(data, "CONNECT%s phase completed for HTTP/3 proxy", + udp_tunnel ? "-UDP" : ""); + data->state.authproxy.done = TRUE; + data->state.authproxy.multipass = FALSE; + FALLTHROUGH(); + case H3_TUNNEL_FAILED: + if(new_state == H3_TUNNEL_FAILED) + CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'failed'", + ts->stream_id); + ts->state = new_state; + /* If a proxy-authorization header was used for the proxy, then we should + make sure that it is not accidentally used for the document request + after we have connected. So let's free and clear it here. */ + curlx_safefree(data->req.hd_proxy_auth); + break; + } +} + +struct cf_ngtcp2_proxy_ctx { + struct cf_quic_ctx q; + struct ssl_peer peer; + struct curl_tls_ctx tls; +#ifdef OPENSSL_QUIC_API2 + ngtcp2_crypto_ossl_ctx *ossl_ctx; +#endif /* OPENSSL_QUIC_API2 */ + ngtcp2_path connected_path; + ngtcp2_conn *qconn; + ngtcp2_cid dcid; + ngtcp2_cid scid; + uint32_t version; + ngtcp2_settings settings; + ngtcp2_transport_params transport_params; + ngtcp2_ccerr last_error; + ngtcp2_crypto_conn_ref conn_ref; + struct cf_call_data call_data; + nghttp3_conn *h3conn; + nghttp3_settings h3settings; + struct curltime started_at; /* time the current attempt started */ + struct curltime handshake_at; /* time connect handshake finished */ + struct bufc_pool stream_bufcp; /* chunk pool for streams */ + struct dynbuf scratch; /* temp buffer for header construction */ + struct uint_hash streams; + /* hash `data->mid` to `h3_proxy_stream_ctx` */ + uint64_t used_bidi_streams; /* bidi streams we have opened */ + uint64_t max_bidi_streams; /* max bidi streams we can open */ + size_t earlydata_max; /* max amount of early data supported by + server on session reuse */ + size_t earlydata_skip; /* sending bytes to skip when earlydata + is accepted by peer */ + CURLcode tls_vrfy_result; /* result of TLS peer verification */ + int qlogfd; + BIT(initialized); + BIT(tls_handshake_complete); /* TLS handshake is done */ + BIT(use_earlydata); /* Using 0RTT data */ + BIT(earlydata_accepted); /* 0RTT was accepted by server */ + BIT(shutdown_started); /* queued shutdown packets */ +}; + +struct cf_h3_proxy_ctx +{ + struct cf_ngtcp2_proxy_ctx *ngtcp2_ctx; + struct cf_call_data call_data; /* fallback before backend ctx exists */ + struct bufq inbufq; /* network receive buffer */ + struct Curl_peer *dest; /* where to tunnel to */ + struct h3_tunnel_stream tunnel; /* our tunnel CONNECT stream */ + BIT(connected); + BIT(udp_tunnel); +}; + +/** + * All about the H3 internals of a stream + */ +struct h3_proxy_stream_ctx +{ + int64_t id; /* HTTP/3 stream identifier */ + struct bufq sendbuf; /* h3 request body */ + size_t sendbuf_len_in_flight; /* sendbuf amount "in flight" */ + uint64_t error3; /* HTTP/3 stream error code */ + curl_off_t upload_left; /* number of request bytes left to upload */ + curl_off_t tun_data_recvd; /* number of bytes received over tunnel */ + uint64_t rx_offset; /* current receive offset */ + uint64_t rx_offset_max; /* allowed receive offset */ + uint64_t window_size_max; /* max flow control window set for stream */ + int status_code; /* HTTP status code */ + CURLcode xfer_result; /* result from xfer_resp_write(_hd) */ + BIT(resp_hds_complete); /* we have a complete, final response */ + BIT(closed); /* TRUE on stream close */ + BIT(reset); /* TRUE on stream reset */ + BIT(send_closed); /* stream is local closed */ + BIT(quic_flow_blocked); /* stream is blocked by QUIC flow control */ +}; + +#define H3_PROXY_STREAM_CTX(ctx, data) \ + ((data) ? Curl_uint32_hash_get(&(ctx)->streams, (data)->mid) : NULL) + +#define H3_STREAM_ID(stream) ((stream)->id) + +static void h3_proxy_stream_ctx_free(struct h3_proxy_stream_ctx *stream) +{ + Curl_bufq_free(&stream->sendbuf); + curlx_free(stream); +} + +static void h3_proxy_stream_hash_free(unsigned int id, void *stream) +{ + (void)id; + DEBUGASSERT(stream); + h3_proxy_stream_ctx_free((struct h3_proxy_stream_ctx *)stream); +} + +static void cf_ngtcp2_proxy_ctx_init(struct cf_ngtcp2_proxy_ctx *ctx) +{ + DEBUGASSERT(!ctx->initialized); + ctx->q.sockfd = CURL_SOCKET_BAD; + ctx->qlogfd = -1; + ctx->version = NGTCP2_PROTO_VER_MAX; + Curl_bufcp_init(&ctx->stream_bufcp, PROXY_H3_STREAM_CHUNK_SIZE, + PROXY_H3_STREAM_POOL_SPARES); + curlx_dyn_init(&ctx->scratch, CURL_MAX_HTTP_HEADER); + Curl_uint32_hash_init(&ctx->streams, 63, h3_proxy_stream_hash_free); + ctx->initialized = TRUE; +} + +static void cf_ngtcp2_proxy_ctx_free(struct cf_ngtcp2_proxy_ctx *ctx) +{ + if(ctx && ctx->initialized) { + Curl_vquic_tls_cleanup(&ctx->tls); + vquic_ctx_free(&ctx->q); + Curl_bufcp_free(&ctx->stream_bufcp); + curlx_dyn_free(&ctx->scratch); + Curl_uint32_hash_destroy(&ctx->streams); + Curl_ssl_peer_cleanup(&ctx->peer); + } + curlx_free(ctx); +} + +static void cf_ngtcp2_proxy_ctx_close(struct cf_ngtcp2_proxy_ctx *ctx) +{ + struct cf_call_data save = ctx->call_data; + + if(!ctx->initialized) + return; + if(ctx->qlogfd != -1) { + curlx_close(ctx->qlogfd); + } + ctx->qlogfd = -1; + Curl_vquic_tls_cleanup(&ctx->tls); + Curl_ssl_peer_cleanup(&ctx->peer); + vquic_ctx_free(&ctx->q); + if(ctx->h3conn) { + nghttp3_conn_del(ctx->h3conn); + ctx->h3conn = NULL; + } + if(ctx->qconn) { + ngtcp2_conn_del(ctx->qconn); + ctx->qconn = NULL; + } +#ifdef OPENSSL_QUIC_API2 + if(ctx->ossl_ctx) { + ngtcp2_crypto_ossl_ctx_del(ctx->ossl_ctx); + ctx->ossl_ctx = NULL; + } +#endif /* OPENSSL_QUIC_API2 */ + ctx->call_data = save; +} + +static void cf_ngtcp2_proxy_setup_keep_alive(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + const ngtcp2_transport_params *rp; + /* Peer should have sent us its transport parameters. If it + * announces a positive `max_idle_timeout` it will close the + * connection when it does not hear from us for that time. + * + * Some servers use this as a keep-alive timer at a rather low + * value. We are doing HTTP/3 here and waiting for the response + * to a request may take a considerable amount of time. We need + * to prevent the peer's QUIC stack from closing in this case. + */ + if(!ctx->qconn) + return; + + rp = ngtcp2_conn_get_remote_transport_params(ctx->qconn); + if(!rp || !rp->max_idle_timeout) { + ngtcp2_conn_set_keep_alive_timeout(ctx->qconn, UINT64_MAX); + CURL_TRC_CF(data, cf, "no peer idle timeout, unset keep-alive"); + } + else if(!Curl_uint32_hash_count(&ctx->streams)) { + ngtcp2_conn_set_keep_alive_timeout(ctx->qconn, UINT64_MAX); + CURL_TRC_CF(data, cf, "no active streams, unset keep-alive"); + } + else { + ngtcp2_duration keep_ns; + keep_ns = (rp->max_idle_timeout > 1) ? (rp->max_idle_timeout / 2) : 1; + ngtcp2_conn_set_keep_alive_timeout(ctx->qconn, keep_ns); + CURL_TRC_CF(data, cf, "peer idle timeout is %" PRIu64 "ms, " + "set keep-alive to %" PRIu64 " ms.", + (uint64_t)(rp->max_idle_timeout / NGTCP2_MILLISECONDS), + (uint64_t)(keep_ns / NGTCP2_MILLISECONDS)); + } +} + +struct proxy_pkt_io_ctx { + struct Curl_cfilter *cf; + struct Curl_easy *data; + ngtcp2_tstamp ts; + ngtcp2_path_storage ps; +}; + +static void proxy_pktx_update_time(struct proxy_pkt_io_ctx *pktx, + struct Curl_cfilter *cf) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + const struct curltime *pnow = Curl_pgrs_now(pktx->data); + + vquic_ctx_update_time(&ctx->q, pnow); + pktx->ts = ((ngtcp2_tstamp)pnow->tv_sec * NGTCP2_SECONDS) + + ((ngtcp2_tstamp)pnow->tv_usec * NGTCP2_MICROSECONDS); +} + +static void proxy_pktx_init(struct proxy_pkt_io_ctx *pktx, + struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + const struct curltime *pnow = Curl_pgrs_now(data); + + pktx->cf = cf; + pktx->data = data; + ngtcp2_path_storage_zero(&pktx->ps); + vquic_ctx_set_time(&ctx->q, pnow); + pktx->ts = ((ngtcp2_tstamp)pnow->tv_sec * NGTCP2_SECONDS) + + ((ngtcp2_tstamp)pnow->tv_usec * NGTCP2_MICROSECONDS); +} + +static ngtcp2_conn *proxy_get_conn(ngtcp2_crypto_conn_ref *conn_ref) +{ + struct Curl_cfilter *cf = conn_ref->user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + return ctx->qconn; +} + +#ifdef DEBUG_NGTCP2 +static void proxy_quic_printf(void *user_data, const char *fmt, ...) +{ + va_list ap; + (void)user_data; + va_start(ap, fmt); + curl_mvfprintf(stderr, fmt, ap); + va_end(ap); + curl_mfprintf(stderr, "\n"); +} +#endif /* DEBUG_NGTCP2 */ + +static void proxy_qlog_callback(void *user_data, uint32_t flags, + const void *data, size_t datalen) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + (void)flags; + if(ctx->qlogfd != -1) { + ssize_t rc = write(ctx->qlogfd, data, datalen); + if(rc == -1) { + /* on write error, stop further write attempts */ + curlx_close(ctx->qlogfd); + ctx->qlogfd = -1; + } + } +} + +static void quic_settings_proxy(struct cf_ngtcp2_proxy_ctx *ctx, + struct Curl_easy *data, + struct proxy_pkt_io_ctx *pktx) +{ + ngtcp2_settings *s = &ctx->settings; + ngtcp2_transport_params *t = &ctx->transport_params; + + ngtcp2_settings_default(s); + ngtcp2_transport_params_default(t); +#ifdef DEBUG_NGTCP2 + s->log_printf = proxy_quic_printf; +#else + s->log_printf = NULL; +#endif /* DEBUG_NGTCP2 */ + + s->initial_ts = pktx->ts; + s->handshake_timeout = (data->set.connecttimeout > 0) ? + data->set.connecttimeout * NGTCP2_MILLISECONDS : + PROXY_QUIC_HANDSHAKE_TIMEOUT; + s->max_window = 100 * PROXY_H3_STREAM_WINDOW_SIZE; + s->max_stream_window = 10 * PROXY_H3_STREAM_WINDOW_SIZE; + s->no_pmtud = FALSE; +#ifdef NGTCP2_SETTINGS_V3 + /* try ten times the ngtcp2 defaults here for problems with Caddy */ + s->glitch_ratelim_burst = 1000 * 10; + s->glitch_ratelim_rate = 33 * 10; +#endif /* NGTCP2_SETTINGS_V3 */ + t->initial_max_data = 10 * PROXY_H3_STREAM_WINDOW_SIZE; + t->initial_max_stream_data_bidi_local = PROXY_H3_STREAM_WINDOW_SIZE; + t->initial_max_stream_data_bidi_remote = PROXY_H3_STREAM_WINDOW_SIZE; + t->initial_max_stream_data_uni = PROXY_H3_STREAM_WINDOW_SIZE; + t->initial_max_streams_bidi = PROXY_QUIC_MAX_STREAMS; + t->initial_max_streams_uni = PROXY_QUIC_MAX_STREAMS; + t->max_idle_timeout = 0; /* no idle timeout from our side */ + if(ctx->qlogfd != -1) { + s->qlog_write = proxy_qlog_callback; + } +} + +static void cf_ngtcp2_proxy_conn_close(struct Curl_cfilter *cf, + struct Curl_easy *data); + +static bool cf_ngtcp2_proxy_err_is_fatal(int code) +{ + return (NGTCP2_ERR_FATAL >= code) || + (NGTCP2_ERR_DROP_CONN == code) || + (NGTCP2_ERR_IDLE_CLOSE == code); +} + +static void cf_ngtcp2_proxy_err_set(struct Curl_cfilter *cf, + struct Curl_easy *data, int code) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + if(!ctx->last_error.error_code) { + if(NGTCP2_ERR_CRYPTO == code) { + ngtcp2_ccerr_set_tls_alert(&ctx->last_error, + ngtcp2_conn_get_tls_alert(ctx->qconn), + NULL, 0); + } + else { + ngtcp2_ccerr_set_liberr(&ctx->last_error, code, NULL, 0); + } + } + if(cf_ngtcp2_proxy_err_is_fatal(code)) + cf_ngtcp2_proxy_conn_close(cf, data); +} + +static bool cf_ngtcp2_proxy_h3_err_is_fatal(int code) +{ + return (NGHTTP3_ERR_FATAL >= code) || + (NGHTTP3_ERR_H3_CLOSED_CRITICAL_STREAM == code); +} + +static void cf_ngtcp2_proxy_h3_err_set(struct Curl_cfilter *cf, + struct Curl_easy *data, int code) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + if(!ctx->last_error.error_code) { + ngtcp2_ccerr_set_application_error(&ctx->last_error, + nghttp3_err_infer_quic_app_error_code(code), NULL, 0); + } + if(cf_ngtcp2_proxy_h3_err_is_fatal(code)) + cf_ngtcp2_proxy_conn_close(cf, data); +} + +/* How to access `call_data` from a cf_h3_proxy filter */ +#undef CF_CTX_CALL_DATA +static struct cf_call_data *cf_h3_proxy_call_data(struct Curl_cfilter *cf) +{ + struct cf_h3_proxy_ctx *ctx = cf ? cf->ctx : NULL; + static struct cf_call_data no_ctx; + + if(!ctx) + return &no_ctx; + if(ctx->ngtcp2_ctx) + return &ctx->ngtcp2_ctx->call_data; + return &ctx->call_data; +} + +#define CF_CTX_CALL_DATA(cf) (*cf_h3_proxy_call_data(cf)) + +static void cf_h3_proxy_ctx_clear(struct cf_h3_proxy_ctx *ctx) +{ + Curl_bufq_free(&ctx->inbufq); + Curl_peer_unlink(&ctx->dest); + h3_tunnel_stream_clear(&ctx->tunnel); + memset(ctx, 0, sizeof(*ctx)); +} + +static void cf_h3_proxy_ctx_free(struct cf_h3_proxy_ctx *ctx) +{ + if(ctx) { + cf_h3_proxy_ctx_clear(ctx); + curlx_free(ctx); + } +} + +static CURLcode h3_proxy_data_setup(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct h3_proxy_stream_ctx *stream = NULL; + + if(!data) + return CURLE_FAILED_INIT; + + if(!ctx) + return CURLE_FAILED_INIT; + + stream = H3_PROXY_STREAM_CTX(ctx, data); + if(stream) + return CURLE_OK; + + stream = curlx_calloc(1, sizeof(*stream)); + if(!stream) + return CURLE_OUT_OF_MEMORY; + + stream->id = -1; + stream->rx_offset = 0; + stream->rx_offset_max = PROXY_H3_STREAM_WINDOW_SIZE; + /* on send, we control how much we put into the buffer */ + Curl_bufq_initp(&stream->sendbuf, &ctx->stream_bufcp, + PROXY_H3_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE); + stream->sendbuf_len_in_flight = 0; + stream->window_size_max = PROXY_H3_STREAM_WINDOW_SIZE; + + if(!Curl_uint32_hash_set(&ctx->streams, data->mid, stream)) { + h3_proxy_stream_ctx_free(stream); + return CURLE_OUT_OF_MEMORY; + } + + if(Curl_uint32_hash_count(&ctx->streams) == 1) + cf_ngtcp2_proxy_setup_keep_alive(cf, data); + + return CURLE_OK; +} + +static int cb_h3_proxy_acked_req_body(nghttp3_conn *conn, int64_t stream_id, + uint64_t datalen, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = stream_user_data; + struct h3_proxy_stream_ctx *stream; + size_t skiplen; + + if(!ctx) + return 0; + stream = H3_PROXY_STREAM_CTX(ctx, data); + if(!stream) + return 0; + /* The server acknowledged `datalen` of bytes from our request body. + * This is a delta. We have kept this data in `sendbuf` for + * re-transmissions and can free it now. */ + if(datalen >= (uint64_t)stream->sendbuf_len_in_flight) + skiplen = stream->sendbuf_len_in_flight; + else + skiplen = (size_t)datalen; + Curl_bufq_skip(&stream->sendbuf, skiplen); + stream->sendbuf_len_in_flight -= skiplen; + + /* Resume upload processing if we have more data to send */ + if(stream->sendbuf_len_in_flight < Curl_bufq_len(&stream->sendbuf)) { + int rv = nghttp3_conn_resume_stream(conn, stream_id); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + return 0; +} + +static int cb_h3_proxy_stream_close(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = stream_user_data; + struct h3_proxy_stream_ctx *stream; + bool tunnel_stream = FALSE; + (void)conn; + + if(!ctx) + return 0; + stream = H3_PROXY_STREAM_CTX(ctx, data); + tunnel_stream = (stream_id == proxy_ctx->tunnel.stream_id); + /* we might be called by nghttp3 after we already cleaned up */ + if(!stream) { + if(tunnel_stream) { + proxy_ctx->tunnel.stream = NULL; + proxy_ctx->tunnel.closed = TRUE; + } + return 0; + } + + stream->closed = TRUE; + stream->error3 = app_error_code; + if(stream->error3 != NGHTTP3_H3_NO_ERROR) { + stream->reset = TRUE; + stream->send_closed = TRUE; + CURL_TRC_CF(data, cf, "[%" PRId64 "] RESET: error %" PRIu64, + H3_STREAM_ID(stream), stream->error3); + } + else { + CURL_TRC_CF(data, cf, "[%" PRId64 "] CLOSED", H3_STREAM_ID(stream)); + } + if(tunnel_stream) { + proxy_ctx->tunnel.stream = NULL; + proxy_ctx->tunnel.closed = TRUE; + } + Curl_multi_mark_dirty(data); + return 0; +} + +static void cf_h3_proxy_upd_rx_win(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h3_proxy_stream_ctx *stream) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + uint64_t cur_win, wanted_win = PROXY_H3_STREAM_WINDOW_SIZE_MAX; + + /* how much does rate limiting allow us to acknowledge? */ + if(Curl_rlimit_active(&data->progress.dl.rlimit)) { + int64_t avail; + + /* start rate limit updates only after first bytes arrived */ + if(!stream->rx_offset) + return; + + avail = Curl_rlimit_avail(&data->progress.dl.rlimit, + Curl_pgrs_now(data)); + if(avail <= 0) { + /* nothing available, do not extend the rx offset */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] dl rate limit exhausted (%" PRId64 + " tokens)", stream->id, avail); + return; + } + wanted_win = CURLMIN((uint64_t)avail, PROXY_H3_STREAM_WINDOW_SIZE_MAX); + } + + if(stream->rx_offset_max < stream->rx_offset) { + DEBUGASSERT(0); + return; + } + cur_win = stream->rx_offset_max - stream->rx_offset; + if(cur_win < wanted_win) { + /* We have exhausted the credit we gave the QUIC peer for DATA. + * We extend it with the amount we can give (rate limit) */ + uint64_t ext = wanted_win - cur_win; + + ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream->id, ext); + ngtcp2_conn_extend_max_offset(ctx->qconn, ext); + stream->rx_offset_max += ext; + if(stream->rx_offset_max > stream->window_size_max) { + stream->window_size_max = stream->rx_offset_max; + CURL_TRC_CF(data, cf, "[%" PRId64 "] max window now -> %" PRIu64, + stream->id, stream->window_size_max); + } + CURL_TRC_CF(data, cf, "[%" PRId64 "] rx_offset_max -> %" PRIu64 + " (ext %" PRIu64 ", win %" PRIu64 ")", + stream->id, stream->rx_offset_max, ext, wanted_win); + } +} + +static int cb_h3_proxy_recv_data(nghttp3_conn *conn, int64_t stream3_id, + const uint8_t *buf, size_t buflen, + void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = stream_user_data; + struct h3_proxy_stream_ctx *stream; + size_t nwritten; + CURLcode result = CURLE_OK; + (void)conn; + (void)stream3_id; + + if(!ctx) + return NGHTTP3_ERR_CALLBACK_FAILURE; + stream = H3_PROXY_STREAM_CTX(ctx, data); + if(!stream) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + stream->tun_data_recvd += (curl_off_t)buflen; + CURL_TRC_CF(data, cf, "[cb_h3_proxy_recv_data] " + "[%" PRIu64 "] DATA len=%zu, total=%zd", + H3_STREAM_ID(stream), buflen, stream->tun_data_recvd); + + result = Curl_bufq_write(&proxy_ctx->inbufq, buf, buflen, &nwritten); + if(result || (nwritten < buflen)) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + /* DATA has been moved into our local recv buffer. Update stream offsets + * and give QUIC read credit back so long transfers over proxy tunnels + * do not stall on stream/connection flow-control limits. */ + stream->rx_offset += buflen; + if(stream->rx_offset_max < stream->rx_offset) + stream->rx_offset_max = stream->rx_offset; + + CURL_TRC_CF(data, cf, "[%" PRId64 "] DATA len=%zu, rx win=%" PRIu64, + stream->id, buflen, stream->rx_offset_max - stream->rx_offset); + cf_h3_proxy_upd_rx_win(cf, data, stream); + + Curl_multi_mark_dirty(data); + return 0; +} + +static int cb_h3_proxy_deferred_consume(nghttp3_conn *conn, int64_t stream_id, + size_t consumed, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + (void)conn; + (void)stream_user_data; + + if(!ctx) + return 0; + + /* nghttp3 has consumed bytes on the QUIC stream and we need to + * tell the QUIC connection to increase its flow control */ + ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream_id, consumed); + ngtcp2_conn_extend_max_offset(ctx->qconn, consumed); + + return 0; +} + +static int cb_h3_proxy_recv_header(nghttp3_conn *conn, int64_t sid, + int32_t token, nghttp3_rcbuf *name, + nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + int64_t stream_id = sid; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + nghttp3_vec h3name = nghttp3_rcbuf_get_buf(name); + nghttp3_vec h3val = nghttp3_rcbuf_get_buf(value); + struct Curl_easy *data = stream_user_data; + struct h3_proxy_stream_ctx *stream; + CURLcode result = CURLE_OK; + int http_status; + struct http_resp *resp; + (void)conn; + (void)stream_id; + (void)token; + (void)flags; + + /* stream_user_data might be NULL for control streams */ + if(!data) { + /* Silently ignore headers on streams without user data (control, etc) */ + return 0; + } + + if(!ctx) + return 0; + stream = H3_PROXY_STREAM_CTX(ctx, data); + if(!stream) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] recv_header: stream lookup " + "failed for data=%p mid=%u", + stream_id, (void *)data, data ? data->mid : 0); + } + + /* we might have cleaned up this transfer already */ + if(!stream) + return 0; + + if(proxy_ctx->tunnel.has_final_response) { + /* we do not do anything with trailers for tunnel streams */ + return 0; + } + + if(token == NGHTTP3_QPACK_TOKEN__STATUS) { + result = Curl_http_decode_status(&stream->status_code, + (const char *)h3val.base, h3val.len); + if(result) + return NGHTTP3_ERR_CALLBACK_FAILURE; + http_status = stream->status_code; + result = Curl_http_resp_make(&resp, http_status, NULL); + if(result) + return NGHTTP3_ERR_CALLBACK_FAILURE; + if(proxy_ctx->tunnel.resp) + Curl_http_resp_free(proxy_ctx->tunnel.resp); + proxy_ctx->tunnel.resp = resp; + } + else { + /* store as an HTTP1-style header */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] header: %.*s: %.*s", + stream_id, (int)h3name.len, h3name.base, + (int)h3val.len, h3val.base); + result = Curl_dynhds_add(&proxy_ctx->tunnel.resp->headers, + (const char *)h3name.base, h3name.len, + (const char *)h3val.base, h3val.len); + if(result) { + return -1; + } + } + return 0; +} + +static int cb_h3_proxy_end_headers(nghttp3_conn *conn, int64_t sid, + int fin, void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = stream_user_data; + int64_t stream_id = sid; + struct h3_proxy_stream_ctx *stream; + (void)conn; + (void)stream_id; + (void)fin; + + /* stream_user_data might be NULL for control streams */ + if(!data) { + /* Silently ignore for streams without user data */ + return 0; + } + + if(!ctx) + return 0; + stream = H3_PROXY_STREAM_CTX(ctx, data); + if(!stream) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] end_headers: stream lookup " + "failed for data=%p mid=%u", + stream_id, (void *)data, data ? data->mid : 0); + } + + if(!stream) + return 0; + + CURL_TRC_CF(data, cf, "[%" PRId64 "] end_headers, status=%d", + stream_id, stream->status_code); + + if(!proxy_ctx->tunnel.has_final_response) { + if(stream->status_code / 100 != 1) { + proxy_ctx->tunnel.has_final_response = TRUE; + } + } + + if(stream->status_code / 100 != 1) { + stream->resp_hds_complete = TRUE; + } + + Curl_multi_mark_dirty(data); + return 0; +} + +static int cb_h3_proxy_stop_sending(nghttp3_conn *conn, int64_t sid, + uint64_t app_error_code, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + (void)conn; + + (void)stream_user_data; + + if(ctx) { + int rv = ngtcp2_conn_shutdown_stream_read(ctx->qconn, 0, sid, + app_error_code); + + if(rv && rv != NGTCP2_ERR_STREAM_NOT_FOUND) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + + return 0; +} + +static int cb_h3_proxy_reset_stream(nghttp3_conn *conn, int64_t sid, + uint64_t app_error_code, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = stream_user_data; + int64_t stream_id = sid; + int rv; + (void)conn; + + if(!ctx) + return 0; + + rv = ngtcp2_conn_shutdown_stream_write(ctx->qconn, 0, stream_id, + app_error_code); + CURL_TRC_CF(data, cf, "[%" PRId64 "] reset -> %d", stream_id, rv); + if(stream_id == proxy_ctx->tunnel.stream_id) { + proxy_ctx->tunnel.stream = NULL; + proxy_ctx->tunnel.closed = TRUE; + } + if(rv && rv != NGTCP2_ERR_STREAM_NOT_FOUND) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static nghttp3_ssize +cb_h3_read_data_for_tunnel_stream(nghttp3_conn *conn, int64_t stream_id, + nghttp3_vec *vec, size_t veccnt, + uint32_t *pflags, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = stream_user_data; + struct h3_proxy_stream_ctx *stream; + size_t nwritten = 0; + size_t nvecs = 0; + const unsigned char *buf_base; + (void)conn; + (void)stream_id; + (void)veccnt; + + if(!ctx) + return NGHTTP3_ERR_CALLBACK_FAILURE; + stream = H3_PROXY_STREAM_CTX(ctx, data); + + if(!stream) + return NGHTTP3_ERR_CALLBACK_FAILURE; + /* nghttp3 keeps references to the sendbuf data until it is ACKed + * by the server (see `cb_h3_proxy_acked_req_body()` for updates). + * `sendbuf_len_in_flight` is the amount of bytes in `sendbuf` + * that we have already passed to nghttp3, but which have not been + * ACKed yet. + * Any amount beyond `sendbuf_len_in_flight` we need still to pass + * to nghttp3. Do that now, if we can. */ + if(stream->sendbuf_len_in_flight < Curl_bufq_len(&stream->sendbuf)) { + nvecs = 0; + while(nvecs < veccnt) { + if(!Curl_bufq_peek_at(&stream->sendbuf, + stream->sendbuf_len_in_flight, + &buf_base, + &vec[nvecs].len)) + break; + vec[nvecs].base = (uint8_t *)(uintptr_t)buf_base; + stream->sendbuf_len_in_flight += vec[nvecs].len; + nwritten += vec[nvecs].len; + ++nvecs; + } + DEBUGASSERT(nvecs > 0); /* we SHOULD have been be able to peek */ + } + + if(nwritten > 0 && + stream->upload_left != -1 && + (H3_STREAM_ID(stream) != proxy_ctx->tunnel.stream_id)) + stream->upload_left -= nwritten; + + /* When we stopped sending and everything in `sendbuf` is "in flight", + * we are at the end of the request body. */ + /* We should NOT set send_closed = TRUE for tunnel stream */ + if(stream->upload_left == 0 && + (H3_STREAM_ID(stream) != proxy_ctx->tunnel.stream_id)) { + *pflags = NGHTTP3_DATA_FLAG_EOF; + stream->send_closed = TRUE; + } + + else if(!nwritten) { + /* Not EOF, and nothing to give, we signal WOULDBLOCK. */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] read req body -> AGAIN", + H3_STREAM_ID(stream)); + return NGHTTP3_ERR_WOULDBLOCK; + } + + CURL_TRC_CF(data, cf, "[%" PRId64 "] read req body -> " + "%d vecs%s with %zd (buffered=%zu, left=%" FMT_OFF_T ")", + H3_STREAM_ID(stream), (int)nvecs, + *pflags == NGHTTP3_DATA_FLAG_EOF ? " EOF" : "", + nwritten, Curl_bufq_len(&stream->sendbuf), + stream->upload_left); + return (nghttp3_ssize)nvecs; +} + +static nghttp3_callbacks ngh3_proxy_callbacks = { + cb_h3_proxy_acked_req_body, /* acked_stream_data */ + cb_h3_proxy_stream_close, + cb_h3_proxy_recv_data, + cb_h3_proxy_deferred_consume, + NULL, /* begin_headers */ + cb_h3_proxy_recv_header, + cb_h3_proxy_end_headers, + NULL, /* begin_trailers */ + cb_h3_proxy_recv_header, + NULL, /* end_trailers */ + cb_h3_proxy_stop_sending, + NULL, /* end_stream */ + cb_h3_proxy_reset_stream, + NULL, /* shutdown */ + NULL, /* recv_settings (deprecated) */ +#ifdef NGHTTP3_CALLBACKS_V2 /* nghttp3 v1.11.0+ */ + NULL, /* recv_origin */ + NULL, /* end_origin */ + NULL, /* rand */ +#endif /* NGHTTP3_CALLBACKS_V2 */ +#ifdef NGHTTP3_CALLBACKS_V3 /* nghttp3 v1.14.0+ */ + NULL, /* recv_settings2 */ +#endif /* NGHTTP3_CALLBACKS_V3 */ +}; + +#if NGTCP2_VERSION_NUM < 0x011100 +struct cf_ngtcp2_proxy_sfind_ctx { + int64_t stream_id; + struct h3_proxy_stream_ctx *stream; + uint32_t mid; +}; + +static bool cf_ngtcp2_proxy_sfind(uint32_t mid, void *value, + void *user_data) +{ + struct cf_ngtcp2_proxy_sfind_ctx *fctx = user_data; + struct h3_proxy_stream_ctx *stream = value; + + if(fctx->stream_id == H3_STREAM_ID(stream)) { + fctx->mid = mid; + fctx->stream = stream; + return FALSE; + } + return TRUE; /* continue */ +} + +static struct h3_proxy_stream_ctx * +cf_ngtcp2_proxy_get_stream(struct cf_ngtcp2_proxy_ctx *ctx, int64_t stream_id) +{ + struct cf_ngtcp2_proxy_sfind_ctx fctx; + fctx.stream_id = stream_id; + fctx.stream = NULL; + Curl_uint32_hash_visit(&ctx->streams, cf_ngtcp2_proxy_sfind, &fctx); + return fctx.stream; +} +#else +static struct h3_proxy_stream_ctx * +cf_ngtcp2_proxy_get_stream(struct cf_ngtcp2_proxy_ctx *ctx, int64_t stream_id) +{ + struct Curl_easy *data = + ngtcp2_conn_get_stream_user_data(ctx->qconn, stream_id); + + if(!data) { + return NULL; + } + return H3_PROXY_STREAM_CTX(ctx, data); +} +#endif /* NGTCP2_VERSION_NUM < 0x011100 */ + +static CURLcode cf_ngtcp2_h3conn_init(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + int64_t ctrl_stream_id, qpack_enc_stream_id, qpack_dec_stream_id; + int rc; + + if(ngtcp2_conn_get_streams_uni_left(ctx->qconn) < 3) { + failf(data, "QUIC connection lacks 3 uni streams to run HTTP/3"); + return CURLE_QUIC_CONNECT_ERROR; + } + + nghttp3_settings_default(&ctx->h3settings); + + rc = nghttp3_conn_client_new(&ctx->h3conn, + &ngh3_proxy_callbacks, + &ctx->h3settings, + Curl_nghttp3_mem(), + cf); + if(rc) { + failf(data, "error creating nghttp3 connection instance"); + return CURLE_OUT_OF_MEMORY; + } + + rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &ctrl_stream_id, NULL); + if(rc) { + failf(data, "error creating HTTP/3 control stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; + } + + rc = nghttp3_conn_bind_control_stream(ctx->h3conn, ctrl_stream_id); + if(rc) { + failf(data, "error binding HTTP/3 control stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; + } + + rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_enc_stream_id, NULL); + if(rc) { + failf(data, "error creating HTTP/3 qpack encoding stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; + } + + rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_dec_stream_id, NULL); + if(rc) { + failf(data, "error creating HTTP/3 qpack decoding stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; + } + + rc = nghttp3_conn_bind_qpack_streams(ctx->h3conn, qpack_enc_stream_id, + qpack_dec_stream_id); + if(rc) { + failf(data, "error binding HTTP/3 qpack streams: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; + } + + CURL_TRC_CF(data, cf, "HTTP/3 connection initialized"); + return CURLE_OK; +} + +static int cb_ngtcp2_proxy_handshake_completed(ngtcp2_conn *tconn, + void *user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data; + + (void)tconn; + DEBUGASSERT(ctx); + data = CF_DATA_CURRENT(cf); + DEBUGASSERT(data); + if(!ctx || !data) + return NGHTTP3_ERR_CALLBACK_FAILURE; + + ctx->handshake_at = *Curl_pgrs_now(data); + ctx->tls_handshake_complete = TRUE; + Curl_vquic_report_handshake(&ctx->tls, cf, data); + + ctx->tls_vrfy_result = Curl_vquic_tls_verify_peer(&ctx->tls, cf, + data, &ctx->peer); +#ifdef CURLVERBOSE + if(Curl_trc_is_verbose(data)) { + const ngtcp2_transport_params *rp; + rp = ngtcp2_conn_get_remote_transport_params(ctx->qconn); + CURL_TRC_CF(data, cf, "handshake complete after %" FMT_TIMEDIFF_T + "ms, remote transport[max_udp_payload=%" PRIu64 + ", initial_max_data=%" PRIu64 + "]", + curlx_ptimediff_ms(&ctx->handshake_at, &ctx->started_at), + rp->max_udp_payload_size, rp->initial_max_data); + } +#endif + + /* In case of earlydata, where we simulate being connected, update + * the handshake time when we really did connect */ + if(ctx->use_earlydata) + Curl_pgrsTimeWas(data, TIMER_APPCONNECT, ctx->handshake_at); + if(ctx->use_earlydata) { +#if defined(USE_OPENSSL) && defined(HAVE_OPENSSL_EARLYDATA) + ctx->earlydata_accepted = + (SSL_get_early_data_status(ctx->tls.ossl.ssl) != + SSL_EARLY_DATA_REJECTED); +#endif +#ifdef USE_GNUTLS + int flags = gnutls_session_get_flags(ctx->tls.gtls.session); + ctx->earlydata_accepted = !!(flags & GNUTLS_SFLAGS_EARLY_DATA); +#endif /* USE_GNUTLS */ +#ifdef USE_WOLFSSL +#ifdef WOLFSSL_EARLY_DATA + ctx->earlydata_accepted = + (wolfSSL_get_early_data_status(ctx->tls.wssl.ssl) != + WOLFSSL_EARLY_DATA_REJECTED); +#else + DEBUGASSERT(0); /* should not come here if ED is disabled. */ + ctx->earlydata_accepted = FALSE; +#endif /* WOLFSSL_EARLY_DATA */ +#endif /* USE_WOLFSSL */ + CURL_TRC_CF(data, cf, "server did%s accept %zu bytes of early data", + ctx->earlydata_accepted ? "" : " not", ctx->earlydata_skip); + Curl_pgrsEarlyData(data, ctx->earlydata_accepted ? + (curl_off_t)ctx->earlydata_skip : + -(curl_off_t)ctx->earlydata_skip); + } + + /* Initialize HTTP/3 connection after successful handshake */ + if(!ctx->h3conn) { + CURLcode result = cf_ngtcp2_h3conn_init(cf, data); + if(result) { + CURL_TRC_CF(data, cf, "HTTP/3 initialization failed: %d", result); + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + + return 0; +} + +static int cb_ngtcp2_recv_stream_data(ngtcp2_conn *tconn, uint32_t flags, + int64_t sid, uint64_t offset, + const uint8_t *buf, size_t buflen, + void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + int64_t stream_id = (int64_t)sid; + nghttp3_ssize nconsumed; + int fin = (flags & NGTCP2_STREAM_DATA_FLAG_FIN) ? 1 : 0; + struct Curl_easy *data = stream_user_data; + (void)offset; + (void)data; + + nconsumed = + nghttp3_conn_read_stream(ctx->h3conn, stream_id, buf, buflen, fin); + if(!data) + data = CF_DATA_CURRENT(cf); + if(data) + CURL_TRC_CF(data, cf, "[%" PRId64 "] read_stream(len=%zu) -> %zd", + stream_id, buflen, nconsumed); + if(nconsumed < 0) { + struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data); + if(data && stream) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] error on known stream, " + "reset=%d, closed=%d", + stream_id, stream->reset, stream->closed); + } + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + /* number of bytes inside buflen which consists of framing overhead + * including QPACK HEADERS. In other words, it does not consume payload of + * DATA frame. */ + if(nconsumed) { + ngtcp2_conn_extend_max_stream_offset(tconn, stream_id, + (uint64_t)nconsumed); + ngtcp2_conn_extend_max_offset(tconn, (uint64_t)nconsumed); + } + + return 0; +} + +static int cb_ngtcp2_acked_stream_data_offset(ngtcp2_conn *tconn, + int64_t stream_id, + uint64_t offset, + uint64_t datalen, + void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + int rv; + (void)stream_id; + (void)tconn; + (void)offset; + (void)datalen; + (void)stream_user_data; + + rv = nghttp3_conn_add_ack_offset(ctx->h3conn, stream_id, datalen); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +static int cb_ngtcp2_stream_close(ngtcp2_conn *tconn, uint32_t flags, + int64_t sid, uint64_t app_error_code, + void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = stream_user_data; + int64_t stream_id = (int64_t)sid; + int rv; + + (void)tconn; + /* stream is closed... */ + if(!data) + data = CF_DATA_CURRENT(cf); + if(!data) + return NGTCP2_ERR_CALLBACK_FAILURE; + + if(!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + + rv = nghttp3_conn_close_stream(ctx->h3conn, stream_id, app_error_code); + CURL_TRC_CF(data, cf, "[%" PRId64 "] quic close(app_error=%" + PRIu64 ") -> %d", stream_id, (uint64_t)app_error_code, + rv); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + cf_ngtcp2_proxy_h3_err_set(cf, data, rv); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +static int cb_ngtcp2_extend_max_local_streams_bidi(ngtcp2_conn *tconn, + uint64_t max_streams, + void *user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = CF_DATA_CURRENT(cf); + + (void)tconn; + ctx->max_bidi_streams = max_streams; + if(data) + CURL_TRC_CF(data, cf, "max bidi streams now %" PRIu64 + ", used %" PRIu64, (uint64_t)ctx->max_bidi_streams, + (uint64_t)ctx->used_bidi_streams); + return 0; +} + +static void cb_ngtcp2_rand(uint8_t *dest, size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) +{ + CURLcode result; + (void)rand_ctx; + + result = Curl_rand(NULL, dest, destlen); + if(result) { + /* cb_rand is only used for non-cryptographic context. If Curl_rand + failed, just fill 0 and call it *random*. */ + memset(dest, 0, destlen); + } +} + +/* for ngtcp2 data, cidlen); + if(result) + return NGTCP2_ERR_CALLBACK_FAILURE; + cid->datalen = cidlen; + + result = Curl_rand(NULL, token, NGTCP2_STATELESS_RESET_TOKENLEN); + if(result) + return NGTCP2_ERR_CALLBACK_FAILURE; + + return 0; +} + +#ifdef NGTCP2_CALLBACKS_V3 /* ngtcp2 v1.22.0+ */ +static int cb_ngtcp2_get_new_connection_id2(ngtcp2_conn *tconn, + ngtcp2_cid *cid, struct ngtcp2_stateless_reset_token *token, + size_t cidlen, void *user_data) +{ + CURLcode result; + (void)tconn; + (void)user_data; + + result = Curl_rand(NULL, cid->data, cidlen); + if(result) + return NGTCP2_ERR_CALLBACK_FAILURE; + cid->datalen = cidlen; + + result = Curl_rand(NULL, token->data, sizeof(token->data)); + if(result) + return NGTCP2_ERR_CALLBACK_FAILURE; + + return 0; +} +#endif + +static int cb_ngtcp2_stream_reset(ngtcp2_conn *tconn, int64_t sid, + uint64_t final_size, uint64_t app_error_code, + void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + int64_t stream_id = (int64_t)sid; + struct Curl_easy *data = stream_user_data; + int rv; + (void)tconn; + (void)final_size; + (void)app_error_code; + (void)data; + + rv = nghttp3_conn_shutdown_stream_read(ctx->h3conn, stream_id); + CURL_TRC_CF(data, cf, "[%" PRId64 "] reset -> %d", stream_id, rv); + if(stream_id == proxy_ctx->tunnel.stream_id) { + proxy_ctx->tunnel.stream = NULL; + proxy_ctx->tunnel.closed = TRUE; + } + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +static int cb_ngtcp2_extend_max_stream_data(ngtcp2_conn *tconn, + int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *s_data = stream_user_data; + struct h3_proxy_stream_ctx *stream = NULL; + int rv; + (void)tconn; + (void)max_data; + + rv = nghttp3_conn_unblock_stream(ctx->h3conn, stream_id); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + stream = H3_PROXY_STREAM_CTX(ctx, s_data); + if(stream && stream->quic_flow_blocked) { + CURL_TRC_CF(s_data, cf, "[%" PRId64 "] unblock quic flow", + (int64_t)stream_id); + stream->quic_flow_blocked = FALSE; + Curl_multi_mark_dirty(s_data); + } + return 0; +} + +static int cb_ngtcp2_stream_stop_sending(ngtcp2_conn *tconn, int64_t stream_id, + uint64_t app_error_code, + void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + int rv; + (void)tconn; + (void)app_error_code; + (void)stream_user_data; + + rv = nghttp3_conn_shutdown_stream_read(ctx->h3conn, stream_id); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +static int cb_ngtcp2_recv_rx_key(ngtcp2_conn *tconn, + ngtcp2_encryption_level level, + void *user_data) +{ + struct Curl_cfilter *cf = user_data; + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct Curl_easy *data = CF_DATA_CURRENT(cf); + (void)tconn; + + if(level != NGTCP2_ENCRYPTION_LEVEL_1RTT) + return 0; + + DEBUGASSERT(ctx); + DEBUGASSERT(data); + if(ctx && data && !ctx->h3conn) { + if(cf_ngtcp2_h3conn_init(cf, data)) + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +#if defined(_MSC_VER) && defined(_DLL) +#pragma warning(push) +#pragma warning(disable:4232) /* MSVC extension, dllimport identity */ +#endif + +static ngtcp2_callbacks ngtcp2_proxy_callbacks = { + ngtcp2_crypto_client_initial_cb, + NULL, /* recv_client_initial */ + ngtcp2_crypto_recv_crypto_data_cb, + cb_ngtcp2_proxy_handshake_completed, + NULL, /* recv_version_negotiation */ + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + cb_ngtcp2_recv_stream_data, + cb_ngtcp2_acked_stream_data_offset, + NULL, /* stream_open */ + cb_ngtcp2_stream_close, + NULL, /* recv_stateless_reset */ + ngtcp2_crypto_recv_retry_cb, + cb_ngtcp2_extend_max_local_streams_bidi, + NULL, /* extend_max_local_streams_uni */ + cb_ngtcp2_rand, + cb_ngtcp2_get_new_connection_id, /* for ngtcp2 cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + ngtcp2_pkt_info pi; + ngtcp2_path path; + size_t offset, pktlen; + int rv; + + if(ecn) + CURL_TRC_CF(pktx->data, pktx->cf, "vquic_recv(len=%zu, gso=%zu, ecn=%x)", + buflen, gso_size, ecn); + ngtcp2_addr_init(&path.local, (struct sockaddr *)&ctx->q.local_addr, + (socklen_t)ctx->q.local_addrlen); + ngtcp2_addr_init(&path.remote, (struct sockaddr *)remote_addr, + remote_addrlen); + pi.ecn = (uint8_t)ecn; + + for(offset = 0; offset < buflen; offset += gso_size) { + pktlen = ((offset + gso_size) <= buflen) ? gso_size : (buflen - offset); + rv = ngtcp2_conn_read_pkt(ctx->qconn, &path, &pi, + buf + offset, pktlen, pktx->ts); + if(rv) { + CURL_TRC_CF(pktx->data, pktx->cf, "ingress, read_pkt -> %s (%d)", + ngtcp2_strerror(rv), rv); + cf_ngtcp2_proxy_err_set(pktx->cf, pktx->data, rv); + + if(rv == NGTCP2_ERR_CRYPTO) + /* this is a "TLS problem", but a failed certificate verification + is a common reason for this */ + return CURLE_PEER_FAILED_VERIFICATION; + return CURLE_RECV_ERROR; + } + } + return CURLE_OK; +} + +static CURLcode proxy_h3_progress_ingress_ngtcp2(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct proxy_pkt_io_ctx *pktx) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct proxy_pkt_io_ctx local_pktx; + CURLcode result = CURLE_OK; + + if(!ctx) + return CURLE_RECV_ERROR; + if(!data || !data->multi) + return CURLE_RECV_ERROR; + + if(!pktx) { + proxy_pktx_init(&local_pktx, cf, data); + pktx = &local_pktx; + } + else { + proxy_pktx_update_time(pktx, cf); + ngtcp2_path_storage_zero(&pktx->ps); + } + + result = Curl_vquic_tls_before_recv(&ctx->tls, cf, data); + if(result) + return result; + + if(ctx->q.sockfd == CURL_SOCKET_BAD) + return CURLE_RECV_ERROR; + + return vquic_recv_packets(cf, data, &ctx->q, 1000, + cf_ngtcp2_recv_pkts_proxy, pktx); +} + +/** + * Read a network packet to send from ngtcp2 into `buf`. + * Return number of bytes written or -1 with *err set. + */ +static CURLcode proxy_read_pkt_to_send(void *userp, + unsigned char *buf, size_t buflen, + size_t *pnread) +{ + struct proxy_pkt_io_ctx *x = userp; + struct cf_h3_proxy_ctx *proxy_ctx = x->cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + nghttp3_vec vec[16]; + nghttp3_ssize veccnt; + ngtcp2_ssize ndatalen; + uint32_t flags; + int64_t stream_id; + int fin; + ssize_t n; + + *pnread = 0; + veccnt = 0; + stream_id = -1; + fin = 0; + + /* ngtcp2 may want to put several frames from different streams into + * this packet. `NGTCP2_WRITE_STREAM_FLAG_MORE` tells it to do so. + * When `NGTCP2_ERR_WRITE_MORE` is returned, we *need* to make + * another iteration. + * When ngtcp2 is happy (because it has no other frame that would fit + * or it has nothing more to send), it returns the total length + * of the assembled packet. This may be 0 if there was nothing to send. */ + for(;;) { + + if(ctx->h3conn && ngtcp2_conn_get_max_data_left(ctx->qconn)) { + veccnt = nghttp3_conn_writev_stream(ctx->h3conn, &stream_id, &fin, vec, + CURL_ARRAYSIZE(vec)); + if(veccnt < 0) { + failf(x->data, "nghttp3_conn_writev_stream returned error: %s", + nghttp3_strerror((int)veccnt)); + cf_ngtcp2_proxy_h3_err_set(x->cf, x->data, (int)veccnt); + return CURLE_SEND_ERROR; + } + } + + flags = NGTCP2_WRITE_STREAM_FLAG_MORE | + (fin ? NGTCP2_WRITE_STREAM_FLAG_FIN : 0); + n = ngtcp2_conn_writev_stream(ctx->qconn, &x->ps.path, + NULL, buf, buflen, + &ndatalen, flags, stream_id, + (const ngtcp2_vec *)vec, veccnt, x->ts); + if(n == 0) { + /* nothing to send */ + return CURLE_AGAIN; + } + else if(n < 0) { + switch(n) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: { + struct h3_proxy_stream_ctx *stream = NULL; + DEBUGASSERT(ndatalen == -1); + nghttp3_conn_block_stream(ctx->h3conn, stream_id); + CURL_TRC_CF(x->data, x->cf, "[%" PRId64 "] block quic flow", + (int64_t)stream_id); + stream = cf_ngtcp2_proxy_get_stream(ctx, stream_id); + if(stream) /* it might be not one of our h3 streams? */ + stream->quic_flow_blocked = TRUE; + n = 0; + break; + } + case NGTCP2_ERR_STREAM_SHUT_WR: + DEBUGASSERT(ndatalen == -1); + nghttp3_conn_shutdown_stream_write(ctx->h3conn, stream_id); + n = 0; + break; + case NGTCP2_ERR_WRITE_MORE: + /* ngtcp2 wants to send more. update the flow of the stream whose data + * is in the buffer and continue */ + DEBUGASSERT(ndatalen >= 0); + n = 0; + break; + default: + DEBUGASSERT(ndatalen == -1); + failf(x->data, "ngtcp2_conn_writev_stream returned error: %s", + ngtcp2_strerror((int)n)); + cf_ngtcp2_proxy_err_set(x->cf, x->data, (int)n); + return CURLE_SEND_ERROR; + } + } + + if(ndatalen >= 0) { + /* we add the amount of data bytes to the flow windows */ + int rv = nghttp3_conn_add_write_offset(ctx->h3conn, stream_id, ndatalen); + if(rv) { + failf(x->data, "nghttp3_conn_add_write_offset returned error: %s", + nghttp3_strerror(rv)); + return CURLE_SEND_ERROR; + } + } + + if(n > 0) { + /* packet assembled, leave */ + *pnread = (size_t)n; + return CURLE_OK; + } + } +} + +static CURLcode proxy_h3_progress_egress_ngtcp2(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct proxy_pkt_io_ctx *pktx) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + size_t nread; + size_t max_payload_size, path_max_payload_size; + size_t pktcnt = 0; + size_t gsolen = 0; /* this disables gso until we have a clue */ + size_t send_quantum; + CURLcode result; + struct proxy_pkt_io_ctx local_pktx; + + if(!pktx) { + proxy_pktx_init(&local_pktx, cf, data); + pktx = &local_pktx; + } + else { + proxy_pktx_update_time(pktx, cf); + ngtcp2_path_storage_zero(&pktx->ps); + } + + result = vquic_flush(cf, data, &ctx->q); + if(result) { + if(result == CURLE_AGAIN) { + Curl_expire(data, 1, EXPIRE_QUIC); + return CURLE_OK; + } + return result; + } + + /* In UDP, there is a maximum theoretical packet payload length and + * a minimum payload length that is "guaranteed" to work. + * To detect if this minimum payload can be increased, ngtcp2 sends + * now and then a packet payload larger than the minimum. It that + * is ACKed by the peer, both parties know that it works and + * the subsequent packets can use a larger one. + * This is called PMTUD (Path Maximum Transmission Unit Discovery). + * Since a PMTUD might be rejected right on send, we do not want it + * be followed by other packets of lesser size. Because those would + * also fail then. If we detect a PMTUD while buffering, we flush. + */ + max_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(ctx->qconn); + path_max_payload_size = + ngtcp2_conn_get_path_max_tx_udp_payload_size(ctx->qconn); + send_quantum = ngtcp2_conn_get_send_quantum(ctx->qconn); + CURL_TRC_CF(data, cf, "egress, collect and send packets, quantum=%zu", + send_quantum); + for(;;) { + /* add the next packet to send, if any, to our buffer */ + result = Curl_bufq_sipn(&ctx->q.sendbuf, max_payload_size, + proxy_read_pkt_to_send, pktx, &nread); + if(result == CURLE_AGAIN) + break; + else if(result) + return result; + else { + size_t buflen = Curl_bufq_len(&ctx->q.sendbuf); + if((buflen >= send_quantum) || + ((buflen + gsolen) >= ctx->q.sendbuf.chunk_size)) + break; + DEBUGASSERT(nread > 0); + ++pktcnt; + if(pktcnt == 1) { + /* first packet in buffer. This is either of a known, "good" + * payload size or it is a PMTUD. We shall see. */ + gsolen = nread; + } + else if(nread > gsolen || + (gsolen > path_max_payload_size && nread != gsolen)) { + /* The added packet is a PMTUD *or* the one(s) before the + * added were PMTUD and the last one is smaller. + * Flush the buffer before the last add. */ + result = vquic_send_tail_split(cf, data, &ctx->q, + gsolen, nread, nread); + if(result) { + if(result == CURLE_AGAIN) { + Curl_expire(data, 1, EXPIRE_QUIC); + return CURLE_OK; + } + return result; + } + pktcnt = 0; + } + else if(nread < gsolen) { + /* Reached capacity of our buffer *or* + * last add was shorter than the previous ones, flush */ + break; + } + } + } + + if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) { + /* time to send */ + CURL_TRC_CF(data, cf, "egress, send collected %zu packets in %zu bytes", + pktcnt, Curl_bufq_len(&ctx->q.sendbuf)); + result = vquic_send(cf, data, &ctx->q, gsolen); + if(result) { + if(result == CURLE_AGAIN) { + Curl_expire(data, 1, EXPIRE_QUIC); + return CURLE_OK; + } + return result; + } + proxy_pktx_update_time(pktx, cf); + ngtcp2_conn_update_pkt_tx_time(ctx->qconn, pktx->ts); + } + return CURLE_OK; +} + +static CURLcode cf_ngtcp2_proxy_shutdown(struct Curl_cfilter *cf, + struct Curl_easy *data, bool *done) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct cf_call_data save; + struct proxy_pkt_io_ctx pktx; + CURLcode result = CURLE_OK; + + if(cf->shutdown || !ctx->qconn) { + *done = TRUE; + return CURLE_OK; + } + + if(!cf->next) { + Curl_bufq_reset(&ctx->q.sendbuf); + *done = TRUE; + return CURLE_OK; + } + + CF_DATA_SAVE(save, cf, data); + *done = FALSE; + proxy_pktx_init(&pktx, cf, data); + + if(!ctx->shutdown_started) { + char buffer[NGTCP2_MAX_UDP_PAYLOAD_SIZE]; + ngtcp2_ssize nwritten; + + if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) { + CURL_TRC_CF(data, cf, "shutdown, flushing sendbuf"); + result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx); + if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) { + CURL_TRC_CF(data, cf, "sending shutdown packets blocked"); + result = CURLE_OK; + goto out; + } + else if(result) { + CURL_TRC_CF(data, cf, "shutdown, error %d flushing sendbuf", result); + *done = TRUE; + goto out; + } + } + + DEBUGASSERT(Curl_bufq_is_empty(&ctx->q.sendbuf)); + ctx->shutdown_started = TRUE; + nwritten = ngtcp2_conn_write_connection_close( + ctx->qconn, NULL, /* path */ + NULL, /* pkt_info */ + (uint8_t *)buffer, sizeof(buffer), + &ctx->last_error, pktx.ts); + CURL_TRC_CF(data, cf, "start shutdown(err_type=%d, err_code=%" + PRIu64 ") -> %zd", ctx->last_error.type, + (uint64_t)ctx->last_error.error_code, (ssize_t)nwritten); + /* there are cases listed in ngtcp2 documentation where this call + * may fail. Since we are doing a connection shutdown as graceful + * as we can, such an error is ignored here. */ + if(nwritten > 0) { + /* Ignore amount written. sendbuf was empty and has always room for + * NGTCP2_MAX_UDP_PAYLOAD_SIZE. It can only completely fail, in which + * case `result` is set non zero. */ + size_t n; + result = Curl_bufq_write(&ctx->q.sendbuf, (const unsigned char *)buffer, + (size_t)nwritten, &n); + if(result) { + CURL_TRC_CF(data, cf, "error %d adding shutdown packets to sendbuf, " + "aborting shutdown", result); + goto out; + } + + ctx->q.no_gso = TRUE; + ctx->q.gsolen = (size_t)nwritten; + ctx->q.split_len = 0; + } + } + + if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) { + CURL_TRC_CF(data, cf, "shutdown, flushing egress"); + result = vquic_flush(cf, data, &ctx->q); + if(result == CURLE_AGAIN) { + CURL_TRC_CF(data, cf, "sending shutdown packets blocked"); + result = CURLE_OK; + goto out; + } + else if(result) { + CURL_TRC_CF(data, cf, "shutdown, error %d flushing sendbuf", result); + *done = TRUE; + goto out; + } + } + + if(Curl_bufq_is_empty(&ctx->q.sendbuf)) { + /* Sent everything off. ngtcp2 seems to have no support for graceful + * shutdowns. We are done. */ + CURL_TRC_CF(data, cf, "shutdown completely sent off, done"); + *done = TRUE; + result = CURLE_OK; + } +out: + CF_DATA_RESTORE(cf, save); + return result; +} + +static void cf_ngtcp2_proxy_conn_close(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + bool done; + cf_ngtcp2_proxy_shutdown(cf, data, &done); +} + +static void cf_ngtcp2_proxy_close(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct cf_call_data save; + + CF_DATA_SAVE(save, cf, data); + if(ctx && ctx->qconn) { + cf_ngtcp2_proxy_conn_close(cf, data); + cf_ngtcp2_proxy_ctx_close(ctx); + CURL_TRC_CF(data, cf, "close"); + } + cf->connected = FALSE; + CF_DATA_RESTORE(cf, save); +} + +static void cf_ngtcp2_proxy_stream_close(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h3_proxy_stream_ctx *stream) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + DEBUGASSERT(data); + DEBUGASSERT(stream); + + if(stream->id == proxy_ctx->tunnel.stream_id) { + proxy_ctx->tunnel.stream = NULL; + proxy_ctx->tunnel.closed = TRUE; + } + + if(ctx->h3conn) + nghttp3_conn_set_stream_user_data(ctx->h3conn, stream->id, NULL); + if(ctx->qconn) + ngtcp2_conn_set_stream_user_data(ctx->qconn, stream->id, NULL); + + if(!stream->closed && ctx->qconn && ctx->h3conn) { + CURLcode result; + + stream->closed = TRUE; + (void)ngtcp2_conn_shutdown_stream(ctx->qconn, 0, stream->id, + NGHTTP3_H3_REQUEST_CANCELLED); + result = proxy_h3_progress_egress_ngtcp2(cf, data, NULL); + if(result) + CURL_TRC_CF(data, cf, "[%" PRId64 "] cancel stream -> %d", + stream->id, result); + } +} + +/** + * Connection maintenance like timeouts on packet ACKs etc. are done by us, not + * the OS like for TCP. POLL events on the socket therefore are not + * sufficient. + * ngtcp2 tells us when it wants to be invoked again. We handle that via + * the `Curl_expire()` mechanisms. + */ +static CURLcode check_and_set_expiry_ngtcp2(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct proxy_pkt_io_ctx *pktx) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct proxy_pkt_io_ctx local_pktx; + ngtcp2_tstamp expiry; + + if(!ctx) + return CURLE_OK; + + if(!pktx) { + proxy_pktx_init(&local_pktx, cf, data); + pktx = &local_pktx; + } + else { + proxy_pktx_update_time(pktx, cf); + } + + expiry = ngtcp2_conn_get_expiry(ctx->qconn); + if(expiry != UINT64_MAX) { + if(expiry <= pktx->ts) { + CURLcode result; + int rv = ngtcp2_conn_handle_expiry(ctx->qconn, pktx->ts); + if(rv) { + failf(data, "ngtcp2_conn_handle_expiry returned error: %s", + ngtcp2_strerror(rv)); + cf_ngtcp2_proxy_err_set(cf, data, rv); + return CURLE_SEND_ERROR; + } + result = proxy_h3_progress_ingress_ngtcp2(cf, data, pktx); + if(result) + return result; + result = proxy_h3_progress_egress_ngtcp2(cf, data, pktx); + if(result) + return result; + /* ask again, things might have changed */ + expiry = ngtcp2_conn_get_expiry(ctx->qconn); + } + + if(expiry > pktx->ts) { + ngtcp2_duration timeout = expiry - pktx->ts; + if(timeout % NGTCP2_MILLISECONDS) { + timeout += NGTCP2_MILLISECONDS; + } + Curl_expire(data, (timediff_t)(timeout / NGTCP2_MILLISECONDS), + EXPIRE_QUIC); + } + } + return CURLE_OK; +} + +static ssize_t proxy_recv_closed_stream(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h3_proxy_stream_ctx *stream, + CURLcode *err) +{ + ssize_t nread = -1; + *err = CURLE_OK; + + if(stream->reset) { + if(stream->error3 == CURL_H3_ERR_REQUEST_REJECTED) { + infof(data, "HTTP/3 stream %" PRId64 " refused by server, try again " + "on a new connection", stream->id); + connclose(cf->conn, "REFUSED_STREAM"); + data->state.refused_stream = TRUE; + *err = CURLE_RECV_ERROR; + goto out; + } + else if(stream->resp_hds_complete && data->req.no_body) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] error after response headers, " + "but we did not want a body anyway, ignore error 0x%" + PRIx64 " %s", stream->id, stream->error3, + vquic_h3_err_str(stream->error3)); + nread = 0; + goto out; + } + failf(data, "HTTP/3 stream %" PRId64 " reset by server (error 0x%" PRIx64 + " %s)", stream->id, stream->error3, + vquic_h3_err_str(stream->error3)); + *err = data->req.bytecount ? CURLE_PARTIAL_FILE : CURLE_HTTP3; + goto out; + } + else if(!stream->resp_hds_complete) { + failf(data, + "HTTP/3 stream %" PRId64 " was closed cleanly, but before " + "getting all response header fields, treated as error", + stream->id); + *err = CURLE_HTTP3; + goto out; + } + nread = 0; + +out: + return nread; +} + +static struct h3_proxy_stream_ctx * +h3_proxy_resolve_send_stream(struct cf_h3_proxy_ctx *proxy_ctx, + struct cf_ngtcp2_proxy_ctx *ctx, + struct Curl_easy *data) +{ + struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data); + + if(stream) + return stream; + + /* send can be driven by a different easy handle during shutdown */ + if(proxy_ctx->tunnel.stream && !proxy_ctx->tunnel.closed) { + return proxy_ctx->tunnel.stream; + } + return NULL; +} + +static CURLcode h3_proxy_sendbuf_add(struct Curl_easy *data, + struct h3_proxy_stream_ctx *stream, + const uint8_t *buf, size_t len, + size_t *pnwritten) +{ + CURLcode result; + *pnwritten = 0; + (void)data; + + result = Curl_bufq_write(&stream->sendbuf, buf, len, pnwritten); + return result; +} + +static CURLcode cf_h3_proxy_send(struct Curl_cfilter *cf, + struct Curl_easy *data, + const uint8_t *buf, size_t len, + bool eos, size_t *pnwritten) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct h3_proxy_stream_ctx *stream = NULL; + struct cf_call_data save; + struct proxy_pkt_io_ctx pktx; + CURLcode result = CURLE_OK; + + CF_DATA_SAVE(save, cf, data); + DEBUGASSERT(cf->connected); + DEBUGASSERT(ctx->qconn); + DEBUGASSERT(ctx->h3conn); + proxy_pktx_init(&pktx, cf, data); + *pnwritten = 0; + + /* handshake verification failed in callback, do not send anything */ + if(ctx->tls_vrfy_result) { + result = ctx->tls_vrfy_result; + goto denied; + } + + (void)eos; /* use for stream EOF and block handling */ + result = proxy_h3_progress_ingress_ngtcp2(cf, data, &pktx); + if(result) + goto out; + + stream = h3_proxy_resolve_send_stream(proxy_ctx, ctx, data); + if(!stream) { + result = CURLE_SEND_ERROR; + goto denied; + } + + if(proxy_ctx->tunnel.closed) { + result = CURLE_SEND_ERROR; + goto denied; + } + + if(stream->closed) { + if(stream->resp_hds_complete) { + /* Server decided to close the stream after having sent us a final + * response. This is valid if it is not interested in the request + * body. This happens on 30x or 40x responses. + * We silently discard the data sent, since this is not a transport + * error situation. */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] discarding data" + "on closed stream with response", stream->id); + result = CURLE_OK; + *pnwritten = len; + goto out; + } + CURL_TRC_CF(data, cf, "[%" PRId64 "] send_body(len=%zu) " + "-> stream closed", stream->id, len); + result = CURLE_HTTP3; + goto out; + } + else { + result = h3_proxy_sendbuf_add(data, stream, buf, len, pnwritten); + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_send, add to " + "sendbuf(len=%zu) -> %d, %zu", + stream->id, len, result, *pnwritten); + if(result) + goto out; + (void)nghttp3_conn_resume_stream(ctx->h3conn, stream->id); + } + + if(*pnwritten > 0 && !ctx->tls_handshake_complete && ctx->use_earlydata) + ctx->earlydata_skip += *pnwritten; + + DEBUGASSERT(!result); + result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx); + +out: + result = Curl_1st_fatal(result, + check_and_set_expiry_ngtcp2(cf, data, &pktx)); +denied: + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_send(len=%zu) -> %d, %zu", + stream ? stream->id : -1, len, result, *pnwritten); + CF_DATA_RESTORE(cf, save); + return result; +} + +/* incoming data frames on the h3 stream */ +static CURLcode cf_h3_proxy_recv(struct Curl_cfilter *cf, + struct Curl_easy *data, + char *buf, size_t len, size_t *pnread) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data); + struct cf_call_data save; + struct proxy_pkt_io_ctx pktx; + CURLcode result = CURLE_OK; + + CF_DATA_SAVE(save, cf, data); + DEBUGASSERT(cf->connected); + DEBUGASSERT(ctx); + DEBUGASSERT(ctx->qconn); + DEBUGASSERT(ctx->h3conn); + *pnread = 0; + + /* handshake verification failed in callback, do not recv anything */ + if(ctx->tls_vrfy_result) { + result = ctx->tls_vrfy_result; + goto denied; + } + + proxy_pktx_init(&pktx, cf, data); + + if(!stream || ctx->shutdown_started) { + result = CURLE_RECV_ERROR; + goto out; + } + + if(!Curl_bufq_is_empty(&proxy_ctx->inbufq)) { + result = Curl_bufq_cread(&proxy_ctx->inbufq, + buf, len, pnread); + if(result) + goto out; + } + + result = proxy_h3_progress_ingress_ngtcp2(cf, data, &pktx); + if(result) + goto out; + + /* inbufq had nothing before, maybe after progressing ingress? */ + if(!*pnread && !Curl_bufq_is_empty(&proxy_ctx->inbufq)) { + result = Curl_bufq_cread(&proxy_ctx->inbufq, + buf, len, pnread); + if(result) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] read inbufq(len=%zu) " + "-> %zd, %d", + stream->id, len, *pnread, result); + goto out; + } + } + + if(*pnread) { + Curl_multi_mark_dirty(data); + } + else { + if(stream->xfer_result) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] xfer write failed", + stream->id); + cf_ngtcp2_proxy_stream_close(cf, data, stream); + result = stream->xfer_result; + goto out; + } + else if(stream->closed) { + ssize_t nread = proxy_recv_closed_stream(cf, data, stream, &result); + if(nread > 0) + *pnread = (size_t)nread; + goto out; + } + result = CURLE_AGAIN; + } + +out: + result = Curl_1st_fatal(result, + proxy_h3_progress_egress_ngtcp2(cf, data, &pktx)); + result = Curl_1st_fatal(result, + check_and_set_expiry_ngtcp2(cf, data, &pktx)); +denied: + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_recv(len=%zu) -> %d, %zu", + stream ? stream->id : -1, len, result, *pnread); + CF_DATA_RESTORE(cf, save); + return result; +} + +static void proxy_h3_submit(int64_t *pstream_id, + struct Curl_cfilter *cf, + struct Curl_easy *data, + struct httpreq *req, + CURLcode *err) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct h3_proxy_stream_ctx *stream = NULL; + + struct dynhds h2_headers; + nghttp3_nv *nva = NULL; + size_t nheader; + + int rc = 0; + unsigned int i; + nghttp3_data_reader reader; + nghttp3_data_reader *preader = NULL; + + Curl_dynhds_init(&h2_headers, 0, DYN_HTTP_REQUEST); + *err = Curl_http_req_to_h2(&h2_headers, req, data); + if(*err) + goto out; + + *err = h3_proxy_data_setup(cf, data); + if(*err) + goto out; + + if(!ctx) { + *err = CURLE_FAILED_INIT; + goto out; + } + + stream = H3_PROXY_STREAM_CTX(ctx, data); + + DEBUGASSERT(stream); + if(!stream) { + *err = CURLE_FAILED_INIT; + goto out; + } + + nheader = Curl_dynhds_count(&h2_headers); + nva = curlx_malloc(sizeof(nghttp3_nv) * nheader); + if(!nva) { + *err = CURLE_OUT_OF_MEMORY; + goto out; + } + + for(i = 0; i < nheader; ++i) { + struct dynhds_entry *e = Curl_dynhds_getn(&h2_headers, i); + nva[i].name = (unsigned char *)e->name; + nva[i].namelen = e->namelen; + nva[i].value = (unsigned char *)e->value; + nva[i].valuelen = e->valuelen; + nva[i].flags = NGHTTP3_NV_FLAG_NONE; + } + + /* Open a bidirectional stream */ + { + int64_t sid; + int rv; + + DEBUGASSERT(stream->id == -1); + rv = ngtcp2_conn_open_bidi_stream(ctx->qconn, &sid, data); + if(rv) { + failf(data, "cannot get bidi streams: %s", ngtcp2_strerror(rv)); + *err = CURLE_SEND_ERROR; + goto out; + } + stream->id = (int64_t)sid; + ++ctx->used_bidi_streams; + + /* Set stream user data in ngtcp2 connection for callbacks */ + rv = ngtcp2_conn_set_stream_user_data(ctx->qconn, sid, data); + if(rv) { + failf(data, "cannot set stream user data: %s", ngtcp2_strerror(rv)); + *err = CURLE_SEND_ERROR; + goto out; + } + proxy_ctx->tunnel.stream = stream; + CURL_TRC_CF(data, cf, "[%" PRId64 "] opened bidi stream", sid); + } + + /* CONNECT-UDP request stream remains open for capsules, no fixed EOF. */ + stream->upload_left = -1; + stream->send_closed = 0; + reader.read_data = cb_h3_read_data_for_tunnel_stream; + preader = &reader; + + rc = nghttp3_conn_submit_request(ctx->h3conn, H3_STREAM_ID(stream), + nva, nheader, preader, data); + + if(rc) { + switch(rc) { + case NGHTTP3_ERR_CONN_CLOSING: + CURL_TRC_CF(data, cf, "h3sid[%" PRId64 "] failed to send, " + "connection is closing", + H3_STREAM_ID(stream)); + break; + default: + CURL_TRC_CF(data, cf, "h3sid[%" PRId64 "] failed to send -> %d (%s)", + H3_STREAM_ID(stream), rc, nghttp3_strerror(rc)); + break; + } + *err = CURLE_SEND_ERROR; + goto out; + } + + if(Curl_trc_is_verbose(data)) { + CURL_TRC_CF(data, cf, "[H3-PROXY] [%" PRId64 "] OPENED stream " + "for %s", H3_STREAM_ID(stream), + Curl_bufref_ptr(&data->state.url)); + } + +out: + curlx_free(nva); + Curl_dynhds_free(&h2_headers); + if(*err == CURLE_OK) { + *pstream_id = H3_STREAM_ID(stream); + } +} + +static bool cf_h3_proxy_is_alive(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool *input_pending) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + bool alive = FALSE; + const ngtcp2_transport_params *rp; + struct cf_call_data save; + + CF_DATA_SAVE(save, cf, data); + *input_pending = FALSE; + + if(!ctx || !ctx->qconn || ctx->shutdown_started) + goto out; + if(proxy_ctx->tunnel.closed) + goto out; + + /* We do not announce a max idle timeout, but when the peer does + * it closes the connection when it expires. */ + rp = ngtcp2_conn_get_remote_transport_params(ctx->qconn); + if(rp && rp->max_idle_timeout) { + timediff_t idletime_ms = + curlx_ptimediff_ms(Curl_pgrs_now(data), &ctx->q.last_io); + if(idletime_ms > 0) { + uint64_t max_idle_ms = + (uint64_t)(rp->max_idle_timeout / NGTCP2_MILLISECONDS); + if((uint64_t)idletime_ms > max_idle_ms) + goto out; + } + } + + if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending)) + goto out; + + alive = TRUE; + if(*input_pending) { + CURLcode result; + /* This happens before we have sent off a request and the connection is + not in use by any other transfer, there should not be any data here, + only "protocol frames" */ + *input_pending = FALSE; + if(!data || !data->multi) { + alive = FALSE; + goto out; + } + result = proxy_h3_progress_ingress_ngtcp2(cf, data, NULL); + CURL_TRC_CF(data, cf, "is_alive, progress ingress -> %d", result); + alive = result ? FALSE : TRUE; + } + +out: + CF_DATA_RESTORE(cf, save); + return alive; +} + +static CURLcode cf_ngtcp2_proxy_query(struct Curl_cfilter *cf, + struct Curl_easy *data, + int query, int *pres1, void *pres2) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct cf_call_data save; + + if(!ctx) + return cf->next ? + cf->next->cft->query(cf->next, data, query, pres1, pres2) : + CURLE_UNKNOWN_OPTION; + + switch(query) { + case CF_QUERY_MAX_CONCURRENT: { + DEBUGASSERT(pres1); + CF_DATA_SAVE(save, cf, data); + /* Set after transport params arrived and continually updated + * by callback. QUIC counts the number over the lifetime of the + * connection, ever increasing. + * We count the *open* transfers plus the budget for new ones. */ + if(!ctx->qconn || ctx->shutdown_started) { + *pres1 = 0; + } + else if(ctx->max_bidi_streams) { + uint64_t avail_bidi_streams = 0; + uint64_t max_streams = cf->conn->attached_xfers; + if(ctx->max_bidi_streams > ctx->used_bidi_streams) + avail_bidi_streams = ctx->max_bidi_streams - ctx->used_bidi_streams; + max_streams += avail_bidi_streams; + *pres1 = (max_streams > INT_MAX) ? INT_MAX : (int)max_streams; + } + else /* transport params not arrived yet? take our default. */ + *pres1 = (int)Curl_multi_max_concurrent_streams(data->multi); + CURL_TRC_CF(data, cf, "query conn[%" FMT_OFF_T "]: " + "MAX_CONCURRENT -> %d (%u in use)", + cf->conn->connection_id, *pres1, cf->conn->attached_xfers); + CF_DATA_RESTORE(cf, save); + return CURLE_OK; + } + case CF_QUERY_CONNECT_REPLY_MS: + if(ctx->q.got_first_byte) { + timediff_t ms = curlx_ptimediff_ms(&ctx->q.first_byte_at, + &ctx->started_at); + *pres1 = (ms < INT_MAX) ? (int)ms : INT_MAX; + } + else + *pres1 = -1; + return CURLE_OK; + case CF_QUERY_TIMER_CONNECT: { + struct curltime *when = pres2; + if(ctx->q.got_first_byte) + *when = ctx->q.first_byte_at; + return CURLE_OK; + } + case CF_QUERY_TIMER_APPCONNECT: { + struct curltime *when = pres2; + if(cf->connected) + *when = ctx->handshake_at; + return CURLE_OK; + } + case CF_QUERY_HTTP_VERSION: + *pres1 = 30; + return CURLE_OK; + case CF_QUERY_SSL_INFO: + case CF_QUERY_SSL_CTX_INFO: { + struct curl_tlssessioninfo *info = pres2; + if(Curl_vquic_tls_get_ssl_info(&ctx->tls, + (query == CF_QUERY_SSL_CTX_INFO), info)) + return CURLE_OK; + break; + } + case CF_QUERY_ALPN_NEGOTIATED: { + const char **palpn = pres2; + DEBUGASSERT(palpn); + *palpn = cf->connected ? "h3" : NULL; + return CURLE_OK; + } + default: + break; + } + return cf->next ? + cf->next->cft->query(cf->next, data, query, pres1, pres2) : + CURLE_UNKNOWN_OPTION; +} + +static CURLcode cf_ngtcp2_proxy_adjust_pollset(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct easy_pollset *ps) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + bool want_recv, want_send; + CURLcode result = CURLE_OK; + + if(!ctx->qconn) + return CURLE_OK; + + Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send); + if(!want_send && !Curl_bufq_is_empty(&ctx->q.sendbuf)) + want_send = TRUE; + + if(want_recv || want_send) { + struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data); + struct cf_call_data save; + bool c_exhaust, s_exhaust; + + CF_DATA_SAVE(save, cf, data); + c_exhaust = want_send && (!ngtcp2_conn_get_cwnd_left(ctx->qconn) || + !ngtcp2_conn_get_max_data_left(ctx->qconn)); + s_exhaust = want_send && stream && H3_STREAM_ID(stream) >= 0 && + stream->quic_flow_blocked; + want_recv = (want_recv || c_exhaust || s_exhaust); + want_send = (!s_exhaust && want_send) || + !Curl_bufq_is_empty(&ctx->q.sendbuf); + + result = Curl_pollset_set(data, ps, ctx->q.sockfd, want_recv, want_send); + CF_DATA_RESTORE(cf, save); + } + return result; +} + +static CURLcode cf_h3_proxy_query(struct Curl_cfilter *cf, + struct Curl_easy *data, + int query, int *pres1, void *pres2) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + + if(!proxy_ctx) + return cf->next ? + cf->next->cft->query(cf->next, data, query, pres1, pres2) : + CURLE_UNKNOWN_OPTION; + return cf_ngtcp2_proxy_query(cf, data, query, pres1, pres2); +} + +static CURLcode cf_h3_proxy_adjust_pollset(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct easy_pollset *ps) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + + if(!proxy_ctx) + return cf->next ? + cf->next->cft->adjust_pollset(cf->next, data, ps) : + CURLE_OK; + return cf_ngtcp2_proxy_adjust_pollset(cf, data, ps); +} + +static bool cf_h3_proxy_data_pending(struct Curl_cfilter *cf, + const struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + if(!proxy_ctx) + return cf->next ? + cf->next->cft->has_data_pending(cf->next, data) : FALSE; + if(!Curl_bufq_is_empty(&proxy_ctx->inbufq)) + return TRUE; + return cf->next ? + cf->next->cft->has_data_pending(cf->next, data) : FALSE; +} + +#ifdef USE_OPENSSL +static int proxy_quic_ossl_new_session_cb(SSL *ssl, SSL_SESSION *ssl_sessionid) +{ + ngtcp2_crypto_conn_ref *cref; + struct Curl_cfilter *cf; + struct cf_h3_proxy_ctx *proxy_ctx; + struct cf_ngtcp2_proxy_ctx *ctx; + struct Curl_easy *data; + + cref = (ngtcp2_crypto_conn_ref *)SSL_get_app_data(ssl); + cf = cref ? cref->user_data : NULL; + proxy_ctx = cf ? cf->ctx : NULL; + ctx = proxy_ctx ? proxy_ctx->ngtcp2_ctx : NULL; + data = cf ? CF_DATA_CURRENT(cf) : NULL; + if(cf && data && ctx) { + unsigned char *quic_tp = NULL; + size_t quic_tp_len = 0; +#ifdef HAVE_OPENSSL_EARLYDATA + ngtcp2_ssize tplen; + uint8_t tpbuf[256]; + + tplen = ngtcp2_conn_encode_0rtt_transport_params(ctx->qconn, tpbuf, + sizeof(tpbuf)); + if(tplen < 0) + CURL_TRC_CF(data, cf, "error encoding 0RTT transport data: %s", + ngtcp2_strerror((int)tplen)); + else { + quic_tp = (unsigned char *)tpbuf; + quic_tp_len = (size_t)tplen; + } +#endif /* HAVE_OPENSSL_EARLYDATA */ + Curl_ossl_add_session(cf, data, ctx->peer.scache_key, ssl_sessionid, + SSL_version(ssl), "h3", quic_tp, quic_tp_len); + } + return 0; +} +#endif /* USE_OPENSSL */ + +static CURLcode cf_ngtcp2_proxy_tls_ctx_setup(struct Curl_cfilter *cf, + struct Curl_easy *data, + void *user_data) +{ + struct curl_tls_ctx *ctx = user_data; + +#ifdef USE_OPENSSL +#if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) + if(ngtcp2_crypto_boringssl_configure_client_context(ctx->ossl.ssl_ctx) + != 0) { + failf(data, "ngtcp2_crypto_boringssl_configure_client_context failed"); + return CURLE_FAILED_INIT; + } +#elif defined(OPENSSL_QUIC_API2) + /* nothing to do */ +#else + if(ngtcp2_crypto_quictls_configure_client_context(ctx->ossl.ssl_ctx) != 0) { + failf(data, "ngtcp2_crypto_quictls_configure_client_context failed"); + return CURLE_FAILED_INIT; + } +#endif + if(Curl_ssl_scache_use(cf, data)) { + SSL_CTX_set_session_cache_mode(ctx->ossl.ssl_ctx, + SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL); + SSL_CTX_sess_set_new_cb(ctx->ossl.ssl_ctx, proxy_quic_ossl_new_session_cb); + } + +#else +#error "ngtcp2 TLS backend not configured" +#endif /* USE_OPENSSL */ + + return CURLE_OK; +} + +static CURLcode cf_ngtcp2_proxy_on_session_reuse(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct alpn_spec *alpns, + struct Curl_ssl_session *scs, + bool *do_early_data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + CURLcode result = CURLE_OK; + + *do_early_data = FALSE; +#if defined(USE_OPENSSL) && defined(HAVE_OPENSSL_EARLYDATA) + ctx->earlydata_max = scs->earlydata_max; +#endif +#ifdef USE_GNUTLS + ctx->earlydata_max = + gnutls_record_get_max_early_data_size(ctx->tls.gtls.session); +#endif /* USE_GNUTLS */ +#ifdef USE_WOLFSSL +#ifdef WOLFSSL_EARLY_DATA + ctx->earlydata_max = scs->earlydata_max; +#else + ctx->earlydata_max = 0; +#endif /* WOLFSSL_EARLY_DATA */ +#endif /* USE_WOLFSSL */ +#if defined(USE_GNUTLS) || defined(USE_WOLFSSL) || \ + (defined(USE_OPENSSL) && defined(HAVE_OPENSSL_EARLYDATA)) + if((!ctx->earlydata_max)) { + CURL_TRC_CF(data, cf, "SSL session does not allow earlydata"); + } + else if(!Curl_alpn_contains_proto(alpns, scs->alpn)) { + CURL_TRC_CF(data, cf, "SSL session from different ALPN, no early data"); + } + else if(!scs->quic_tp || !scs->quic_tp_len) { + CURL_TRC_CF(data, cf, "no 0RTT transport parameters, no early data, "); + } + else { + int rv; + rv = ngtcp2_conn_decode_and_set_0rtt_transport_params( + ctx->qconn, (const uint8_t *)scs->quic_tp, scs->quic_tp_len); + if(rv) + CURL_TRC_CF(data, cf, "no early data, failed to set 0RTT transport " + "parameters: %s", ngtcp2_strerror(rv)); + else { + infof(data, "SSL session allows %zu bytes of early data, " + "reusing ALPN '%s'", ctx->earlydata_max, scs->alpn); + result = cf_ngtcp2_h3conn_init(cf, data); + if(!result) { + ctx->use_earlydata = TRUE; + proxy_ctx->connected = TRUE; + *do_early_data = TRUE; + } + } + } +#else /* not supported in the TLS backend */ + (void)data; + (void)ctx; + (void)scs; + (void)alpns; +#endif + return result; +} + +static CURLcode cf_h3_proxy_ctx_init(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = NULL; + int rc; + int rv; + CURLcode result = CURLE_OK; + const struct Curl_sockaddr_ex *sockaddr = NULL; + int qfd; + static const struct alpn_spec ALPN_SPEC_H3 = {{ "h3", "h3-29" }, 2}; + struct proxy_pkt_io_ctx pktx; + + ctx = curlx_calloc(1, sizeof(struct cf_ngtcp2_proxy_ctx)); + if(!ctx) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + cf_ngtcp2_proxy_ctx_init(ctx); + + memset(&proxy_ctx->tunnel, 0, sizeof(proxy_ctx->tunnel)); + + Curl_bufq_init2(&proxy_ctx->inbufq, PROXY_H3_STREAM_CHUNK_SIZE, + PROXY_H3_STREAM_RECV_CHUNKS, BUFQ_OPT_SOFT_LIMIT); + + result = h3_tunnel_stream_init(&proxy_ctx->tunnel, proxy_ctx->dest); + if(result) + goto out; + + DEBUGASSERT(ctx->initialized); + ctx->started_at = *Curl_pgrs_now(data); + + /* Initialize connection IDs BEFORE creating the connection */ + ctx->dcid.datalen = NGTCP2_MAX_CIDLEN; + result = Curl_rand(data, ctx->dcid.data, NGTCP2_MAX_CIDLEN); + if(result) + goto out; + + ctx->scid.datalen = NGTCP2_MAX_CIDLEN; + result = Curl_rand(data, ctx->scid.data, NGTCP2_MAX_CIDLEN); + if(result) + goto out; + + (void)Curl_qlogdir(data, ctx->scid.data, NGTCP2_MAX_CIDLEN, &qfd); + ctx->qlogfd = qfd; /* -1 if failure above */ + + result = CURLE_QUIC_CONNECT_ERROR; + if(!cf->next) { + CURL_TRC_CF(data, cf, "h3_proxy_ctx_init: no lower filter"); + goto out; + } + ctx->q.sockfd = Curl_conn_cf_get_socket(cf->next, data); + if(ctx->q.sockfd == CURL_SOCKET_BAD) + goto out; + /* Get remote address from the socket filter below */ + if(cf->next->cft->query(cf->next, data, CF_QUERY_REMOTE_ADDR, NULL, + CURL_UNCONST(&sockaddr))) + goto out; + if(!sockaddr) + goto out; + ctx->q.local_addrlen = sizeof(ctx->q.local_addr); + rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr, + &ctx->q.local_addrlen); + if(rv == -1) + goto out; + + /* Initialize vquic context BEFORE proxy_pktx_init which needs it */ + result = vquic_ctx_init(data, &ctx->q); + if(result) + goto out; + + /* Set ngtcp2_ctx in proxy_ctx BEFORE proxy_pktx_init which accesses it */ + proxy_ctx->ngtcp2_ctx = ctx; + + /* Now we can safely initialize pktx and settings */ + proxy_pktx_init(&pktx, cf, data); + quic_settings_proxy(ctx, data, &pktx); + + ngtcp2_addr_init(&ctx->connected_path.local, + (struct sockaddr *)&ctx->q.local_addr, + ctx->q.local_addrlen); + ngtcp2_addr_init(&ctx->connected_path.remote, + &sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen); + + rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid, + &ctx->connected_path, + NGTCP2_PROTO_VER_V1, &ngtcp2_proxy_callbacks, + &ctx->settings, &ctx->transport_params, + Curl_ngtcp2_mem(), cf); + if(rc) { + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + + ctx->conn_ref.get_conn = proxy_get_conn; + ctx->conn_ref.user_data = cf; + + result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, &ALPN_SPEC_H3, + cf_ngtcp2_proxy_tls_ctx_setup, &ctx->tls, + &ctx->conn_ref, + cf_ngtcp2_proxy_on_session_reuse); + if(result) + goto out; + +#if defined(USE_OPENSSL) && defined(OPENSSL_QUIC_API2) + if(ngtcp2_crypto_ossl_ctx_new(&ctx->ossl_ctx, ctx->tls.ossl.ssl) != 0) { + failf(data, "ngtcp2_crypto_ossl_ctx_new failed"); + result = CURLE_FAILED_INIT; + goto out; + } + ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->ossl_ctx); + if(ngtcp2_crypto_ossl_configure_client_session(ctx->tls.ossl.ssl) != 0) { + failf(data, "ngtcp2_crypto_ossl_configure_client_session failed"); + result = CURLE_FAILED_INIT; + goto out; + } +#elif defined(USE_OPENSSL) + SSL_set_quic_use_legacy_codepoint(ctx->tls.ossl.ssl, 0); + ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->tls.ossl.ssl); +#else +#error "ngtcp2 TLS backend not defined" +#endif /* USE_OPENSSL */ + + ngtcp2_ccerr_default(&ctx->last_error); + + proxy_ctx->connected = FALSE; + +out: + if(result) { + if(ctx) { + proxy_ctx->ngtcp2_ctx = NULL; /* Clear before freeing on error */ + cf_ngtcp2_proxy_ctx_free(ctx); + } + } + CURL_TRC_CF(data, cf, "QUIC tls init -> %d", result); + return result; +} + +static CURLcode h3_submit_CONNECT(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h3_tunnel_stream *ts) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + CURLcode result; + struct httpreq *req = NULL; + + result = Curl_http_proxy_create_tunnel_request(&req, cf, data, + proxy_ctx->dest, + PROXY_HTTP_V3, + (bool)proxy_ctx->udp_tunnel); + if(result) + goto out; + result = Curl_creader_set_null(data); + if(result) + goto out; + + proxy_h3_submit(&ts->stream_id, cf, data, req, &result); + +out: + if(req) + Curl_http_req_free(req); + if(result) + failf(data, "Failed sending CONNECT to proxy"); + return result; +} + +static CURLcode +h3_proxy_inspect_response(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h3_tunnel_stream *ts) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + proxy_inspect_result res; + CURLcode result; + + result = Curl_http_proxy_inspect_tunnel_response( + cf, data, ts->resp, (bool)proxy_ctx->udp_tunnel, &res); + if(result) + return result; + switch(res) { + case PROXY_INSPECT_OK: + h3_tunnel_go_state(cf, ts, H3_TUNNEL_ESTABLISHED, data, + (bool)proxy_ctx->udp_tunnel); + break; + case PROXY_INSPECT_FAILED: + h3_tunnel_go_state(cf, ts, H3_TUNNEL_FAILED, data, + (bool)proxy_ctx->udp_tunnel); + result = CURLE_COULDNT_CONNECT; + break; + case PROXY_INSPECT_AUTH_RETRY: + h3_tunnel_go_state(cf, ts, H3_TUNNEL_INIT, data, + (bool)proxy_ctx->udp_tunnel); + break; + } + return result; +} + +static CURLcode cf_h3_proxy_quic_connect(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool *done) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_call_data save; + CURLcode result = CURLE_OK; + struct proxy_pkt_io_ctx pktx; + + if(proxy_ctx->connected) { + *done = TRUE; + return CURLE_OK; + } + + /* Connect the sub-chain (UDP via happy eyeballs) */ + if(cf->next && !cf->next->connected) { + result = Curl_conn_cf_connect(cf->next, data, done); + if(result || !*done) + return result; + } + + *done = FALSE; + + if(!proxy_ctx->ngtcp2_ctx) { + result = cf_h3_proxy_ctx_init(cf, data); + if(result) + return result; + } + + /* Initialize pktx AFTER ensuring ngtcp2_ctx exists */ + proxy_pktx_init(&pktx, cf, data); + + CF_DATA_SAVE(save, cf, data); + + if(!proxy_ctx->ngtcp2_ctx->qconn) { + proxy_ctx->ngtcp2_ctx->started_at = *Curl_pgrs_now(data); + if(proxy_ctx->connected) { + *done = TRUE; + goto out; + } + result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx); + /* we do not expect to be able to recv anything yet */ + goto out; + } + + result = proxy_h3_progress_ingress_ngtcp2(cf, data, &pktx); + if(result) + goto out; + + result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx); + if(result) + goto out; + + if(ngtcp2_conn_get_handshake_completed(proxy_ctx->ngtcp2_ctx->qconn)) { + result = proxy_ctx->ngtcp2_ctx->tls_vrfy_result; + if(!result) { + CURL_TRC_CF(data, cf, "peer verified"); + proxy_ctx->connected = TRUE; + *done = TRUE; + connkeep(cf->conn, "HTTP/3 default"); + } + } + +out: + if(proxy_ctx->ngtcp2_ctx->qconn && + ((result == CURLE_RECV_ERROR) || (result == CURLE_SEND_ERROR)) && + ngtcp2_conn_in_draining_period(proxy_ctx->ngtcp2_ctx->qconn)) { + const ngtcp2_ccerr *cerr = + ngtcp2_conn_get_ccerr(proxy_ctx->ngtcp2_ctx->qconn); + + result = CURLE_COULDNT_CONNECT; + if(cerr) { + CURL_TRC_CF(data, cf, "connect error, type=%d, code=%" + PRIu64, + cerr->type, (uint64_t)cerr->error_code); + switch(cerr->type) { + case NGTCP2_CCERR_TYPE_VERSION_NEGOTIATION: + CURL_TRC_CF(data, cf, "error in version negotiation"); + break; + default: + if(cerr->error_code >= NGTCP2_CRYPTO_ERROR) { + CURL_TRC_CF(data, cf, "crypto error, tls alert=%u", + (unsigned int)(cerr->error_code & 0xffU)); + } + else if(cerr->error_code == NGTCP2_CONNECTION_REFUSED) { + CURL_TRC_CF(data, cf, "connection refused by server"); + /* When a QUIC server instance is shutting down, it may send us a + * CONNECTION_CLOSE with this code right away. We want + * to keep on trying in this case. */ + result = CURLE_WEIRD_SERVER_REPLY; + } + } + } + } + +#ifdef CURLVERBOSE + if(result) { + bool is_ipv6; + struct ip_quadruple ip; + if(!Curl_conn_cf_get_ip_info(cf->next, data, &is_ipv6, &ip)) + infof(data, "QUIC connect to %s port %u failed: %s", + ip.remote_ip, ip.remote_port, curl_easy_strerror(result)); + } +#endif + if(!result && proxy_ctx->ngtcp2_ctx->qconn) { + result = check_and_set_expiry_ngtcp2(cf, data, &pktx); + } + if(result || *done) + CURL_TRC_CF(data, cf, "connect -> %d, done=%d", result, *done); + CF_DATA_RESTORE(cf, save); + return result; +} + +static CURLcode H3_CONNECT(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h3_tunnel_stream *ts) +{ + struct cf_h3_proxy_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + + DEBUGASSERT(ts); + DEBUGASSERT(ts->authority); + + do { + switch(ts->state) { + case H3_TUNNEL_INIT: + CURL_TRC_CF(data, cf, "[0] CONNECT start for %s", ts->authority); + result = h3_submit_CONNECT(cf, data, ts); + if(result) + goto out; + h3_tunnel_go_state(cf, ts, H3_TUNNEL_CONNECT, data, + (bool)ctx->udp_tunnel); + + result = proxy_h3_progress_egress_ngtcp2(cf, data, NULL); + if(result) + goto out; + FALLTHROUGH(); + + case H3_TUNNEL_CONNECT: + /* Non-blocking: call ingress/egress once and return. + * The multi interface will call us again when ready. */ + result = proxy_h3_progress_ingress_ngtcp2(cf, data, NULL); + if(result) + goto out; + result = proxy_h3_progress_egress_ngtcp2(cf, data, NULL); + if(result && result != CURLE_AGAIN) { + h3_tunnel_go_state(cf, ts, H3_TUNNEL_FAILED, data, + (bool)ctx->udp_tunnel); + goto out; + } + + if(ts->has_final_response) { + h3_tunnel_go_state(cf, ts, H3_TUNNEL_RESPONSE, data, + (bool)ctx->udp_tunnel); + } + else { + /* Not done yet, return and let multi interface call us again */ + result = CURLE_OK; + goto out; + } + FALLTHROUGH(); + + case H3_TUNNEL_RESPONSE: + DEBUGASSERT(ts->has_final_response); + result = h3_proxy_inspect_response(cf, data, ts); + if(result) + goto out; + ctx->connected = TRUE; + break; + + case H3_TUNNEL_ESTABLISHED: + return CURLE_OK; + + case H3_TUNNEL_FAILED: + return CURLE_RECV_ERROR; + + default: + break; + } + + } while(ts->state == H3_TUNNEL_INIT); + +out: + if((result && (result != CURLE_AGAIN)) || ctx->tunnel.closed) + h3_tunnel_go_state(cf, ts, H3_TUNNEL_FAILED, data, (bool)ctx->udp_tunnel); + return result; +} + +static CURLcode +cf_h3_proxy_connect(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool *done) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_call_data save = {0}; + CURLcode result = CURLE_OK; + timediff_t check; + struct h3_tunnel_stream *ts = &proxy_ctx->tunnel; + bool data_saved = FALSE; + + /* Curl_cft_http_proxy --> Curl_cft_h3_proxy --> HAPPY-EYEBALLS --> UDP */ + if(cf->connected) { + *done = TRUE; + return CURLE_OK; + } + + *done = FALSE; + + check = Curl_timeleft_ms(data); + if(check <= 0) { + failf(data, "Proxy CONNECT aborted due to timeout"); + result = CURLE_OPERATION_TIMEDOUT; + goto out; + } + + result = cf_h3_proxy_quic_connect(cf, data, done); + if(*done != TRUE) + goto out; + + CF_DATA_SAVE(save, cf, data); + data_saved = TRUE; + + /* At this point the QUIC is connected, but the proxy isn't connected */ + *done = FALSE; + + result = H3_CONNECT(cf, data, ts); + +out: + *done = (result == CURLE_OK) && (ts->state == H3_TUNNEL_ESTABLISHED); + if(*done) { + cf->connected = TRUE; + /* The real request will follow the CONNECT, reset request partially */ + Curl_req_soft_reset(&data->req, data); + Curl_client_reset(data); + } + + if(data_saved) + CF_DATA_RESTORE(cf, save); + return result; +} + +static CURLcode h3_proxy_data_pause(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool pause) +{ + (void)cf; + if(!pause) { + /* unpaused. make it run again right away */ + Curl_multi_mark_dirty(data); + } + return CURLE_OK; +} + +static void h3_proxy_data_done(struct Curl_cfilter *cf, struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct h3_proxy_stream_ctx *stream; + + if(!ctx) + return; + + stream = H3_PROXY_STREAM_CTX(ctx, data); + if(stream) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] easy handle is done", + stream->id); + cf_ngtcp2_proxy_stream_close(cf, data, stream); + Curl_uint32_hash_remove(&ctx->streams, data->mid); + if(!Curl_uint32_hash_count(&ctx->streams)) + cf_ngtcp2_proxy_setup_keep_alive(cf, data); + } +} + +static CURLcode cf_h3_proxy_cntrl(struct Curl_cfilter *cf, + struct Curl_easy *data, + int event, int arg1, void *arg2) +{ + struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx; + struct cf_call_data save; + CURLcode result = CURLE_OK; + + CF_DATA_SAVE(save, cf, data); + + (void)arg1; + (void)arg2; + switch(event) { + case CF_CTRL_DATA_SETUP: + break; + case CF_CTRL_DATA_PAUSE: + result = h3_proxy_data_pause(cf, data, (arg1 != 0)); + break; + case CF_CTRL_DATA_DONE: + h3_proxy_data_done(cf, data); + break; + case CF_CTRL_DATA_DONE_SEND: { + struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx; + struct h3_proxy_stream_ctx *stream = NULL; + if(ctx) { + stream = H3_PROXY_STREAM_CTX(ctx, data); + if(stream && !stream->send_closed && + (H3_STREAM_ID(stream) != proxy_ctx->tunnel.stream_id)) { + stream->send_closed = TRUE; + stream->upload_left = Curl_bufq_len(&stream->sendbuf) - + stream->sendbuf_len_in_flight; + (void)nghttp3_conn_resume_stream(ctx->h3conn, H3_STREAM_ID(stream)); + } + } + break; + } + case CF_CTRL_CONN_INFO_UPDATE: + if(!cf->sockindex && cf->connected) { + cf->conn->httpversion_seen = 30; + Curl_conn_set_multiplex(cf->conn); + } + break; + default: + break; + } + + CF_DATA_RESTORE(cf, save); + return result; +} + +static void cf_h3_proxy_destroy(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *ctx = cf->ctx; + + if(ctx) { + /* Clean up the ngtcp2 context properly */ + if(ctx->ngtcp2_ctx) { + CURL_TRC_CF(data, cf, "cf_ngtcp2_proxy_ctx_close()"); + cf_ngtcp2_proxy_close(cf, data); + cf_ngtcp2_proxy_ctx_free(ctx->ngtcp2_ctx); + ctx->ngtcp2_ctx = NULL; + } + cf_h3_proxy_ctx_free(ctx); + cf->ctx = NULL; + } +} + +static void cf_h3_proxy_close(struct Curl_cfilter *cf, struct Curl_easy *data) +{ + struct cf_h3_proxy_ctx *ctx = cf->ctx; + + if(ctx) { + if(ctx->ngtcp2_ctx) { + cf_ngtcp2_proxy_close(cf, data); + cf_ngtcp2_proxy_ctx_free(ctx->ngtcp2_ctx); + ctx->ngtcp2_ctx = NULL; + } + cf_h3_proxy_ctx_clear(ctx); + cf->connected = FALSE; + } + + if(cf->next) + cf->next->cft->do_close(cf->next, data); +} + +static CURLcode cf_h3_proxy_shutdown(struct Curl_cfilter *cf, + struct Curl_easy *data, bool *done) +{ + return cf_ngtcp2_proxy_shutdown(cf, data, done); +} + +struct Curl_cftype Curl_cft_h3_proxy = { + "H3-PROXY", + CF_TYPE_IP_CONNECT | CF_TYPE_PROXY, + CURL_LOG_LVL_NONE, + cf_h3_proxy_destroy, + cf_h3_proxy_connect, + cf_h3_proxy_close, + cf_h3_proxy_shutdown, + cf_h3_proxy_adjust_pollset, + cf_h3_proxy_data_pending, + cf_h3_proxy_send, + cf_h3_proxy_recv, + cf_h3_proxy_cntrl, + cf_h3_proxy_is_alive, + Curl_cf_def_conn_keep_alive, + cf_h3_proxy_query, +}; + +CURLcode Curl_cf_h3_proxy_insert_after(struct Curl_cfilter *cf_at, + struct Curl_easy *data, + struct Curl_peer *dest, + bool udp_tunnel) +{ + struct Curl_cfilter *cf = NULL; + struct cf_h3_proxy_ctx *ctx; + CURLcode result = CURLE_OUT_OF_MEMORY; + (void)data; + + ctx = curlx_calloc(1, sizeof(*ctx)); + if(!ctx) + goto out; + Curl_peer_link(&ctx->dest, dest); + ctx->udp_tunnel = udp_tunnel; + + result = Curl_cf_create(&cf, &Curl_cft_h3_proxy, ctx); + if(result) + goto out; + + /* H3-PROXY uses the UDP socket created by happy eyeballs below it. + Curl_conn_cf_insert_after chains the existing sub-filters, i.e. + "HAPPY-EYEBALLS -> UDP" as cf->next of H3-PROXY. */ + Curl_conn_cf_insert_after(cf_at, cf); + +out: + if(result) { + if(cf) + Curl_conn_cf_discard_chain(&cf, data); + else if(ctx) + cf_h3_proxy_ctx_free(ctx); + } + return result; +} + +#endif + +/* Do not leak this filter's call_data accessor in unity builds. */ +#undef CF_CTX_CALL_DATA diff --git a/lib/cf-h3-proxy.h b/lib/cf-h3-proxy.h new file mode 100644 index 0000000000..c1d5dd1511 --- /dev/null +++ b/lib/cf-h3-proxy.h @@ -0,0 +1,42 @@ +#ifndef HEADER_CURL_H3_PROXY_H +#define HEADER_CURL_H3_PROXY_H +/*************************************************************************** + * _ _ ____ _ + * 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" + +#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY) && \ + defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) && \ + defined(USE_NGTCP2) && defined(USE_OPENSSL) + +CURLcode Curl_cf_h3_proxy_insert_after(struct Curl_cfilter *cf_at, + struct Curl_easy *data, + struct Curl_peer *dest, + bool udp_tunnel); + +extern struct Curl_cftype Curl_cft_h3_proxy; + +#endif + +#endif /* HEADER_CURL_H3_PROXY_H */ diff --git a/lib/cf-ip-happy.c b/lib/cf-ip-happy.c index 17b2821b08..965415d458 100644 --- a/lib/cf-ip-happy.c +++ b/lib/cf-ip-happy.c @@ -1023,3 +1023,28 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at, Curl_conn_cf_insert_after(cf_at, cf); return CURLE_OK; } + +#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \ + defined(USE_PROXY_HTTP3) +CURLcode cf_ip_happy_quic_udp_insert_after(struct Curl_cfilter *cf_at, + struct Curl_easy *data) +{ + /* For H3 proxy: create happy eyeballs that races IPv4/IPv6 using raw + UDP sockets with TRNSPRT_QUIC transport. Using TRNSPRT_QUIC causes + cf_udp_connect() to call cf_udp_setup_quic() which connects the + socket to the peer address, making send() work without an explicit + destination. We use Curl_cf_udp_create (not Curl_cf_quic_create) + because H3-PROXY manages its own ngtcp2 QUIC stack on top. */ + struct Curl_cfilter *cf; + CURLcode result; + + DEBUGASSERT(cf_at); + result = cf_ip_happy_create(&cf, data, cf_at->conn, + Curl_cf_udp_create, TRNSPRT_QUIC); + if(result) + return result; + + Curl_conn_cf_insert_after(cf_at, cf); + return CURLE_OK; +} +#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */ diff --git a/lib/cf-ip-happy.h b/lib/cf-ip-happy.h index 547ee4b4ac..5805d6397c 100644 --- a/lib/cf-ip-happy.h +++ b/lib/cf-ip-happy.h @@ -52,6 +52,15 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, uint8_t transport); +#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \ + defined(USE_PROXY_HTTP3) +/* For H3 proxy: create happy eyeballs that races IPv4/IPv6 using raw UDP + sockets with TRNSPRT_QUIC transport so the socket is connected to the + proxy peer. H3-PROXY manages its own ngtcp2 QUIC stack on top. */ +CURLcode cf_ip_happy_quic_udp_insert_after(struct Curl_cfilter *cf_at, + struct Curl_easy *data); +#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */ + extern struct Curl_cftype Curl_cft_ip_happy; #endif /* HEADER_CURL_IP_HAPPY_H */ diff --git a/lib/connect.c b/lib/connect.c index e74bda5dfb..64ec2ff941 100644 --- a/lib/connect.c +++ b/lib/connect.c @@ -63,6 +63,7 @@ #include "curlx/inet_ntop.h" #include "curlx/strparse.h" #include "vtls/vtls.h" /* for vtls cfilters */ +#include "vquic/vquic.h" /* for QUIC cfilters */ #include "progress.h" #include "conncache.h" #include "multihandle.h" @@ -341,6 +342,66 @@ struct cf_setup_ctx { uint8_t transport; }; +#ifndef CURL_DISABLE_PROXY +static CURLcode cf_setup_add_http_proxy(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct cf_setup_ctx *ctx) +{ + CURLcode result = CURLE_OK; +#ifndef USE_SSL + (void)cf; + (void)data; + (void)ctx; +#else + /* Skipping the Curl_conn_is_ssl check because SSL is a part of QUIC + For CURLPROXY_HTTPS and CURLPROXY_HTTPS2: + Curl_cft_setup --> Curl_cft_ssl --> Curl_cft_http_proxy --> ... + For CURLPROXY_HTTPS3: + Curl_cft_setup --> Curl_cft_http3 --> Curl_cft_http_proxy --> ... */ + if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy) { + if(!IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) { + result = Curl_cf_ssl_proxy_insert_after(cf, data); + if(result) + return result; + } + } + else { + if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype) + && !Curl_conn_is_ssl(cf->conn, cf->sockindex) + && !IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) { + result = Curl_cf_ssl_proxy_insert_after(cf, data); + if(result) + return result; + } + } +#endif /* USE_SSL */ + +#ifndef CURL_DISABLE_HTTP + if(cf->conn->bits.tunnel_proxy) { + struct Curl_peer *dest; /* where HTTP should tunnel to */ + bool udp_tun = false; + dest = Curl_conn_get_destination(cf->conn, cf->sockindex); + /* Use CONNECT-UDP only for explicit HTTP/3-only target tunnels. + Do not derive this from proxy transport (for example HTTPS3 proxy). */ + if(data->state.http_neg.wanted == CURL_HTTP_V3x) { +#ifdef USE_PROXY_HTTP3 + udp_tun = TRUE; +#else + failf(data, "HTTP/3 proxy tunnel support not built-in"); + return CURLE_NOT_BUILT_IN; +#endif /* USE_PROXY_HTTP3 */ + } + result = Curl_cf_http_proxy_insert_after(cf, data, dest, + cf->conn->http_proxy.proxytype, + udp_tun); + if(result) + return result; + } +#endif /* !CURL_DISABLE_HTTP */ + return result; +} +#endif /* !CURL_DISABLE_PROXY */ + static CURLcode cf_setup_connect(struct Curl_cfilter *cf, struct Curl_easy *data, bool *done) @@ -364,7 +425,35 @@ connect_sub_chain: } if(ctx->state < CF_SETUP_CNNCT_EYEBALLS) { - result = cf_ip_happy_insert_after(cf, data, ctx->transport); +#ifndef CURL_DISABLE_PROXY +#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \ + defined(USE_PROXY_HTTP3) + if(IS_QUIC_PROXY(cf->conn->http_proxy.proxytype) && + cf->conn->bits.tunnel_proxy) { + /* For HTTPS3 proxy tunnels, H3-PROXY manages the QUIC connection + on top of the UDP socket. Let happy eyeballs race IPv4/IPv6 using + QUIC-transport UDP sockets so the socket is connected to the + proxy peer and H3-PROXY can send directly via send(). + Filter chains: + H1/H2 target (CONNECT over QUIC): + SETUP --> HTTP/1.1 or HTTP/2 --> SSL --> HTTP-PROXY --> + H3-PROXY --> HAPPY-EYEBALLS --> UDP + H3 target (MASQUE CONNECT-UDP over QUIC): + SETUP --> HTTP/3 --> CAPSULE --> HTTP-PROXY --> + H3-PROXY --> HAPPY-EYEBALLS --> UDP */ + result = cf_ip_happy_quic_udp_insert_after(cf, data); + } + /* When tunneling QUIC through an HTTP proxy (CONNECT-UDP), + the underlying conn to the proxy is TCP. */ + else +#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */ + if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy + && !IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) + result = cf_ip_happy_insert_after(cf, data, TRNSPRT_TCP); + else +#endif /* !CURL_DISABLE_PROXY */ + result = cf_ip_happy_insert_after(cf, data, ctx->transport); + if(result) return result; ctx->state = CF_SETUP_CNNCT_EYEBALLS; @@ -402,25 +491,9 @@ connect_sub_chain: } if(ctx->state < CF_SETUP_CNNCT_HTTP_PROXY && cf->conn->bits.httpproxy) { -#ifdef USE_SSL - if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype) && - !Curl_conn_is_ssl(cf->conn, cf->sockindex)) { - result = Curl_cf_ssl_proxy_insert_after(cf, data); - if(result) - return result; - } -#endif /* USE_SSL */ - -#ifndef CURL_DISABLE_HTTP - if(cf->conn->bits.tunnel_proxy) { - struct Curl_peer *dest; /* where HTTP should tunnel to */ - dest = Curl_conn_get_destination(cf->conn, cf->sockindex); - result = Curl_cf_http_proxy_insert_after( - cf, data, dest, cf->conn->http_proxy.proxytype); - if(result) - return result; - } -#endif /* !CURL_DISABLE_HTTP */ + result = cf_setup_add_http_proxy(cf, data, ctx); + if(result) + return result; ctx->state = CF_SETUP_CNNCT_HTTP_PROXY; if(!cf->next || !cf->next->connected) goto connect_sub_chain; @@ -445,21 +518,41 @@ connect_sub_chain: goto connect_sub_chain; } - if(ctx->state < CF_SETUP_CNNCT_SSL) { -#ifdef USE_SSL - if((ctx->ssl_mode == CURL_CF_SSL_ENABLE || - (ctx->ssl_mode != CURL_CF_SSL_DISABLE && - cf->conn->scheme->flags & PROTOPT_SSL)) && /* we want SSL */ - !Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */ - result = Curl_cf_ssl_insert_after(cf, data); + /* Adding Curl_cf_quic_insert_after() because now we + need the next filter to be QUIC/HTTP/3 (which has SSL) */ +#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \ + defined(USE_PROXY_HTTP3) + if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy && + cf->conn->bits.tunnel_proxy && + (data->state.http_neg.wanted == CURL_HTTP_V3x)) { + if(ctx->state < CF_SETUP_CNNCT_SSL) { + result = Curl_cf_quic_insert_after(cf); if(result) return result; + ctx->state = CF_SETUP_CNNCT_SSL; } -#endif /* USE_SSL */ - ctx->state = CF_SETUP_CNNCT_SSL; if(!cf->next || !cf->next->connected) goto connect_sub_chain; } + else +#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */ + { + if(ctx->state < CF_SETUP_CNNCT_SSL) { +#ifdef USE_SSL + if((ctx->ssl_mode == CURL_CF_SSL_ENABLE || + (ctx->ssl_mode != CURL_CF_SSL_DISABLE && + cf->conn->scheme->flags & PROTOPT_SSL)) /* we want SSL */ + && !Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */ + result = Curl_cf_ssl_insert_after(cf, data); + if(result) + return result; + } +#endif /* USE_SSL */ + ctx->state = CF_SETUP_CNNCT_SSL; + if(!cf->next || !cf->next->connected) + goto connect_sub_chain; + } + } ctx->state = CF_SETUP_DONE; cf->connected = TRUE; diff --git a/lib/curl_config-cmake.h.in b/lib/curl_config-cmake.h.in index 31e94d0691..5c7fdd670b 100644 --- a/lib/curl_config-cmake.h.in +++ b/lib/curl_config-cmake.h.in @@ -712,6 +712,9 @@ ${SIZEOF_TIME_T_CODE} /* if libuv is in use */ #cmakedefine USE_LIBUV 1 +/* if HTTP/3 proxy support is available */ +#cmakedefine USE_PROXY_HTTP3 1 + /* Define to 1 if you have the header file. */ #cmakedefine HAVE_UV_H 1 diff --git a/lib/curl_trc.c b/lib/curl_trc.c index c6115cf7f6..d54c171a54 100644 --- a/lib/curl_trc.c +++ b/lib/curl_trc.c @@ -35,6 +35,7 @@ #include "http_proxy.h" #include "cf-h1-proxy.h" #include "cf-h2-proxy.h" +#include "cf-h3-proxy.h" #include "cf-haproxy.h" #include "cf-https-connect.h" #include "cf-ip-happy.h" @@ -578,6 +579,9 @@ static struct trc_cft_def trc_cfts[] = { { &Curl_cft_h1_proxy, TRC_CT_PROXY }, #ifdef USE_NGHTTP2 { &Curl_cft_h2_proxy, TRC_CT_PROXY }, +#endif +#if defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) + { &Curl_cft_h3_proxy, TRC_CT_PROXY }, #endif { &Curl_cft_http_proxy, TRC_CT_PROXY }, #endif /* !CURL_DISABLE_HTTP */ diff --git a/lib/http.c b/lib/http.c index 5d98aab9d7..c935d4f69f 100644 --- a/lib/http.c +++ b/lib/http.c @@ -1757,6 +1757,12 @@ CURLcode Curl_add_custom_headers(struct Curl_easy *data, 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; } #else (void)is_connect; @@ -2721,7 +2727,11 @@ static CURLcode http_check_new_conn(struct Curl_easy *data) alpn = Curl_conn_get_alpn_negotiated(data, conn); if(alpn && !strcmp("h3", alpn)) { - DEBUGASSERT(Curl_conn_http_version(data, conn) == 30); +#ifndef CURL_DISABLE_PROXY + if((Curl_conn_http_version(data, conn) == 30) || !conn->bits.proxy || + conn->bits.tunnel_proxy) +#endif + DEBUGASSERT(Curl_conn_http_version(data, conn) == 30); info_version = "HTTP/3"; } else if(alpn && !strcmp("h2", alpn)) { @@ -4847,7 +4857,6 @@ struct name_const { size_t namelen; }; -/* keep them sorted by length! */ static const struct name_const H2_NON_FIELD[] = { { STRCONST("Host") }, { STRCONST("Upgrade") }, @@ -4861,10 +4870,8 @@ static bool h2_permissible_field(struct dynhds_entry *e) { size_t i; for(i = 0; i < CURL_ARRAYSIZE(H2_NON_FIELD); ++i) { - if(e->namelen < H2_NON_FIELD[i].namelen) - return TRUE; if(e->namelen == H2_NON_FIELD[i].namelen && - curl_strequal(H2_NON_FIELD[i].name, e->name)) + curl_strnequal(H2_NON_FIELD[i].name, e->name, e->namelen)) return FALSE; } return TRUE; diff --git a/lib/http.h b/lib/http.h index 9c25471d33..ed93d265e3 100644 --- a/lib/http.h +++ b/lib/http.h @@ -83,8 +83,6 @@ char *Curl_checkProxyheaders(struct Curl_easy *data, CURLcode Curl_add_timecondition(struct Curl_easy *data, struct dynbuf *req); CURLcode Curl_add_custom_headers(struct Curl_easy *data, bool is_connect, int httpversion, struct dynbuf *req); -CURLcode Curl_dynhds_add_custom(struct Curl_easy *data, bool is_connect, - struct dynhds *hds); void Curl_http_to_fold(struct dynbuf *bf); diff --git a/lib/http2.c b/lib/http2.c index 9eb1e0aeaa..9e755a0e1d 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -3021,3 +3021,6 @@ char *curl_pushheader_byname(struct curl_pushheaders *h, const char *name) } #endif /* !CURL_DISABLE_HTTP && USE_NGHTTP2 */ + +/* Do not leak this filter's call_data accessor in unity builds. */ +#undef CF_CTX_CALL_DATA diff --git a/lib/http_proxy.c b/lib/http_proxy.c index fd87c1db19..373865272b 100644 --- a/lib/http_proxy.c +++ b/lib/http_proxy.c @@ -33,13 +33,15 @@ #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, - struct dynhds *hds) + bool is_udp, struct dynhds *hds) { struct connectdata *conn = data->conn; struct curl_slist *h[2]; @@ -49,10 +51,12 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data, enum Curl_proxy_use proxy; - if(is_connect) + 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 ? + proxy = (conn->bits.httpproxy && !conn->bits.tunnel_proxy) ? HEADER_PROXY : HEADER_SERVER; switch(proxy) { @@ -72,6 +76,12 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data, 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 */ @@ -166,15 +176,30 @@ 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; +} + CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq, struct Curl_cfilter *cf, struct Curl_easy *data, struct Curl_peer *dest, - int httpversion) + proxy_http_ver ver) { char *authority = NULL; + int httpversion = proxy_http_ver_major(ver); CURLcode result; struct httpreq *req = NULL; @@ -201,7 +226,7 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq, goto out; /* If user is not overriding Host: header, we add for HTTP/1.x */ - if(httpversion < 20 && + if(ver == PROXY_HTTP_V1 && !Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) { result = Curl_dynhds_cadd(&req->headers, "Host", authority); if(result) @@ -223,14 +248,15 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq, goto out; } - if(httpversion < 20 && + 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, &req->headers); + result = dynhds_add_custom(data, TRUE, httpversion, + FALSE, &req->headers); out: if(result && req) { @@ -242,33 +268,327 @@ out: return result; } +CURLcode Curl_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 = Curl_http_proxy_create_CONNECTUDP(preq, cf, data, dest, ver); + else + result = Curl_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, "connect"); + CURL_TRC_CF(data, cf, "%s", tunnel_type); connect_sub: - result = cf->next->cft->do_connect(cf->next, data, done); - if(result || !*done) - return result; + /* 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 = Curl_conn_cf_get_alpn_negotiated(cf->next, data); + 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, "CONNECT: '%s' negotiated", alpn); + infof(data, "%s: '%s' negotiated", tunnel_type, alpn); else if(!alpn) { /* No ALPN, proxytype rules. Fake ALPN */ - infof(data, "CONNECT: no ALPN negotiated"); + infof(data, "%s: no ALPN negotiated", tunnel_type); switch(ctx->proxytype) { case CURLPROXY_HTTP_1_0: alpn = "http/1.0"; @@ -276,6 +596,9 @@ connect_sub: case CURLPROXY_HTTPS2: alpn = "h2"; break; + case CURLPROXY_HTTPS3: + alpn = "h3"; + break; default: alpn = "http/1.1"; break; @@ -284,7 +607,8 @@ connect_sub: 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); + result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, 10, + (bool)ctx->udp_tunnel); if(result) goto out; } @@ -292,20 +616,32 @@ connect_sub: 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); + 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); + result = Curl_cf_h2_proxy_insert_after(cf, data, ctx->dest, + (bool)ctx->udp_tunnel); if(result) goto out; } -#endif +#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, "CONNECT: negotiated ALPN '%s' not supported", alpn); + failf(data, "%s: negotiated ALPN '%s' not supported", tunnel_type, alpn); result = CURLE_COULDNT_CONNECT; goto out; } @@ -321,6 +657,19 @@ connect_sub: * 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; } @@ -404,7 +753,8 @@ struct Curl_cftype Curl_cft_http_proxy = { CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, struct Curl_peer *dest, - uint8_t proxytype) + uint8_t proxytype, + bool udp_tunnel) { struct Curl_cfilter *cf; struct cf_proxy_ctx *ctx = NULL; @@ -421,6 +771,7 @@ CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, } 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) diff --git a/lib/http_proxy.h b/lib/http_proxy.h index c122aa6dd8..b0becedf03 100644 --- a/lib/http_proxy.h +++ b/lib/http_proxy.h @@ -32,14 +32,47 @@ enum Curl_proxy_use { HEADER_SERVER, /* direct to server */ HEADER_PROXY, /* regular request to proxy */ - HEADER_CONNECT /* sending CONNECT to a proxy */ + HEADER_CONNECT, /* sending CONNECT to a proxy */ + HEADER_CONNECT_UDP /* sending CONNECT-UDP to a proxy */ }; +/* HTTP version for proxy tunnel request creation */ +typedef enum { + PROXY_HTTP_V1 = 1, + PROXY_HTTP_V2 = 2, + PROXY_HTTP_V3 = 3 +} proxy_http_ver; + +/* Result from inspecting a proxy tunnel response */ +typedef enum { + PROXY_INSPECT_OK, /* Tunnel established */ + PROXY_INSPECT_FAILED, /* Tunnel failed */ + PROXY_INSPECT_AUTH_RETRY /* Retry with auth */ +} proxy_inspect_result; + CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq, struct Curl_cfilter *cf, struct Curl_easy *data, struct Curl_peer *dest, - int httpversion); + proxy_http_ver ver); +CURLcode Curl_http_proxy_create_CONNECTUDP(struct httpreq **preq, + struct Curl_cfilter *cf, + struct Curl_easy *data, + struct Curl_peer *dest, + proxy_http_ver ver); + +/* Create CONNECT or CONNECT-UDP request */ +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); + +/* Inspect tunnel response for H2/H3 proxy (capsule-protocol, auth) */ +struct http_resp; +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); /* Default proxy timeout in milliseconds */ #define PROXY_TIMEOUT (3600 * 1000) @@ -47,13 +80,17 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq, CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, struct Curl_peer *dest, - uint8_t proxytype); + uint8_t proxytype, + bool udp_tunnel); extern struct Curl_cftype Curl_cft_http_proxy; #endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ #define IS_HTTPS_PROXY(t) (((t) == CURLPROXY_HTTPS) || \ - ((t) == CURLPROXY_HTTPS2)) + ((t) == CURLPROXY_HTTPS2) || \ + ((t) == CURLPROXY_HTTPS3)) + +#define IS_QUIC_PROXY(t) ((t) == CURLPROXY_HTTPS3) #endif /* HEADER_CURL_HTTP_PROXY_H */ diff --git a/lib/peer.c b/lib/peer.c index 43e5aef0f0..5dd3aad372 100644 --- a/lib/peer.c +++ b/lib/peer.c @@ -536,6 +536,37 @@ out: #define UNIX_SOCKET_PREFIX "localhost" #endif +CURLcode Curl_scheme_to_proxytype(struct Curl_easy *data, + const char *scheme, + uint8_t *proxytype, const char *url) +{ + if(!scheme) + return CURLE_OK; + + if(curl_strequal("https", scheme)) { + if(*proxytype != CURLPROXY_HTTPS2 && *proxytype != CURLPROXY_HTTPS3) + *proxytype = CURLPROXY_HTTPS; + } + else if(curl_strequal("socks5h", scheme)) + *proxytype = CURLPROXY_SOCKS5_HOSTNAME; + else if(curl_strequal("socks5", scheme)) + *proxytype = CURLPROXY_SOCKS5; + else if(curl_strequal("socks4a", scheme)) + *proxytype = CURLPROXY_SOCKS4A; + else if(curl_strequal("socks4", scheme) || curl_strequal("socks", scheme)) + *proxytype = CURLPROXY_SOCKS4; + else if(curl_strequal("http", scheme)) { + if(*proxytype != CURLPROXY_HTTP_1_0) + *proxytype = CURLPROXY_HTTP; + } + else { + /* Any other xxx:// reject! */ + failf(data, "Unsupported proxy scheme for \'%s\'", url); + return CURLE_COULDNT_CONNECT; + } + return CURLE_OK; +} + CURLcode Curl_peer_from_proxy_url(CURLU *uh, struct Curl_easy *data, const char *url, @@ -570,6 +601,7 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh, break; case CURLPROXY_HTTPS: case CURLPROXY_HTTPS2: + case CURLPROXY_HTTPS3: pp.scheme = &Curl_scheme_https; break; case CURLPROXY_SOCKS4: @@ -592,29 +624,9 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh, } else { pp.scheme = Curl_get_scheme(scheme); - if(pp.scheme == &Curl_scheme_https) { - proxytype = (proxytype != CURLPROXY_HTTPS2) ? - CURLPROXY_HTTPS : CURLPROXY_HTTPS2; - } - else if(pp.scheme == &Curl_scheme_socks5h) - proxytype = CURLPROXY_SOCKS5_HOSTNAME; - else if(pp.scheme == &Curl_scheme_socks5) - proxytype = CURLPROXY_SOCKS5; - else if(pp.scheme == &Curl_scheme_socks4a) - proxytype = CURLPROXY_SOCKS4A; - else if((pp.scheme == &Curl_scheme_socks4) || - (pp.scheme == &Curl_scheme_socks)) - proxytype = CURLPROXY_SOCKS4; - else if(pp.scheme == &Curl_scheme_http) { - proxytype = (uint8_t)((proxytype != CURLPROXY_HTTP_1_0) ? - CURLPROXY_HTTP : CURLPROXY_HTTP_1_0); - } - else { - /* Any other xxx:// reject! */ - failf(data, "Unsupported proxy scheme for \'%s\'", url); - result = CURLE_COULDNT_CONNECT; + result = Curl_scheme_to_proxytype(data, scheme, &proxytype, url); + if(result) goto out; - } } DEBUGASSERT(pp.scheme); diff --git a/lib/peer.h b/lib/peer.h index daa01db8ff..7946735a23 100644 --- a/lib/peer.h +++ b/lib/peer.h @@ -94,6 +94,11 @@ CURLcode Curl_peer_from_connect_to(struct Curl_easy *data, #ifndef CURL_DISABLE_PROXY +CURLcode Curl_scheme_to_proxytype(struct Curl_easy *data, + const char *scheme, + uint8_t *proxytype, + const char *url); + CURLcode Curl_peer_from_proxy_url(CURLU *uh, struct Curl_easy *data, const char *url, diff --git a/lib/setopt.c b/lib/setopt.c index 2e08a310eb..e67a3c8beb 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -1042,8 +1042,12 @@ static CURLcode setopt_long_proxy(struct Curl_easy *data, CURLoption option, case CURLOPT_PROXYAUTH: return httpauth(data, TRUE, (unsigned long)arg); case CURLOPT_PROXYTYPE: - if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_SOCKS5_HOSTNAME)) + if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_HTTPS3)) return CURLE_BAD_FUNCTION_ARGUMENT; +#ifndef USE_PROXY_HTTP3 + if(arg == CURLPROXY_HTTPS3) + return CURLE_NOT_BUILT_IN; +#endif s->proxytype = (unsigned char)arg; break; case CURLOPT_SOCKS5_AUTH: diff --git a/lib/url.c b/lib/url.c index 796d35e229..93a5f14f07 100644 --- a/lib/url.c +++ b/lib/url.c @@ -99,6 +99,7 @@ #include "headers.h" #include "curlx/strerr.h" #include "curlx/strparse.h" +#include "peer.h" /* Now for the protocols */ #include "ftp.h" @@ -1316,7 +1317,12 @@ static struct connectdata *allocate_conn(struct Curl_easy *data) #endif conn->ip_version = data->set.ipver; conn->bits.connect_only = (bool)data->set.connect_only; - conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */ +#ifndef CURL_DISABLE_PROXY + if(conn->http_proxy.proxytype == CURLPROXY_HTTPS3) + conn->transport_wanted = TRNSPRT_QUIC; + else +#endif + conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */ /* Store the local bind parameters that will be used for this connection */ if(data->set.str[STRING_DEVICE]) { @@ -1793,6 +1799,7 @@ static CURLcode parse_proxy(struct Curl_easy *data, { char *proxyuser = NULL; char *proxypasswd = NULL; + char *scheme = NULL; CURLcode result = CURLE_OK; /* Set the start proxy type for url scheme guessing */ uint8_t proxytype = for_pre_proxy ? CURLPROXY_SOCKS4 : data->set.proxytype; @@ -1807,7 +1814,21 @@ static CURLcode parse_proxy(struct Curl_easy *data, these made up ones for proxies. Guess scheme for URLs without it. */ uc = curl_url_set(uhp, CURLUPART_URL, proxy, CURLU_NON_SUPPORT_SCHEME | CURLU_GUESS_SCHEME); - if(uc) { + if(!uc) { + /* parsed okay as a URL - only update proxytype when scheme was explicit */ + uc = curl_url_get(uhp, CURLUPART_SCHEME, &scheme, CURLU_NO_GUESS_SCHEME); + if(!uc) { + result = Curl_scheme_to_proxytype(data, scheme, &proxytype, proxy); + if(result) + goto error; + } + else if(uc != CURLUE_NO_SCHEME) { + result = CURLE_OUT_OF_MEMORY; + goto error; + } + /* else: no explicit scheme, keep the configured proxytype */ + } + else { failf(data, "Unsupported proxy syntax in \'%s\': %s", proxy, curl_url_strerror(uc)); result = CURLE_COULDNT_RESOLVE_PROXY; @@ -1824,6 +1845,7 @@ static CURLcode parse_proxy(struct Curl_easy *data, case CURLPROXY_HTTP_1_0: case CURLPROXY_HTTPS: case CURLPROXY_HTTPS2: + case CURLPROXY_HTTPS3: if(for_pre_proxy) { failf(data, "Unsupported pre-proxy type for \'%s\'", proxy); result = CURLE_COULDNT_RESOLVE_PROXY; @@ -1878,6 +1900,7 @@ static CURLcode parse_proxy(struct Curl_easy *data, proxyinfo->proxytype = proxytype; error: + curlx_free(scheme); curlx_free(proxyuser); curlx_free(proxypasswd); curl_url_cleanup(uhp); diff --git a/lib/version.c b/lib/version.c index b3b0a46abb..d5870333ab 100644 --- a/lib/version.c +++ b/lib/version.c @@ -491,6 +491,9 @@ static const struct feat features_table[] = { #ifdef USE_NTLM FEATURE("NTLM", NULL, CURL_VERSION_NTLM), #endif +#ifdef USE_PROXY_HTTP3 + FEATURE("PROXY-HTTP3", NULL, 0), +#endif #ifdef USE_LIBPSL FEATURE("PSL", NULL, CURL_VERSION_PSL), #endif diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index fb7fd61889..6cafda2da0 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -72,6 +72,7 @@ #define QUIC_MAX_STREAMS (256 * 1024) #define QUIC_HANDSHAKE_TIMEOUT (10 * NGTCP2_SECONDS) +#define QUIC_TUNNEL_INBUF_SIZE (64 * 1024) /* We announce a small window size in transport param to the server, * and grow that immediately to max when no rate limit is in place. @@ -95,6 +96,7 @@ #define H3_STREAM_SEND_BUFFER_MAX (10 * 1024 * 1024) #define H3_STREAM_SEND_CHUNKS \ (H3_STREAM_SEND_BUFFER_MAX / H3_STREAM_CHUNK_SIZE) +#define QUIC_TUNNEL_INGRESS_PKT_LIMIT 1000 /* * Store ngtcp2 version info in this buffer. @@ -139,6 +141,8 @@ struct cf_ngtcp2_ctx { is accepted by peer */ CURLcode tls_vrfy_result; /* result of TLS peer verification */ int qlogfd; + unsigned char *tunnel_inbuf; /* ingress buffer for tunneled packets */ + size_t tunnel_inbuf_len; BIT(initialized); BIT(tls_handshake_complete); /* TLS handshake is done */ BIT(use_earlydata); /* Using 0RTT data */ @@ -156,6 +160,8 @@ static void cf_ngtcp2_ctx_init(struct cf_ngtcp2_ctx *ctx) { DEBUGASSERT(!ctx->initialized); ctx->qlogfd = -1; + ctx->tunnel_inbuf = NULL; + ctx->tunnel_inbuf_len = 0; ctx->version = NGTCP2_PROTO_VER_MAX; Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE, H3_STREAM_POOL_SPARES); @@ -173,6 +179,8 @@ static void cf_ngtcp2_ctx_free(struct cf_ngtcp2_ctx *ctx) curlx_dyn_free(&ctx->scratch); Curl_uint32_hash_destroy(&ctx->streams); Curl_ssl_peer_cleanup(&ctx->peer); + curlx_safefree(ctx->tunnel_inbuf); + ctx->tunnel_inbuf_len = 0; } curlx_free(ctx); } @@ -493,7 +501,7 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx, static CURLcode init_ngh3_conn(struct Curl_cfilter *cf, struct Curl_easy *data); -static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data) +static int cb_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data) { struct Curl_cfilter *cf = user_data; struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL; @@ -863,7 +871,7 @@ static ngtcp2_callbacks ng_callbacks = { ngtcp2_crypto_client_initial_cb, NULL, /* recv_client_initial */ ngtcp2_crypto_recv_crypto_data_cb, - cf_ngtcp2_handshake_completed, + cb_ngtcp2_handshake_completed, NULL, /* recv_version_negotiation */ ngtcp2_crypto_encrypt_cb, ngtcp2_crypto_decrypt_cb, @@ -982,6 +990,11 @@ static CURLcode cf_ngtcp2_adjust_pollset(struct Curl_cfilter *cf, if(!ctx->qconn) return CURLE_OK; + if(ctx->q.sockfd == CURL_SOCKET_BAD) { + /* Tunneled QUIC, no direct socket - delegate to next filter */ + return cf->next->cft->adjust_pollset(cf->next, data, ps); + } + Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send); if(!want_send && !Curl_bufq_is_empty(&ctx->q.sendbuf)) want_send = TRUE; @@ -1904,8 +1917,72 @@ static CURLcode cf_progress_ingress(struct Curl_cfilter *cf, rctx.pktx = pktx; rctx.pkt_count = 0; - return vquic_recv_packets(cf, data, &ctx->q, 1000, + + if(ctx->q.sockfd != CURL_SOCKET_BAD) { + /* Direct UDP socket (via happy eyeballs) */ + return vquic_recv_packets(cf, data, &ctx->q, 1000, cf_ngtcp2_recv_pkts, &rctx); + } + else { + /* Tunneled QUIC (CONNECT-UDP through proxy) */ + unsigned char *buf; + size_t max_udp_payload = QUIC_TUNNEL_INBUF_SIZE; + size_t pkt_limit = QUIC_TUNNEL_INGRESS_PKT_LIMIT; + size_t nread; + struct sockaddr_storage remote_addr; + socklen_t remote_addrlen; + + if(ctx->qconn) { + size_t max_path_payload; + max_path_payload = + ngtcp2_conn_get_path_max_tx_udp_payload_size(ctx->qconn); + if(max_path_payload > max_udp_payload) + max_udp_payload = max_path_payload; + } + + if(ctx->tunnel_inbuf_len < max_udp_payload) { + unsigned char *newbuf = + (unsigned char *)curlx_realloc(ctx->tunnel_inbuf, max_udp_payload); + if(!newbuf) + return CURLE_OUT_OF_MEMORY; + ctx->tunnel_inbuf = newbuf; + ctx->tunnel_inbuf_len = max_udp_payload; + } + buf = ctx->tunnel_inbuf; + + while(pkt_limit--) { + result = Curl_conn_cf_recv(cf->next, data, (char *)buf, + ctx->tunnel_inbuf_len, &nread); + if(result == CURLE_AGAIN) { + /* no more data available at the moment */ + return CURLE_OK; + } + if(result) { + CURL_TRC_CF(data, cf, "ingress, recv from tunnel failed: %d", + result); + return result; + } + if(nread == 0) { + /* tunnel closed */ + return CURLE_OK; + } + + memcpy(&remote_addr, ctx->connected_path.remote.addr, + ctx->connected_path.remote.addrlen); + remote_addrlen = (socklen_t)ctx->connected_path.remote.addrlen; + result = cf_ngtcp2_recv_pkts(buf, nread, nread, &remote_addr, + remote_addrlen, 0, &rctx); + if(result) + return result; + + if(!ctx->q.got_first_byte) { + ctx->q.got_first_byte = TRUE; + ctx->q.first_byte_at = ctx->q.last_op; + } + ctx->q.last_io = ctx->q.last_op; + } + return CURLE_OK; + } } /** @@ -2189,6 +2266,7 @@ static void cf_ngtcp2_ctx_close(struct cf_ngtcp2_ctx *ctx) } ctx->qlogfd = -1; Curl_vquic_tls_cleanup(&ctx->tls); + Curl_ssl_peer_cleanup(&ctx->peer); vquic_ctx_free(&ctx->q); if(ctx->h3conn) { nghttp3_conn_del(ctx->h3conn); @@ -2220,6 +2298,12 @@ static CURLcode cf_ngtcp2_shutdown(struct Curl_cfilter *cf, return CURLE_OK; } + if(!cf->next) { + Curl_bufq_reset(&ctx->q.sendbuf); + *done = TRUE; + return CURLE_OK; + } + CF_DATA_SAVE(save, cf, data); *done = FALSE; pktx_init(&pktx, cf, data); @@ -2648,30 +2732,81 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf, if(result) return result; - if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL)) - return CURLE_QUIC_CONNECT_ERROR; - ctx->q.local_addrlen = sizeof(ctx->q.local_addr); - rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr, - &ctx->q.local_addrlen); - if(rv == -1) - return CURLE_QUIC_CONNECT_ERROR; + /* Query socket and remote address from sub-chain */ + if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL)) { + /* No direct socket - must be tunneled QUIC (CONNECT-UDP through proxy) */ + ctx->q.sockfd = CURL_SOCKET_BAD; + } - ngtcp2_addr_init(&ctx->connected_path.local, - (struct sockaddr *)&ctx->q.local_addr, - ctx->q.local_addrlen); - ngtcp2_addr_init(&ctx->connected_path.remote, - &sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen); + if(ctx->q.sockfd != CURL_SOCKET_BAD) { + /* Direct UDP socket - get local address for ngtcp2 */ + ctx->q.local_addrlen = sizeof(ctx->q.local_addr); + rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr, + &ctx->q.local_addrlen); + if(rv == -1) + return CURLE_QUIC_CONNECT_ERROR; - rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid, - &ctx->connected_path, - NGTCP2_PROTO_VER_V1, &ng_callbacks, - &ctx->settings, &ctx->transport_params, - Curl_ngtcp2_mem(), cf); - if(rc) - return CURLE_QUIC_CONNECT_ERROR; + ngtcp2_addr_init(&ctx->connected_path.local, + (struct sockaddr *)&ctx->q.local_addr, + ctx->q.local_addrlen); + ngtcp2_addr_init(&ctx->connected_path.remote, + &sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen); - ctx->conn_ref.get_conn = get_conn; - ctx->conn_ref.user_data = cf; + rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid, + &ctx->connected_path, + NGTCP2_PROTO_VER_V1, &ng_callbacks, + &ctx->settings, &ctx->transport_params, + Curl_ngtcp2_mem(), cf); + if(rc) + return CURLE_QUIC_CONNECT_ERROR; + + ctx->conn_ref.get_conn = get_conn; + ctx->conn_ref.user_data = cf; + } + else { + /* Tunneled QUIC (e.g. CONNECT-UDP): get remote address + from the connected filter below */ + const struct Curl_sockaddr_ex *remote = NULL; + if(cf->next->cft->query(cf->next, data, CF_QUERY_REMOTE_ADDR, NULL, + CURL_UNCONST(&remote))) + return CURLE_QUIC_CONNECT_ERROR; + if(!remote) + return CURLE_QUIC_CONNECT_ERROR; + + memset(&ctx->q.local_addr, 0, sizeof(ctx->q.local_addr)); + switch(remote->family) { + case AF_INET: + ((struct sockaddr_in *)&ctx->q.local_addr)->sin_family = AF_INET; + ctx->q.local_addrlen = sizeof(struct sockaddr_in); + break; +#ifdef USE_IPV6 + case AF_INET6: + ((struct sockaddr_in6 *)&ctx->q.local_addr)->sin6_family = AF_INET6; + ctx->q.local_addrlen = sizeof(struct sockaddr_in6); + break; +#endif + default: + return CURLE_QUIC_CONNECT_ERROR; + } + + ngtcp2_addr_init(&ctx->connected_path.local, + (struct sockaddr *)&ctx->q.local_addr, + ctx->q.local_addrlen); + ngtcp2_addr_init(&ctx->connected_path.remote, + &remote->curl_sa_addr, + (socklen_t)remote->addrlen); + + rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid, + &ctx->connected_path, + NGTCP2_PROTO_VER_V1, &ng_callbacks, + &ctx->settings, &ctx->transport_params, + Curl_ngtcp2_mem(), cf); + if(rc) + return CURLE_QUIC_CONNECT_ERROR; + + ctx->conn_ref.get_conn = get_conn; + ctx->conn_ref.user_data = cf; + } result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, &ALPN_SPEC_H3, cf_ngtcp2_tls_ctx_setup, &ctx->tls, @@ -2720,8 +2855,8 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf, return CURLE_OK; } - /* Connect the UDP filter first */ - if(!cf->next->connected) { + /* Connect the sub-chain */ + if(cf->next && !cf->next->connected) { result = Curl_conn_cf_connect(cf->next, data, done); if(result || !*done) return result; @@ -2803,11 +2938,14 @@ out: #ifdef CURLVERBOSE if(result) { - struct ip_quadruple ip; + if(ctx->q.sockfd != CURL_SOCKET_BAD) { + /* Direct UDP socket - get IP info for error reporting */ + struct ip_quadruple ip; - if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip)) - infof(data, "QUIC connect to %s port %u failed: %s", - ip.remote_ip, ip.remote_port, curl_easy_strerror(result)); + if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip)) + infof(data, "QUIC connect to %s port %u failed: %s", + ip.remote_ip, ip.remote_port, curl_easy_strerror(result)); + } } #endif if(!result && ctx->qconn) { @@ -3003,4 +3141,33 @@ out: return result; } +CURLcode Curl_cf_ngtcp2_insert_after(struct Curl_cfilter *cf_at) +{ + struct cf_ngtcp2_ctx *ctx = NULL; + struct Curl_cfilter *cf = NULL; + CURLcode result; + + ctx = curlx_calloc(1, sizeof(*ctx)); + if(!ctx) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + cf_ngtcp2_ctx_init(ctx); + + result = Curl_cf_create(&cf, &Curl_cft_http3, ctx); + if(result) + goto out; + Curl_conn_cf_insert_after(cf_at, cf); + cf->conn = cf_at->conn; +out: + if(result) { + curlx_safefree(cf); + cf_ngtcp2_ctx_free(ctx); + } + return result; +} + #endif + +/* Do not leak this filter's call_data accessor in unity builds. */ +#undef CF_CTX_CALL_DATA diff --git a/lib/vquic/curl_ngtcp2.h b/lib/vquic/curl_ngtcp2.h index 185272ace0..d69ae08eae 100644 --- a/lib/vquic/curl_ngtcp2.h +++ b/lib/vquic/curl_ngtcp2.h @@ -54,6 +54,8 @@ CURLcode Curl_cf_ngtcp2_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr); + +CURLcode Curl_cf_ngtcp2_insert_after(struct Curl_cfilter *cf_at); #endif #endif /* HEADER_CURL_VQUIC_CURL_NGTCP2_H */ diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index 73f664a653..43a16958a6 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -156,6 +156,7 @@ static void cf_quiche_ctx_close(struct cf_quiche_ctx *ctx) quiche_config_free(ctx->cfg); ctx->cfg = NULL; } + Curl_ssl_peer_cleanup(&ctx->peer); } static CURLcode cf_flush_egress(struct Curl_cfilter *cf, diff --git a/lib/vquic/vquic-tls.c b/lib/vquic/vquic-tls.c index ad4c713fa9..00366b7d30 100644 --- a/lib/vquic/vquic-tls.c +++ b/lib/vquic/vquic-tls.c @@ -72,6 +72,8 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx, return CURLE_FAILED_INIT; #endif (void)session_reuse_cb; + if(peer->dest) + Curl_ssl_peer_cleanup(peer); result = Curl_ssl_peer_init(peer, cf, tls_id, TRNSPRT_QUIC); if(result) return result; diff --git a/lib/vquic/vquic.c b/lib/vquic/vquic.c index cf4bc5a65f..9ac657c291 100644 --- a/lib/vquic/vquic.c +++ b/lib/vquic/vquic.c @@ -261,6 +261,44 @@ out: return result; } +/* Split QUIC payload by datagram (gso) boundaries when sending over a + * non-UDP lower filter (for example CONNECT-UDP proxy tunnel). */ +static CURLcode send_packet_no_gso_cf(struct Curl_cfilter *cf, + struct Curl_easy *data, + const uint8_t *pkt, size_t pktlen, + size_t gsolen, size_t *psent) +{ + const uint8_t *p, *end = pkt + pktlen; + size_t sent, len; + CURLcode result = CURLE_OK; + VERBOSE(size_t calls = 0); + + *psent = 0; + + /* Send one datagram-sized chunk per call into the lower filter. */ + for(p = pkt; p < end; p += len) { + len = CURLMIN(gsolen, (size_t)(end - p)); + result = Curl_conn_cf_send(cf->next, data, p, len, FALSE, &sent); + /* Report forward progress even if we return CURLE_AGAIN later. */ + *psent += sent; + VERBOSE(++calls); + /* Preserve lower-filter errors (including CURLE_AGAIN). */ + if(result) + goto out; + if(sent < len) { + /* We need whole datagrams here. Partial accept means blocked. */ + result = CURLE_AGAIN; + goto out; + } + } + +out: + CURL_TRC_CF(data, cf, "vquic_cf_send(len=%zu, gso=%zu, calls=%zu)" + " -> %d, sent=%zu", + pktlen, gsolen, calls, result, *psent); + return result; +} + static CURLcode vquic_send_packets(struct Curl_cfilter *cf, struct Curl_easy *data, struct cf_quic_ctx *qctx, @@ -310,7 +348,22 @@ CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data, blen = qctx->split_len; } - result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent); + if(qctx->sockfd != CURL_SOCKET_BAD) { + /* Direct UDP socket (via happy eyeballs) */ + result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent); + } + else { + /* Tunneled QUIC (CONNECT-UDP through proxy) */ + if(gsolen && (blen > gsolen)) { + /* Send one datagram at a time to preserve packet boundaries. */ + result = send_packet_no_gso_cf(cf, data, buf, blen, gsolen, &sent); + } + else { + /* No GSO aggregate to split, regular lower-filter send is enough. */ + result = Curl_conn_cf_send(cf->next, data, buf, blen, FALSE, &sent); + } + } + if(result) { if(result == CURLE_AGAIN) { Curl_bufq_skip(&qctx->sendbuf, sent); @@ -699,6 +752,16 @@ CURLcode Curl_qlogdir(struct Curl_easy *data, return CURLE_OK; } +CURLcode Curl_cf_quic_insert_after(struct Curl_cfilter *cf_at) +{ +#if defined(USE_NGTCP2) && defined(USE_NGHTTP3) + return Curl_cf_ngtcp2_insert_after(cf_at); +#else + (void)cf_at; + return CURLE_NOT_BUILT_IN; +#endif +} + CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, @@ -737,10 +800,6 @@ CURLcode Curl_conn_may_http3(struct Curl_easy *data, failf(data, "HTTP/3 is not supported over a SOCKS proxy"); return CURLE_URL_MALFORMAT; } - if(conn->bits.httpproxy && conn->bits.tunnel_proxy) { - failf(data, "HTTP/3 is not supported over an HTTP proxy"); - return CURLE_URL_MALFORMAT; - } #endif return CURLE_OK; diff --git a/lib/vquic/vquic.h b/lib/vquic/vquic.h index 1f0a1ab5e5..59178acd94 100644 --- a/lib/vquic/vquic.h +++ b/lib/vquic/vquic.h @@ -39,6 +39,8 @@ CURLcode Curl_qlogdir(struct Curl_easy *data, size_t scidlen, int *qlogfdp); +CURLcode Curl_cf_quic_insert_after(struct Curl_cfilter *cf_at); + CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index b4a0f9684f..fde151590b 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -3713,8 +3713,11 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx, return result; } - if(data->set.fdebug && data->set.verbose) { - /* the SSL trace callback is only used for verbose logging */ + if(data->set.fdebug && data->set.verbose && + (peer->transport != TRNSPRT_QUIC)) { + /* the SSL trace callback is only used for verbose logging; + * QUIC connections use a different TLS record format that + * ossl_trace cannot handle */ SSL_CTX_set_msg_callback(octx->ssl_ctx, ossl_trace); SSL_CTX_set_msg_callback_arg(octx->ssl_ctx, cf); } @@ -4007,12 +4010,20 @@ static CURLcode ossl_connect_step1(struct Curl_cfilter *cf, { struct ssl_connect_data *connssl = cf->ctx; struct ossl_ctx *octx = (struct ossl_ctx *)connssl->backend; + char tls_id[80]; BIO *bio; CURLcode result; DEBUGASSERT(ssl_connect_1 == connssl->connecting_state); DEBUGASSERT(octx); + if(!connssl->peer.dest) { + Curl_ossl_version(tls_id, sizeof(tls_id)); + result = Curl_ssl_peer_init(&connssl->peer, cf, tls_id, TRNSPRT_TCP); + if(result) + return result; + } + result = Curl_ossl_ctx_init(octx, cf, data, &connssl->peer, connssl->alpn, NULL, NULL, ossl_new_session_cb, cf, diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index e7dbad09c6..d640df4f03 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -1197,6 +1197,7 @@ void Curl_ssl_peer_cleanup(struct ssl_peer *peer) Curl_peer_unlink(&peer->dest); curlx_safefree(peer->sni); curlx_safefree(peer->scache_key); + peer->transport = TRNSPRT_NONE; peer->type = CURL_SSL_PEER_DNS; } @@ -1206,6 +1207,8 @@ static void cf_close(struct Curl_cfilter *cf, struct Curl_easy *data) if(connssl) { connssl->ssl_impl->close(cf, data); connssl->state = ssl_connection_none; + connssl->connecting_state = ssl_connect_1; + connssl->prefs_checked = FALSE; Curl_ssl_peer_cleanup(&connssl->peer); } cf->connected = FALSE; diff --git a/lib/vtls/vtls_int.h b/lib/vtls/vtls_int.h index 6700ee74cb..a0d8159a58 100644 --- a/lib/vtls/vtls_int.h +++ b/lib/vtls/vtls_int.h @@ -133,9 +133,6 @@ struct ssl_connect_data { BIT(input_pending); /* data for SSL_read() may be available */ }; -#undef CF_CTX_CALL_DATA -#define CF_CTX_CALL_DATA(cf) ((struct ssl_connect_data *)(cf)->ctx)->call_data - /* Definitions for SSL Implementations */ struct Curl_ssl { @@ -209,3 +206,9 @@ CURLcode Curl_on_session_reuse(struct Curl_cfilter *cf, #endif /* USE_SSL */ #endif /* HEADER_CURL_VTLS_INT_H */ + +#ifdef USE_SSL +/* Restore the default SSL filter call_data accessor for unity builds. */ +#undef CF_CTX_CALL_DATA +#define CF_CTX_CALL_DATA(cf) ((struct ssl_connect_data *)(cf)->ctx)->call_data +#endif diff --git a/src/tool_getparam.c b/src/tool_getparam.c index a7458a3b5f..7e776ea6b6 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -250,6 +250,7 @@ static const struct LongShort aliases[]= { {"proxy-digest", ARG_BOOL, ' ', C_PROXY_DIGEST}, {"proxy-header", ARG_STRG, ' ', C_PROXY_HEADER}, {"proxy-http2", ARG_BOOL, ' ', C_PROXY_HTTP2}, + {"proxy-http3", ARG_BOOL, ' ', C_PROXY_HTTP3}, {"proxy-insecure", ARG_BOOL, ' ', C_PROXY_INSECURE}, {"proxy-key", ARG_FILE|ARG_TLS, ' ', C_PROXY_KEY}, {"proxy-key-type", ARG_STRG|ARG_TLS, ' ', C_PROXY_KEY_TYPE}, @@ -2024,6 +2025,18 @@ static ParameterError opt_bool(struct OperationConfig *config, config->proxyver = toggle ? CURLPROXY_HTTPS2 : CURLPROXY_HTTPS; break; + case C_PROXY_HTTP3: /* --proxy-http3 */ +#ifndef USE_PROXY_HTTP3 + if(toggle) + return PARAM_LIBCURL_DOESNT_SUPPORT; + config->proxyver = CURLPROXY_HTTPS; +#else + if(!feature_httpsproxy || !feature_http3) + return PARAM_LIBCURL_DOESNT_SUPPORT; + + config->proxyver = toggle ? CURLPROXY_HTTPS3 : CURLPROXY_HTTPS; +#endif + break; case C_APPEND: /* --append */ config->ftp_append = toggle; break; @@ -2895,7 +2908,8 @@ static ParameterError opt_string(struct OperationConfig *config, case C_PROXY: /* --proxy */ /* --proxy */ err = getstr(&config->proxy, nextarg, ALLOW_BLANK); - if(config->proxyver != CURLPROXY_HTTPS2) + if(config->proxyver != CURLPROXY_HTTPS2 && + config->proxyver != CURLPROXY_HTTPS3) config->proxyver = CURLPROXY_HTTP; break; case C_REQUEST: /* --request */ diff --git a/src/tool_getparam.h b/src/tool_getparam.h index e137cc322f..32476d3776 100644 --- a/src/tool_getparam.h +++ b/src/tool_getparam.h @@ -201,6 +201,7 @@ typedef enum { C_PROXY_DIGEST, C_PROXY_HEADER, C_PROXY_HTTP2, + C_PROXY_HTTP3, C_PROXY_INSECURE, C_PROXY_KEY, C_PROXY_KEY_TYPE, diff --git a/src/tool_listhelp.c b/src/tool_listhelp.c index 864771bfba..c0b0af792f 100644 --- a/src/tool_listhelp.c +++ b/src/tool_listhelp.c @@ -542,6 +542,9 @@ const struct helptxt helptext[] = { { " --proxy-http2", "Use HTTP/2 with HTTPS proxy", CURLHELP_HTTP | CURLHELP_PROXY }, + { " --proxy-http3", + "Use HTTP/3 with HTTPS proxy", + CURLHELP_HTTP | CURLHELP_PROXY }, { " --proxy-insecure", "Skip HTTPS proxy cert verification", CURLHELP_PROXY | CURLHELP_TLS }, diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am index 78779a5518..4887a3594a 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -289,6 +289,8 @@ test3216 test3217 test3218 test3219 test3220 \ \ test3300 test3301 test3302 test3303 test3304 \ \ +test3400 \ +\ test4000 test4001 EXTRA_DIST = $(TESTCASES) DISABLED data-xml1 data320.html \ diff --git a/tests/data/test3400 b/tests/data/test3400 new file mode 100644 index 0000000000..12d014bffd --- /dev/null +++ b/tests/data/test3400 @@ -0,0 +1,19 @@ + + + + +unittest +capsule + + + + + +unittest + + +capsule protocol encode and decode unit tests + + + + diff --git a/tests/http/CMakeLists.txt b/tests/http/CMakeLists.txt index 3373f8af2b..9801d51907 100644 --- a/tests/http/CMakeLists.txt +++ b/tests/http/CMakeLists.txt @@ -28,6 +28,12 @@ if(NOT CADDY) endif() mark_as_advanced(CADDY) +find_program(H2O "h2o") # /usr/local/bin/h2o +if(NOT H2O) + set(H2O "") +endif() +mark_as_advanced(H2O) + find_program(VSFTPD "vsftpd") # /usr/sbin/vsftpd if(NOT VSFTPD) set(VSFTPD "") diff --git a/tests/http/Makefile.am b/tests/http/Makefile.am index f4dc92f61b..7232c1e8aa 100644 --- a/tests/http/Makefile.am +++ b/tests/http/Makefile.am @@ -31,6 +31,7 @@ TESTENV = \ testenv/dnsd.py \ testenv/dante.py \ testenv/env.py \ + testenv/h2o.py \ testenv/httpd.py \ testenv/mod_curltest/mod_curltest.c \ testenv/nghttpx.py \ @@ -72,6 +73,7 @@ EXTRA_DIST = \ test_40_socks.py \ test_50_scp.py \ test_51_sftp.py \ + test_60_h3_proxy.py \ $(TESTENV) clean-local: diff --git a/tests/http/config.ini.in b/tests/http/config.ini.in index 78808e966d..daf9869b7c 100644 --- a/tests/http/config.ini.in +++ b/tests/http/config.ini.in @@ -44,3 +44,6 @@ danted = @DANTED@ [sshd] sshd = @SSHD@ sftpd = @SFTPD@ + +[h2o] +h2o = @H2O@ diff --git a/tests/http/conftest.py b/tests/http/conftest.py index 08da73ac0f..0de5c1a8b9 100644 --- a/tests/http/conftest.py +++ b/tests/http/conftest.py @@ -1,4 +1,4 @@ -#*************************************************************************** +# *************************************************************************** # _ _ ____ _ # Project ___| | | | _ \| | # / __| | | | |_) | | @@ -31,9 +31,10 @@ from typing import Generator, Union import pytest from testenv.env import EnvConfig -sys.path.append(os.path.join(os.path.dirname(__file__), '.')) +sys.path.append(os.path.join(os.path.dirname(__file__), ".")) from testenv import Env, Httpd, Nghttpx, NghttpxFwd, NghttpxQuic, Sshd +from testenv.h2o import H2oProxy, H2oServer log = logging.getLogger(__name__) @@ -42,51 +43,47 @@ def pytest_report_header(config): # Env inits its base properties only once, we can report them here env = Env() report = [ - f'Testing curl {env.curl_version()}', - f' platform: {platform.platform()}', - f' curl: Version: {env.curl_version_string()}', - f' curl: Features: {env.curl_features_string()}', - f' curl: Protocols: {env.curl_protocols_string()}', - f' httpd: {env.httpd_version()}', - f' httpd-proxy: {env.httpd_version()}' + f"Testing curl {env.curl_version()}", + f" platform: {platform.platform()}", + f" curl: Version: {env.curl_version_string()}", + f" curl: Features: {env.curl_features_string()}", + f" curl: Protocols: {env.curl_protocols_string()}", + f" httpd: {env.httpd_version()}", + f" httpd-proxy: {env.httpd_version()}", ] if env.have_h3(): - report.extend([ - f' nghttpx: {env.nghttpx_version()}' - ]) + report.extend([f" nghttpx: {env.nghttpx_version()}"]) + if env.have_h2o(): + report.extend([f" h2o: {env.h2o_version()}"]) if env.has_caddy(): - report.extend([ - f' Caddy: {env.caddy_version()}' - ]) + report.extend([f" Caddy: {env.caddy_version()}"]) if env.has_vsftpd(): - report.extend([ - f' VsFTPD: {env.vsftpd_version()}' - ]) - buildinfo_fn = os.path.join(env.build_dir, 'buildinfo.txt') + report.extend([f" VsFTPD: {env.vsftpd_version()}"]) + buildinfo_fn = os.path.join(env.build_dir, "buildinfo.txt") if os.path.exists(buildinfo_fn): - with open(buildinfo_fn, 'r') as file_in: + with open(buildinfo_fn, "r") as file_in: for line in file_in: line = line.strip() - if line and not line.startswith('#'): + if line and not line.startswith("#"): report.extend([line]) - return '\n'.join(report) + return "\n".join(report) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def env_config(pytestconfig, testrun_uid, worker_id) -> EnvConfig: - return EnvConfig(pytestconfig=pytestconfig, - testrun_uid=testrun_uid, - worker_id=worker_id) + return EnvConfig( + pytestconfig=pytestconfig, testrun_uid=testrun_uid, worker_id=worker_id + ) -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def env(pytestconfig, env_config) -> Env: env = Env(pytestconfig=pytestconfig, env_config=env_config) level = logging.DEBUG if env.verbose > 0 else logging.INFO - logging.getLogger('').setLevel(level=level) - if not env.curl_has_protocol('http'): + logging.getLogger("").setLevel(level=level) + if not env.curl_has_protocol("http"): pytest.skip("curl built without HTTP support") - if not env.curl_has_protocol('https'): + if not env.curl_has_protocol("https"): pytest.skip("curl built without HTTPS support") if env.setup_incomplete(): pytest.skip(env.incomplete_reason()) @@ -95,23 +92,23 @@ def env(pytestconfig, env_config) -> Env: return env -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def httpd(env) -> Generator[Httpd, None, None]: httpd = Httpd(env=env) if not httpd.exists(): - pytest.skip(f'httpd not found: {env.httpd}') + pytest.skip(f"httpd not found: {env.httpd}") httpd.clear_logs() assert httpd.initial_start() yield httpd httpd.stop() -@pytest.fixture(scope='session') -def nghttpx(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]: +@pytest.fixture(scope="session") +def nghttpx(env, httpd) -> Generator[Union[Nghttpx, bool], None, None]: nghttpx = NghttpxQuic(env=env) if nghttpx.exists(): if not nghttpx.supports_h3() and env.have_h3_curl(): - log.warning('nghttpx does not support QUIC, but curl does') + log.warning("nghttpx does not support QUIC, but curl does") nghttpx.clear_logs() assert nghttpx.initial_start() yield nghttpx @@ -120,8 +117,8 @@ def nghttpx(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]: yield False -@pytest.fixture(scope='session') -def nghttpx_fwd(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]: +@pytest.fixture(scope="session") +def nghttpx_fwd(env, httpd) -> Generator[Union[Nghttpx, bool], None, None]: nghttpx = NghttpxFwd(env=env) if nghttpx.exists(): nghttpx.clear_logs() @@ -132,37 +129,63 @@ def nghttpx_fwd(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]: yield False -@pytest.fixture(scope='session') -def sshd(env: Env) -> Generator[Union[Sshd,bool], None, None]: +@pytest.fixture(scope="session") +def sshd(env: Env) -> Generator[Union[Sshd, bool], None, None]: if env.has_sshd(): sshd = Sshd(env=env) - assert sshd.initial_start(), f'{sshd.dump_log()}' + assert sshd.initial_start(), f"{sshd.dump_log()}" yield sshd sshd.stop() else: yield False -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def configures_httpd(env, httpd) -> Generator[bool, None, None]: # include this fixture as test parameter if the test configures httpd itself yield True -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def configures_nghttpx(env, httpd) -> Generator[bool, None, None]: # include this fixture as test parameter if the test configures nghttpx itself yield True -@pytest.fixture(autouse=True, scope='function') +@pytest.fixture(autouse=True, scope="function") def server_reset(request, env, httpd, nghttpx): # make sure httpd is in default configuration when a test starts - if 'configures_httpd' not in request.node._fixtureinfo.argnames: + if "configures_httpd" not in request.node._fixtureinfo.argnames: httpd.reset_config() httpd.reload_if_config_changed() - if env.have_h3() and \ - 'nghttpx' in request.node._fixtureinfo.argnames and \ - 'configures_nghttpx' not in request.node._fixtureinfo.argnames: + if ( + env.have_h3() + and "nghttpx" in request.node._fixtureinfo.argnames + and "configures_nghttpx" not in request.node._fixtureinfo.argnames + ): nghttpx.reset_config() nghttpx.reload_if_config_changed() + + +@pytest.fixture(scope="session") +def h2o_server(env) -> Generator[Union[H2oServer, bool], None, None]: + h2o = H2oServer(env=env) + if env.have_h2o(): + h2o.clear_logs() + assert h2o.initial_start() + yield h2o + h2o.stop() + else: + yield False + + +@pytest.fixture(scope="session") +def h2o_proxy(env) -> Generator[Union[H2oProxy, bool], None, None]: + h2o = H2oProxy(env=env) + if env.have_h2o(): + h2o.clear_logs() + assert h2o.initial_start() + yield h2o + h2o.stop() + else: + yield False diff --git a/tests/http/test_60_h3_proxy.py b/tests/http/test_60_h3_proxy.py new file mode 100644 index 0000000000..def32a6fe7 --- /dev/null +++ b/tests/http/test_60_h3_proxy.py @@ -0,0 +1,689 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# *************************************************************************** +# _ _ ____ _ +# 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 +# +########################################################################### +# +import os +import subprocess +import time + +import pytest +from testenv import CurlClient, Env + +MARK_NEEDS_HTTPS_PROXY = pytest.mark.skipif( + condition=not Env.curl_has_feature("HTTPS-proxy"), + reason="curl lacks HTTPS-proxy support" +) +MARK_NEEDS_HTTP3 = pytest.mark.skipif( + condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support" +) +MARK_NEEDS_PROXY_HTTP3 = pytest.mark.skipif( + condition=not Env.curl_has_feature("PROXY-HTTP3"), + reason="curl lacks experimental HTTP/3 proxy support" +) +MARK_NEEDS_NGHTTP3 = pytest.mark.skipif( + condition=not Env.curl_uses_lib("nghttp3"), reason="only supported with nghttp3" +) +MARK_NEEDS_NGHTTP2 = pytest.mark.skipif( + condition=not Env.curl_uses_lib("nghttp2"), reason="only supported with nghttp2" +) +MARK_NEEDS_H2O = pytest.mark.skipif( + condition=not Env.have_h2o(), reason="no h2o available" +) +MARK_NEEDS_NGHTTPX = pytest.mark.skipif( + condition=not Env.have_nghttpx(), reason="no nghttpx available" +) + +H3_PROXY_COMMON_MARKS = [ + MARK_NEEDS_HTTPS_PROXY, + MARK_NEEDS_HTTP3, + MARK_NEEDS_PROXY_HTTP3, + MARK_NEEDS_NGHTTP3, +] + +NGTCP2_ONLY_MSG = "only supported with the ngtcp2 quic stack" +UNSUPPORTED_OPT_MSG = "does not support this" +H2O_HELLO_MSG = '"message": "Hello from h2o HTTP/3 server"' + + +def _require_available(**items): + missing = [name for name, value in items.items() if not value] + if missing: + pytest.skip(f"{' or '.join(missing)} not available") + + +def _download_path(curl: CurlClient) -> str: + return os.path.join(curl.run_dir, "download_#1.data") + + +def _check_download_message(curl: CurlClient, expected: str): + dpath = _download_path(curl) + assert os.path.exists(dpath), f"Download file not found: {dpath}" + with open(dpath, "r") as fd: + content = fd.read() + assert expected in content, f"Unexpected response content: {content}" + + +def _check_download_size(curl: CurlClient, expected_size: int): + dpath = _download_path(curl) + assert os.path.exists(dpath), f"Download file not found: {dpath}" + actual = os.path.getsize(dpath) + assert actual == expected_size, f"expected {expected_size}B download, got {actual}B" + + +def _nghttpx_proxy_args( + env: Env, + nghttpx, + proxy_proto: str, + tunnel: bool, + insecure: bool = False, +): + xargs = [ + "--proxy", + f"https://{env.proxy_domain}:{nghttpx._port}/", + "--resolve", + f"{env.proxy_domain}:{nghttpx._port}:127.0.0.1", + "--proxy-cacert", + env.ca.cert_file, + ] + if proxy_proto == "h3": + xargs.append("--proxy-http3") + elif proxy_proto == "h2": + xargs.append("--proxy-http2") + + if tunnel: + xargs.append("--proxytunnel") + + xargs.extend(["--cacert", env.ca.cert_file, "--proxy-insecure"]) + if insecure: + xargs.append("--insecure") + return xargs + + +def _h2o_proxy_args( + env: Env, + h2o_proxy, + proxy_proto: str, + tunnel: bool, + insecure: bool = False, +): + if proxy_proto == "h3": + pport = h2o_proxy.port + elif proxy_proto == "h2": + pport = h2o_proxy.h2_port + else: + pport = h2o_proxy.h1_port + + xargs = [ + "--proxy", + f"https://{env.proxy_domain}:{pport}/", + "--resolve", + f"{env.proxy_domain}:{pport}:127.0.0.1", + "--proxy-cacert", + env.ca.cert_file, + ] + if proxy_proto == "h2": + xargs.append("--proxy-http2") + elif proxy_proto == "h3": + xargs.append("--proxy-http3") + + if tunnel: + xargs.append("--proxytunnel") + + xargs.extend(["--cacert", env.ca.cert_file, "--proxy-insecure"]) + if insecure: + xargs.append("--insecure") + return xargs + + +class TestH3ProxySuccess: + """Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP.""" + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] + + @pytest.mark.parametrize( + ["alpn_proto", "proxy_proto"], + [ + pytest.param("http/1.1", "h3", id="h1_over_h3_proxytunnel"), + pytest.param( + "h2", + "h3", + marks=MARK_NEEDS_NGHTTP2, + id="h2_over_h3_proxytunnel", + ), + pytest.param("h3", "h3", id="h3_over_h3_proxytunnel"), + pytest.param( + "h3", + "h2", + marks=MARK_NEEDS_NGHTTP2, + id="h3_over_h2_proxytunnel", + ), + pytest.param("h3", "http/1.1", id="h3_over_h1_proxytunnel"), + ], + ) + def test_60_01_connect_tunnel( + self, + env: Env, + h2o_server, + h2o_proxy, + alpn_proto, + proxy_proto, + ): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + + curl = CurlClient(env=env) + url = f"https://localhost:{h2o_server.port}/data.json" + proxy_args = _h2o_proxy_args( + env, h2o_proxy, proxy_proto, tunnel=True, insecure=True + ) + + r = curl.http_download( + urls=[url], alpn_proto=alpn_proto, with_stats=True, extra_args=proxy_args + ) + r.check_response(count=1, http_status=200) + _check_download_message(curl, H2O_HELLO_MSG) + + +class TestH3ProxyFailure: + """Failure matrix when proxy side does not support requested mode.""" + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX] + + @pytest.mark.parametrize( + ["alpn_proto", "proxy_proto", "exp_err"], + [ + pytest.param( + "http/1.1", + "h3", + "could not connect to server", + id="fail_h1_over_h3_proxytunnel", + ), + pytest.param( + "h2", + "h3", + "could not connect to server", + marks=MARK_NEEDS_NGHTTP2, + id="fail_h2_over_h3_proxytunnel", + ), + pytest.param( + "h3", + "h3", + "could not connect to server", + id="fail_h3_over_h3_proxytunnel", + ), + pytest.param( + "h3", + "h2", + "connect-udp response status 400", + marks=MARK_NEEDS_NGHTTP2, + id="fail_h3_over_h2_proxytunnel", + ), + pytest.param( + "h3", + "http/1.1", + "connect-udp tunnel failed, response 404", + id="fail_h3_over_h1_proxytunnel", + ), + ], + ) + def test_60_02_connect_tunnel_fail( + self, + env: Env, + httpd, + nghttpx, + alpn_proto, + proxy_proto, + exp_err, + ): + _require_available(httpd=httpd, nghttpx=nghttpx) + + curl = CurlClient(env=env) + url = f"https://localhost:{httpd.ports['https']}/data.json" + proxy_args = _nghttpx_proxy_args(env, nghttpx, proxy_proto, tunnel=True) + r = curl.http_download( + urls=[url], alpn_proto=alpn_proto, with_stats=True, extra_args=proxy_args + ) + assert r.exit_code != 0, f"Expected failure but curl succeeded: {r}" + assert exp_err in r.stderr.lower(), ( + f"Expected protocol/proxy error but got: {r.stderr}" + ) + + +class TestH3ProxyModeSelection: + """Behavior checks for tunnel vs non-tunnel proxy mode selection.""" + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX] + + @pytest.mark.parametrize( + ["proxy_proto"], + [ + pytest.param("h3", id="proxy_h3"), + pytest.param("h2", marks=MARK_NEEDS_NGHTTP2, id="proxy_h2"), + pytest.param("http/1.1", id="proxy_h1"), + ], + ) + def test_60_03_h3_target_auto_connect_udp( + self, env: Env, httpd, nghttpx, proxy_proto + ): + _require_available(httpd=httpd, nghttpx=nghttpx) + + curl = CurlClient(env=env) + url = f"https://localhost:{httpd.ports['https']}/data.json" + proxy_args = _nghttpx_proxy_args( + env, nghttpx, proxy_proto, tunnel=False + ) + r = curl.http_download( + urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args + ) + + # An HTTP/3 target auto-triggers CONNECT-UDP even without --proxytunnel, + # just as HTTPS targets auto-trigger CONNECT. nghttpx does not support + # CONNECT-UDP so this fails, which confirms auto-CONNECT-UDP is active. + assert r.exit_code != 0, ( + "expected failure: h3 target auto-triggers CONNECT-UDP " + "which nghttpx does not support" + ) + assert "connect-udp" in r.stderr.lower(), ( + f"expected CONNECT-UDP attempt in output, got: {r.stderr}" + ) + + +class TestH3ProxyRuntimeGuards: + """Guard checks for unsupported HTTP/3 proxy options.""" + + pytestmark = [ + MARK_NEEDS_HTTPS_PROXY, + MARK_NEEDS_PROXY_HTTP3, + pytest.mark.skipif( + condition=Env.curl_uses_lib("ngtcp2"), + reason="guard only applies to non-ngtcp2 builds", + ), + ] + + @pytest.mark.skipif( + condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support" + ) + def test_60_04_guard_proxy_http3_unsupported(self, env: Env, httpd): + curl = CurlClient(env=env) + url = f"https://localhost:{httpd.ports['https']}/data.json" + proxy_args = [ + "--proxy", + "https://127.0.0.1:1/", + "--proxy-http3", + "--proxytunnel", + "--proxy-insecure", + "--cacert", + env.ca.cert_file, + ] + + r = curl.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + if not env.curl_has_feature("PROXY-HTTP3"): + r.check_exit_code(2) + assert UNSUPPORTED_OPT_MSG in r.stderr.lower(), ( + f"Expected unsupported option failure but got: {r.stderr}" + ) + return + + r.check_exit_code(1) + assert NGTCP2_ONLY_MSG in r.stderr.lower(), ( + f"Expected ngtcp2 guard failure but got: {r.stderr}" + ) + + +class TestH3ProxyRobustness: + """Robustness checks for shutdown and proxy loss during transfer.""" + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] + + @pytest.fixture(autouse=True, scope="class") + def _class_scope(self, env): + doc_root = os.path.join(env.gen_dir, "docs") + env.make_data_file( + indir=doc_root, fname="proxy-drop-20m", fsize=20 * 1024 * 1024 + ) + + def test_60_05_graceful_shutdown( + self, env: Env, h2o_server, h2o_proxy + ): + if not env.curl_is_debug(): + pytest.skip("needs debug curl for shutdown trace lines") + if not env.curl_is_verbose(): + pytest.skip("needs verbose-strings curl build") + + curl = CurlClient(env=env, run_env={"CURL_DEBUG": "all"}) + url = f"https://localhost:{h2o_server.port}/data.json" + proxy_args = curl.get_proxy_args(proto="h3", tunnel=True) + proxy_args.extend(["--cacert", env.ca.cert_file, "--insecure"]) + + r = curl.http_download( + urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=1, http_status=200) + + shutdown_lines = [ + line + for line in r.trace_lines + if ("start shutdown(" in line.lower()) + or ("shutdown completely sent off" in line.lower()) + ] + assert shutdown_lines, f"No shutdown trace lines found:\n{r.stderr}" + + def test_60_06_proxy_drop_mid_transfer(self, env: Env, h2o_server, h2o_proxy): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + + proxy_port = h2o_proxy.port + url = f"https://localhost:{h2o_server.port}/proxy-drop-20m" + out_path = os.path.join(env.gen_dir, "proxy-drop.out") + args = [ + env.curl, + "--http1.1", + "--proxy", + f"https://{env.proxy_domain}:{proxy_port}/", + "--resolve", + f"{env.proxy_domain}:{proxy_port}:127.0.0.1", + "--proxy-cacert", + env.ca.cert_file, + "--proxy-http3", + "--proxytunnel", + "--proxy-insecure", + "--cacert", + env.ca.cert_file, + "--limit-rate", + "100k", + "--max-time", + "20", + "-o", + out_path, + url, + ] + + proc = None + try: + proc = subprocess.Popen( + args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + time.sleep(1.0) + assert h2o_proxy.stop(), "failed to stop h2o proxy" + _, stderr = proc.communicate(timeout=30) + assert proc.returncode != 0, ( + "curl should fail when proxy is terminated mid-transfer" + ) + serr = stderr.lower() + assert ( + "failed" in serr + or "transfer closed" in serr + or "recv failure" in serr + or "connection" in serr + ), f"Unexpected error output: {stderr}" + finally: + if proc and (proc.poll() is None): + proc.kill() + proc.wait(timeout=5) + assert h2o_proxy.start(), "failed to restart h2o proxy" + + +class TestH3ProxyDataTransfer: + """Large file transfers and multiplexing through HTTP/3 proxy.""" + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] + + @pytest.fixture(autouse=True, scope="class") + def _class_scope(self, env): + doc_root = os.path.join(env.gen_dir, "docs") + env.make_data_file(indir=doc_root, fname="download-1m", fsize=1 * 1024 * 1024) + env.make_data_file(indir=doc_root, fname="download-10m", fsize=10 * 1024 * 1024) + env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024) + + def test_60_07_large_download(self, env: Env, h2o_server, h2o_proxy): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + curl = CurlClient(env=env) + url = f"https://localhost:{h2o_server.port}/download-10m" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + r = curl.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=1, http_status=200) + _check_download_size(curl, 10 * 1024 * 1024) + + def test_60_08_large_upload(self, env: Env, httpd, h2o_server, h2o_proxy): + _require_available(h2o_proxy=h2o_proxy) + fdata = os.path.join(env.gen_dir, "upload-2m") + curl = CurlClient(env=env) + url = f"https://localhost:{httpd.ports['https']}/curltest/echo?id=[0-0]" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + r = curl.http_upload( + urls=[url], + data=f"@{fdata}", + alpn_proto="http/1.1", + with_stats=True, + extra_args=proxy_args, + ) + r.check_response(count=1, http_status=200) + + def test_60_09_parallel_downloads(self, env: Env, h2o_server, h2o_proxy): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + count = 5 + curl = CurlClient(env=env) + urln = f"https://localhost:{h2o_server.port}/download-1m?[0-{count - 1}]" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args.extend(["--parallel", "--parallel-max", f"{count}"]) + r = curl.http_download( + urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=count, http_status=200) + + +class TestH3ProxyConnectionManagement: + """Proxy authentication, connection reuse, and session resumption.""" + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] + + def test_60_10_proxy_basic_auth(self, env: Env, h2o_server, h2o_proxy): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + curl = CurlClient(env=env) + url = f"https://localhost:{h2o_server.port}/data.json" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args.extend(["--proxy-user", "testuser:testpass"]) + r = curl.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=1, http_status=200) + _check_download_message(curl, H2O_HELLO_MSG) + + def test_60_11_connection_reuse(self, env: Env, h2o_server, h2o_proxy): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + curl = CurlClient(env=env) + urln = f"https://localhost:{h2o_server.port}/data.json?[0-2]" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + r = curl.http_download( + urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=3, http_status=200) + assert r.total_connects <= 3, ( + f"expected proxy connection reuse, got {r.total_connects} connects" + ) + + def test_60_12_quic_session_resumption(self, env: Env, h2o_server, h2o_proxy): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + # First request establishes QUIC session + curl1 = CurlClient(env=env) + url = f"https://localhost:{h2o_server.port}/data.json" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + r1 = curl1.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r1.check_response(count=1, http_status=200) + # Second request from a fresh CurlClient; session may be reused + # by the TLS session cache if supported + curl2 = CurlClient(env=env) + r2 = curl2.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r2.check_response(count=1, http_status=200) + # Third request from a fresh CurlClient; session may be reused + # by the TLS session cache if supported + curl3 = CurlClient(env=env) + r3 = curl3.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r3.check_response(count=1, http_status=200) + + +class TestH3ProxyUdpTunnel: + """CONNECT-UDP tunnel payload size and capsule-protocol tests.""" + + pytestmark = H3_PROXY_COMMON_MARKS + + @pytest.fixture(autouse=True, scope="class") + def _class_scope(self, env): + doc_root = os.path.join(env.gen_dir, "docs") + env.make_data_file(indir=doc_root, fname="download-1400", fsize=1400) + env.make_data_file(indir=doc_root, fname="download-1m", fsize=1 * 1024 * 1024) + env.make_data_file(indir=doc_root, fname="download-10m", fsize=10 * 1024 * 1024) + + @MARK_NEEDS_H2O + @pytest.mark.parametrize( + "fname,fsize", + [ + ("download-1400", 1400), + ("download-1m", 1 * 1024 * 1024), + ("download-10m", 10 * 1024 * 1024), + ], + ) + def test_60_13_udp_tunnel_payload_sizes( + self, env: Env, h2o_server, h2o_proxy, fname, fsize + ): + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + curl = CurlClient(env=env) + url = f"https://localhost:{h2o_server.port}/{fname}" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + r = curl.http_download( + urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=1, http_status=200) + _check_download_size(curl, fsize) + + @MARK_NEEDS_NGHTTPX + def test_60_14_udp_tunnel_capsule_absent(self, env: Env, httpd, nghttpx): + _require_available(httpd=httpd, nghttpx=nghttpx) + curl = CurlClient(env=env) + url = f"https://localhost:{httpd.ports['https']}/data.json" + proxy_args = _nghttpx_proxy_args(env, nghttpx, "h3", tunnel=True) + r = curl.http_download( + urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args + ) + assert r.exit_code != 0, ( + "expected failure: nghttpx does not support CONNECT-UDP / Capsule-Protocol" + ) + + +class TestH3ProxyEdgeCases: + """Timeout and protocol-mismatch edge cases.""" + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] + + def test_60_15_connect_timeout(self, env: Env, h2o_server): + _require_available(h2o_server=h2o_server) + curl = CurlClient(env=env, timeout=15) + url = f"https://localhost:{h2o_server.port}/data.json" + proxy_args = [ + "--proxy", + "https://192.0.2.1:1/", + "--proxy-http3", + "--proxytunnel", + "--proxy-insecure", + "--connect-timeout", + "3", + "--cacert", + env.ca.cert_file, + ] + r = curl.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + assert r.exit_code != 0, "expected timeout connecting to unreachable proxy" + assert r.duration.total_seconds() < 10, ( + f"timeout not respected: took {r.duration.total_seconds():.1f}s" + ) + + @MARK_NEEDS_NGHTTP2 + def test_60_16_h2_uses_connect_tcp_not_udp(self, env: Env, httpd, h2o_proxy): + _require_available(httpd=httpd, h2o_proxy=h2o_proxy) + curl = CurlClient(env=env) + url = f"https://localhost:{httpd.ports['https']}/data.json" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + # h2 inner traffic always uses CONNECT (TCP), never CONNECT-UDP, + # even through an HTTP/3 proxy with --proxytunnel. h2o supports + # CONNECT TCP tunneling, so this request succeeds. + r = curl.http_download( + urls=[url], alpn_proto="h2", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=1, http_status=200) + + +class TestH3ProxyHappyEyeballs: + """ + Verify that happy eyeballs is active for HTTP/3 proxy connections. + + With the H3-PROXY filter sitting above HAPPY-EYEBALLS -> UDP, address + family selection to the proxy is done by happy eyeballs. + """ + + pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] + + def test_60_17_h3_proxy_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy): + """Verbose trace confirms HAPPY-EYEBALLS filter is in the H3 proxy chain.""" + if not env.curl_is_debug(): + pytest.skip("needs debug curl for filter trace") + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + curl = CurlClient(env=env, run_env={"CURL_DEBUG": "HAPPY-EYEBALLS,H3-PROXY"}) + url = f"https://localhost:{h2o_server.port}/data.json" + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + r = curl.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + ) + r.check_response(count=1, http_status=200) + assert "happy-eyeballs" in r.stderr.lower(), ( + f"expected HAPPY-EYEBALLS trace for H3 proxy, got: {r.stderr}" + ) + + @MARK_NEEDS_NGHTTP2 + def test_60_18_h3_proxy_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy): + """IPv4-forced H3 proxy works for h1/h2/h3 inner protocols.""" + _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) + for alpn_proto in ["http/1.1", "h2", "h3"]: + curl = CurlClient(env=env) + url = f"https://localhost:{h2o_server.port}/data.json" + proxy_args = _h2o_proxy_args( + env, h2o_proxy, "h3", tunnel=True, insecure=True + ) + proxy_args.append("--ipv4") + r = curl.http_download( + urls=[url], + alpn_proto=alpn_proto, + with_stats=True, + extra_args=proxy_args, + ) + r.check_response(count=1, http_status=200) diff --git a/tests/http/testenv/curl.py b/tests/http/testenv/curl.py index 99aa649bc0..272b6045cb 100644 --- a/tests/http/testenv/curl.py +++ b/tests/http/testenv/curl.py @@ -688,7 +688,12 @@ class CurlClient: proxy_name = '[::1]' if use_ipv6 else \ self._server_addr if use_ip else self.env.proxy_domain if proxys: - pport = self.env.pts_port(proto) if tunnel else self.env.proxys_port + if tunnel: + pport = self.env.pts_port(proto) + elif proto == 'h3': + pport = self.env.h3proxys_port + else: + pport = self.env.proxys_port xargs = [ '--proxy', f'https://{proxy_name}:{pport}/', '--proxy-cacert', self.env.ca.cert_file, @@ -697,6 +702,8 @@ class CurlClient: xargs.extend(['--resolve', f'{proxy_name}:{pport}:{self._server_addr}']) if proto == 'h2': xargs.append('--proxy-http2') + elif proto == 'h3': + xargs.append('--proxy-http3') else: xargs = [ '--proxy', f'http://{proxy_name}:{self.env.proxy_port}/', diff --git a/tests/http/testenv/env.py b/tests/http/testenv/env.py index c7bbfc4c54..a2032f82ce 100644 --- a/tests/http/testenv/env.py +++ b/tests/http/testenv/env.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#*************************************************************************** +# *************************************************************************** # _ _ ____ _ # Project ___| | | | _ \| | # / __| | | | |_) | | @@ -54,20 +54,21 @@ def init_config_from(conf_path): TESTS_HTTPD_PATH = os.path.dirname(os.path.dirname(__file__)) PROJ_PATH = os.path.dirname(os.path.dirname(TESTS_HTTPD_PATH)) TOP_PATH = os.path.join(os.getcwd(), os.path.pardir) -CONFIG_PATH = os.path.join(TOP_PATH, 'tests', 'http', 'config.ini') +CONFIG_PATH = os.path.join(TOP_PATH, "tests", "http", "config.ini") if not os.path.exists(CONFIG_PATH): - ALT_CONFIG_PATH = os.path.join(PROJ_PATH, 'tests', 'http', 'config.ini') + ALT_CONFIG_PATH = os.path.join(PROJ_PATH, "tests", "http", "config.ini") if not os.path.exists(ALT_CONFIG_PATH): - raise Exception(f'unable to find config.ini in {CONFIG_PATH} nor {ALT_CONFIG_PATH}') + raise Exception( + f"unable to find config.ini in {CONFIG_PATH} nor {ALT_CONFIG_PATH}" + ) TOP_PATH = PROJ_PATH CONFIG_PATH = ALT_CONFIG_PATH DEF_CONFIG = init_config_from(CONFIG_PATH) -CURL = os.path.join(TOP_PATH, 'src', 'curl') -CURLINFO = os.path.join(TOP_PATH, 'src', 'curlinfo') +CURL = os.path.join(TOP_PATH, "src", "curl") +CURLINFO = os.path.join(TOP_PATH, "src", "curlinfo") class NghttpxUtil: - CMD = None VERSION_FULL = None @@ -76,34 +77,37 @@ class NghttpxUtil: if cmd is None: return None if cls.VERSION_FULL is None or cmd != cls.CMD: - p = subprocess.run(args=[cmd, '--version'], - capture_output=True, text=True) + p = subprocess.run(args=[cmd, "--version"], capture_output=True, text=True) if p.returncode != 0: - raise RuntimeError(f'{cmd} --version failed with exit code: {p.returncode}') + raise RuntimeError( + f"{cmd} --version failed with exit code: {p.returncode}" + ) cls.CMD = cmd for line in p.stdout.splitlines(keepends=False): - if line.startswith('nghttpx '): + if line.startswith("nghttpx "): cls.VERSION_FULL = line if cls.VERSION_FULL is None: - raise RuntimeError(f'{cmd}: unable to determine version') + raise RuntimeError(f"{cmd}: unable to determine version") return cls.VERSION_FULL @staticmethod def version_with_h3(version): - return re.match(r'.* ngtcp2/\d+\.\d+\.\d+.*', version) is not None + return re.match(r".* ngtcp2/\d+\.\d+\.\d+.*", version) is not None class EnvConfig: - - def __init__(self, pytestconfig: Optional[pytest.Config] = None, - testrun_uid=None, - worker_id=None): + def __init__( + self, + pytestconfig: Optional[pytest.Config] = None, + testrun_uid=None, + worker_id=None, + ): self.pytestconfig = pytestconfig self.testrun_uid = testrun_uid - self.worker_id = worker_id if worker_id is not None else 'master' + self.worker_id = worker_id if worker_id is not None else "master" self.tests_dir = TESTS_HTTPD_PATH - self.gen_root = self.gen_dir = os.path.join(self.tests_dir, 'gen') - if self.worker_id != 'master': + self.gen_root = self.gen_dir = os.path.join(self.tests_dir, "gen") + if self.worker_id != "master": self.gen_dir = os.path.join(self.gen_dir, self.worker_id) self.project_dir = os.path.dirname(os.path.dirname(self.tests_dir)) self.build_dir = TOP_PATH @@ -111,57 +115,56 @@ class EnvConfig: # check cur and its features self.curl = CURL self.curlinfo = CURLINFO - if 'CURL' in os.environ: - self.curl = os.environ['CURL'] + if "CURL" in os.environ: + self.curl = os.environ["CURL"] self.curl_props = { - 'version_string': '', - 'version': '', - 'os': '', - 'fullname': '', - 'features_string': '', - 'features': set(), - 'protocols_string': '', - 'protocols': set(), - 'libs': set(), - 'lib_versions': set(), + "version_string": "", + "version": "", + "os": "", + "fullname": "", + "features_string": "", + "features": set(), + "protocols_string": "", + "protocols": set(), + "libs": set(), + "lib_versions": set(), } self.curl_is_debug = False self.curl_protos = [] - p = subprocess.run(args=[self.curl, '-V'], - capture_output=True, text=True) + p = subprocess.run(args=[self.curl, "-V"], capture_output=True, text=True) if p.returncode != 0: - raise RuntimeError(f'{self.curl} -V failed with exit code: {p.returncode}') - if p.stderr.startswith('WARNING:'): + raise RuntimeError(f"{self.curl} -V failed with exit code: {p.returncode}") + if p.stderr.startswith("WARNING:"): self.curl_is_debug = True for line in p.stdout.splitlines(keepends=False): - if line.startswith('curl '): - self.curl_props['version_string'] = line - m = re.match(r'^curl (?P\S+) (?P\S+) (?P.*)$', line) + if line.startswith("curl "): + self.curl_props["version_string"] = line + m = re.match(r"^curl (?P\S+) (?P\S+) (?P.*)$", line) if m: - self.curl_props['fullname'] = m.group(0) - self.curl_props['version'] = m.group('version') - self.curl_props['os'] = m.group('os') - self.curl_props['lib_versions'] = { - lib.lower() for lib in m.group('libs').split(' ') + self.curl_props["fullname"] = m.group(0) + self.curl_props["version"] = m.group("version") + self.curl_props["os"] = m.group("os") + self.curl_props["lib_versions"] = { + lib.lower() for lib in m.group("libs").split(" ") } - self.curl_props['libs'] = { - re.sub(r'/[a-z0-9.-]*', '', lib) for lib in self.curl_props['lib_versions'] + self.curl_props["libs"] = { + re.sub(r"/[a-z0-9.-]*", "", lib) + for lib in self.curl_props["lib_versions"] } - if line.startswith('Features: '): - self.curl_props['features_string'] = line[10:] - self.curl_props['features'] = { - feat.lower() for feat in line[10:].split(' ') + if line.startswith("Features: "): + self.curl_props["features_string"] = line[10:] + self.curl_props["features"] = { + feat.lower() for feat in line[10:].split(" ") } - if line.startswith('Protocols: '): - self.curl_props['protocols_string'] = line[11:] - self.curl_props['protocols'] = { - prot.lower() for prot in line[11:].split(' ') + if line.startswith("Protocols: "): + self.curl_props["protocols_string"] = line[11:] + self.curl_props["protocols"] = { + prot.lower() for prot in line[11:].split(" ") } - p = subprocess.run(args=[self.curlinfo], - capture_output=True, text=True) + p = subprocess.run(args=[self.curlinfo], capture_output=True, text=True) if p.returncode != 0: - raise RuntimeError(f'{self.curlinfo} failed with exit code: {p.returncode}') + raise RuntimeError(f"{self.curlinfo} failed with exit code: {p.returncode}") self.curl_is_verbose = 'verbose-strings: ON' in p.stdout self.curl_can_cert_status = 'cert-status: ON' in p.stdout self.curl_override_dns = 'override-dns: ON' in p.stdout @@ -169,18 +172,18 @@ class EnvConfig: self.ports = {} - self.httpd = self.config['httpd']['httpd'] - self.apxs = self.config['httpd']['apxs'] + self.httpd = self.config["httpd"]["httpd"] + self.apxs = self.config["httpd"]["apxs"] if len(self.apxs) == 0: self.apxs = None self._httpd_version = None self.examples_pem = { - 'key': 'xxx', - 'cert': 'xxx', + "key": "xxx", + "cert": "xxx", } - self.htdocs_dir = os.path.join(self.gen_dir, 'htdocs') - self.tld = 'http.curl.se' + self.htdocs_dir = os.path.join(self.gen_dir, "htdocs") + self.tld = "http.curl.se" self.domain1 = f"one.{self.tld}" self.domain1brotli = f"brotli.one.{self.tld}" self.domain2 = f"two.{self.tld}" @@ -188,22 +191,43 @@ class EnvConfig: self.proxy_domain = f"proxy.{self.tld}" self.expired_domain = f"expired.{self.tld}" self.cert_specs = [ - CertificateSpec(domains=[self.domain1, self.domain1brotli, 'localhost', '127.0.0.1'], key_type='rsa2048'), - CertificateSpec(name='domain1-no-ip', domains=[self.domain1, self.domain1brotli], key_type='rsa2048'), - CertificateSpec(name='domain1-very-bad', domains=[self.domain1, 'dns:127.0.0.1'], key_type='rsa2048'), - CertificateSpec(domains=[self.domain2], key_type='rsa2048'), - CertificateSpec(domains=[self.ftp_domain], key_type='rsa2048'), - CertificateSpec(domains=[self.proxy_domain, '127.0.0.1'], key_type='rsa2048'), - CertificateSpec(domains=[self.expired_domain], key_type='rsa2048', - valid_from=timedelta(days=-100), valid_to=timedelta(days=-10)), - CertificateSpec(name="clientsX", sub_specs=[ - CertificateSpec(name="user1", client=True), - ]), + CertificateSpec( + domains=[self.domain1, self.domain1brotli, "localhost", "127.0.0.1"], + key_type="rsa2048", + ), + CertificateSpec( + name="domain1-no-ip", + domains=[self.domain1, self.domain1brotli], + key_type="rsa2048", + ), + CertificateSpec( + name="domain1-very-bad", + domains=[self.domain1, "dns:127.0.0.1"], + key_type="rsa2048", + ), + CertificateSpec(domains=[self.domain2], key_type="rsa2048"), + CertificateSpec(domains=[self.ftp_domain], key_type="rsa2048"), + CertificateSpec( + domains=[self.proxy_domain, "127.0.0.1"], key_type="rsa2048" + ), + CertificateSpec( + domains=[self.expired_domain], + key_type="rsa2048", + valid_from=timedelta(days=-100), + valid_to=timedelta(days=-10), + ), + CertificateSpec( + name="clientsX", + sub_specs=[ + CertificateSpec(name="user1", client=True), + ], + ), ] - self.openssl = 'openssl' - p = subprocess.run(args=[self.openssl, 'version'], - capture_output=True, text=True) + self.openssl = "openssl" + p = subprocess.run( + args=[self.openssl, "version"], capture_output=True, text=True + ) if p.returncode != 0: # no openssl in path self.openssl = None @@ -211,7 +235,7 @@ class EnvConfig: else: self.openssl_version = p.stdout.strip() - self.nghttpx = self.config['nghttpx']['nghttpx'] + self.nghttpx = self.config["nghttpx"]["nghttpx"] if len(self.nghttpx.strip()) == 0: self.nghttpx = None self._nghttpx_version = None @@ -220,30 +244,58 @@ class EnvConfig: self._nghttpx_version = NghttpxUtil.version(self.nghttpx) self.nghttpx_with_h3 = NghttpxUtil.version_with_h3(self._nghttpx_version) - self.caddy = self.config['caddy']['caddy'] + self.caddy = self.config["caddy"]["caddy"] self._caddy_version = None if len(self.caddy.strip()) == 0: self.caddy = None + + self.h2o = self.config["h2o"]["h2o"] + if len(self.h2o.strip()) == 0: + self.h2o = None + self._h2o_version = None + if self.h2o is not None: + try: + p = subprocess.run( + args=[self.h2o, "--version"], capture_output=True, text=True + ) + if p.returncode != 0: + # not a working h2o + self.h2o = None + else: + # h2o --version output format: "h2o version 2.3.0" + m = re.search(r"h2o version (\S+)", p.stdout) + if m: + self._h2o_version = m.group(1) + else: + self.h2o = None + except Exception: + log.exception("checking h2o version") + self.h2o = None + if self.caddy is not None: - p = subprocess.run(args=[self.caddy, 'version'], - capture_output=True, text=True) + p = subprocess.run( + args=[self.caddy, "version"], capture_output=True, text=True + ) if p.returncode != 0: # not a working caddy self.caddy = None - m = re.match(r'v?(\d+\.\d+\.\d+).*', p.stdout) + m = re.match(r"v?(\d+\.\d+\.\d+).*", p.stdout) if m: self._caddy_version = m.group(1) else: - raise RuntimeError(f'Unable to determine caddy version from: {p.stdout}') + raise RuntimeError( + f"Unable to determine caddy version from: {p.stdout}" + ) - self.vsftpd = self.config['vsftpd']['vsftpd'] - if self.vsftpd == '': + self.vsftpd = self.config["vsftpd"]["vsftpd"] + if self.vsftpd == "": self.vsftpd = None self._vsftpd_version = None if self.vsftpd is not None: - with tempfile.TemporaryFile('w+') as tmp: - p = subprocess.run(args=[self.vsftpd, '-v'], - capture_output=True, text=True, stdin=tmp) + with tempfile.TemporaryFile("w+") as tmp: + p = subprocess.run( + args=[self.vsftpd, "-v"], capture_output=True, text=True, stdin=tmp + ) if p.returncode != 0: # not a working vsftpd self.vsftpd = None @@ -256,80 +308,83 @@ class EnvConfig: # any data there instead. tmp.seek(0) ver_text = tmp.read() - m = re.match(r'vsftpd: version (\d+\.\d+\.\d+)', ver_text) + m = re.match(r"vsftpd: version (\d+\.\d+\.\d+)", ver_text) if m: self._vsftpd_version = m.group(1) elif len(p.stderr) == 0: # vsftp does not use stdout or stderr for printing its version... -.- - self._vsftpd_version = 'unknown' + self._vsftpd_version = "unknown" else: - raise Exception(f'Unable to determine VsFTPD version from: {p.stderr}') + raise Exception(f"Unable to determine VsFTPD version from: {p.stderr}") - self.danted = self.config['danted']['danted'] - if self.danted == '': + self.danted = self.config["danted"]["danted"] + if self.danted == "": self.danted = None self._danted_version = None if self.danted is not None: - p = subprocess.run(args=[self.danted, '-v'], - capture_output=True, text=True) + p = subprocess.run(args=[self.danted, "-v"], capture_output=True, text=True) assert p.returncode == 0 if p.returncode != 0: # not a working vsftpd self.danted = None - m = re.match(r'^Dante v(\d+\.\d+\.\d+).*', p.stdout) + m = re.match(r"^Dante v(\d+\.\d+\.\d+).*", p.stdout) if not m: - m = re.match(r'^Dante v(\d+\.\d+\.\d+).*', p.stderr) + m = re.match(r"^Dante v(\d+\.\d+\.\d+).*", p.stderr) if m: self._danted_version = m.group(1) else: self.danted = None - raise Exception(f'Unable to determine danted version from: {p.stderr}') + raise Exception(f"Unable to determine danted version from: {p.stderr}") - self.sshd = self.config['sshd']['sshd'] - if self.sshd == '': + self.sshd = self.config["sshd"]["sshd"] + if self.sshd == "": self.sshd = None self._sshd_version = None if self.sshd is not None: - p = subprocess.run(args=[self.sshd, '-V'], - capture_output=True, text=True) + p = subprocess.run(args=[self.sshd, "-V"], capture_output=True, text=True) assert p.returncode == 0 if p.returncode != 0: self.sshd = None else: - m = re.match(r'^OpenSSH_(\d+\.\d+.*),.*', p.stderr) - assert m, f'version: {p.stderr}' + m = re.match(r"^OpenSSH_(\d+\.\d+.*),.*", p.stderr) + assert m, f"version: {p.stderr}" if m: self._sshd_version = m.group(1) else: self.sshd = None - raise Exception(f'Unable to determine sshd version from: {p.stderr}') + raise Exception( + f"Unable to determine sshd version from: {p.stderr}" + ) if self.sshd: - self.sftpd = self.config['sshd']['sftpd'] - if self.sftpd == '': + self.sftpd = self.config["sshd"]["sftpd"] + if self.sftpd == "": self.sftpd = None else: self.sftpd = None - self._tcpdump = shutil.which('tcpdump') + self._tcpdump = shutil.which("tcpdump") @property def httpd_version(self): if self._httpd_version is None and self.apxs is not None: try: - p = subprocess.run(args=[self.apxs, '-q', 'HTTPD_VERSION'], - capture_output=True, text=True) + p = subprocess.run( + args=[self.apxs, "-q", "HTTPD_VERSION"], + capture_output=True, + text=True, + ) if p.returncode != 0: - log.error(f'{self.apxs} failed to query HTTPD_VERSION: {p}') + log.error(f"{self.apxs} failed to query HTTPD_VERSION: {p}") else: self._httpd_version = p.stdout.strip() except Exception: - log.exception(f'{self.apxs} failed to run') + log.exception(f"{self.apxs} failed to run") return self._httpd_version def versiontuple(self, v): - v = re.sub(r'(\d+\.\d+(\.\d+)?)(-\S+)?', r'\1', v) - return tuple(map(int, v.split('.'))) + v = re.sub(r"(\d+\.\d+(\.\d+)?)(-\S+)?", r"\1", v) + return tuple(map(int, v.split("."))) def httpd_is_at_least(self, minv): if self.httpd_version is None: @@ -344,15 +399,17 @@ class EnvConfig: return hv >= self.versiontuple(minv) def is_complete(self) -> bool: - return os.path.isfile(self.httpd) and \ - self.apxs is not None and \ - os.path.isfile(self.apxs) + return ( + os.path.isfile(self.httpd) + and self.apxs is not None + and os.path.isfile(self.apxs) + ) def get_incomplete_reason(self) -> Optional[str]: if self.httpd is None or len(self.httpd.strip()) == 0: - return 'httpd not configured, see `--with-test-httpd=`' + return "httpd not configured, see `--with-test-httpd=`" if not os.path.isfile(self.httpd): - return f'httpd ({self.httpd}) not found' + return f"httpd ({self.httpd}) not found" if self.apxs is None: return "command apxs not found (commonly provided in apache2-dev)" if not os.path.isfile(self.apxs): @@ -371,18 +428,21 @@ class EnvConfig: def vsftpd_version(self): return self._vsftpd_version + @property + def h2o_version(self): + return self._h2o_version + @property def tcpdmp(self) -> Optional[str]: return self._tcpdump def clear_locks(self): - ca_lock = os.path.join(self.gen_root, 'ca/ca.lock') + ca_lock = os.path.join(self.gen_root, "ca/ca.lock") if os.path.exists(ca_lock): os.remove(ca_lock) class Env: - SERVER_TIMEOUT = 30 # seconds to wait for server to come up/reload CONFIG = EnvConfig() @@ -407,98 +467,106 @@ class Env: def have_h3_server() -> bool: return Env.CONFIG.nghttpx_with_h3 + @staticmethod + def have_h2o() -> bool: + return Env.CONFIG.h2o is not None + @staticmethod def have_ssl_curl() -> bool: - return Env.curl_has_feature('ssl') or Env.curl_has_feature('multissl') + return Env.curl_has_feature("ssl") or Env.curl_has_feature("multissl") @staticmethod def have_h2_curl() -> bool: - return 'http2' in Env.CONFIG.curl_props['features'] + return "http2" in Env.CONFIG.curl_props["features"] @staticmethod def have_h3_curl() -> bool: - return 'http3' in Env.CONFIG.curl_props['features'] + return "http3" in Env.CONFIG.curl_props["features"] @staticmethod def have_compressed_curl() -> bool: - return 'brotli' in Env.CONFIG.curl_props['libs'] or \ - 'zlib' in Env.CONFIG.curl_props['libs'] or \ - 'zstd' in Env.CONFIG.curl_props['libs'] + return ( + "brotli" in Env.CONFIG.curl_props["libs"] + or "zlib" in Env.CONFIG.curl_props["libs"] + or "zstd" in Env.CONFIG.curl_props["libs"] + ) @staticmethod def curl_uses_lib(libname: str) -> bool: - return libname.lower() in Env.CONFIG.curl_props['libs'] + return libname.lower() in Env.CONFIG.curl_props["libs"] @staticmethod def curl_uses_any_libs(libs: List[str]) -> bool: for libname in libs: - if libname.lower() in Env.CONFIG.curl_props['libs']: + if libname.lower() in Env.CONFIG.curl_props["libs"]: return True return False @staticmethod def curl_uses_ossl_quic() -> bool: if Env.have_h3_curl(): - return not Env.curl_uses_lib('ngtcp2') and Env.curl_uses_lib('nghttp3') + return not Env.curl_uses_lib("ngtcp2") and Env.curl_uses_lib("nghttp3") return False @staticmethod def curl_version_string() -> str: - return Env.CONFIG.curl_props['version_string'] + return Env.CONFIG.curl_props["version_string"] @staticmethod def curl_features_string() -> str: - return Env.CONFIG.curl_props['features_string'] + return Env.CONFIG.curl_props["features_string"] @staticmethod def curl_has_feature(feature: str) -> bool: - return feature.lower() in Env.CONFIG.curl_props['features'] + return feature.lower() in Env.CONFIG.curl_props["features"] @staticmethod def curl_protocols_string() -> str: - return Env.CONFIG.curl_props['protocols_string'] + return Env.CONFIG.curl_props["protocols_string"] @staticmethod def curl_has_protocol(protocol: str) -> bool: - return protocol.lower() in Env.CONFIG.curl_props['protocols'] + return protocol.lower() in Env.CONFIG.curl_props["protocols"] @staticmethod def curl_lib_version(libname: str) -> str: - prefix = f'{libname.lower()}/' - for lversion in Env.CONFIG.curl_props['lib_versions']: + prefix = f"{libname.lower()}/" + for lversion in Env.CONFIG.curl_props["lib_versions"]: if lversion.startswith(prefix): - return lversion[len(prefix):] - return 'unknown' + return lversion[len(prefix) :] + return "unknown" @staticmethod def curl_lib_version_at_least(libname: str, min_version) -> bool: lversion = Env.curl_lib_version(libname) - if lversion != 'unknown': - return Env.CONFIG.versiontuple(min_version) <= \ - Env.CONFIG.versiontuple(lversion) + if lversion != "unknown": + return Env.CONFIG.versiontuple(min_version) <= Env.CONFIG.versiontuple( + lversion + ) return False @staticmethod def curl_lib_version_before(libname: str, lib_version) -> bool: lversion = Env.curl_lib_version(libname) - if lversion != 'unknown': - if m := re.match(r'(\d+\.\d+\.\d+).*', lversion): + if lversion != "unknown": + if m := re.match(r"(\d+\.\d+\.\d+).*", lversion): lversion = m.group(1) - return Env.CONFIG.versiontuple(lib_version) > \ - Env.CONFIG.versiontuple(lversion) + return Env.CONFIG.versiontuple(lib_version) > Env.CONFIG.versiontuple( + lversion + ) return False @staticmethod def curl_os() -> str: - return Env.CONFIG.curl_props['os'] + return Env.CONFIG.curl_props["os"] @staticmethod def curl_fullname() -> str: - return Env.CONFIG.curl_props['fullname'] + return Env.CONFIG.curl_props["fullname"] @staticmethod def curl_version() -> str: - return Env.CONFIG.curl_props['version'] + return Env.CONFIG.curl_props["version"] @staticmethod def curl_is_debug() -> bool: @@ -528,32 +596,31 @@ class Env: @staticmethod def curl_can_h3_early_data() -> bool: - return Env.curl_can_early_data() and \ - Env.curl_uses_lib('ngtcp2') + return Env.curl_can_early_data() and Env.curl_uses_lib("ngtcp2") @staticmethod def http_protos() -> List[str]: # http protocols we can test if Env.have_h2_curl(): if Env.have_h3(): - return ['http/1.1', 'h2', 'h3'] - return ['http/1.1', 'h2'] - return ['http/1.1'] + return ["http/1.1", "h2", "h3"] + return ["http/1.1", "h2"] + return ["http/1.1"] @staticmethod def http_h1_h2_protos() -> List[str]: # http 1+2 protocols we can test if Env.have_h2_curl(): - return ['http/1.1', 'h2'] - return ['http/1.1'] + return ["http/1.1", "h2"] + return ["http/1.1"] @staticmethod def http_mplx_protos() -> List[str]: # http multiplexing protocols we can test if Env.have_h2_curl(): if Env.have_h3(): - return ['h2', 'h3'] - return ['h2'] + return ["h2", "h3"] + return ["h2"] return [] @staticmethod @@ -572,6 +639,10 @@ class Env: def caddy_version() -> str: return Env.CONFIG.caddy_version + @staticmethod + def h2o_version() -> str: + return Env.CONFIG.h2o_version + @staticmethod def caddy_is_at_least(minv) -> bool: return Env.CONFIG.caddy_is_at_least(minv) @@ -611,21 +682,20 @@ class Env: def __init__(self, pytestconfig=None, env_config=None): if env_config: Env.CONFIG = env_config - self._verbose = pytestconfig.option.verbose \ - if pytestconfig is not None else 0 + self._verbose = pytestconfig.option.verbose if pytestconfig is not None else 0 self._ca = None self._test_timeout = 300.0 if self._verbose > 1 else 60.0 # seconds def issue_certs(self): if self._ca is None: # ca_dir = os.path.join(self.CONFIG.gen_root, 'ca') - ca_dir = os.path.join(self.gen_dir, 'ca') + ca_dir = os.path.join(self.gen_dir, "ca") os.makedirs(ca_dir, exist_ok=True) - lock_file = os.path.join(ca_dir, 'ca.lock') + lock_file = os.path.join(ca_dir, "ca.lock") with FileLock(lock_file): - self._ca = TestCA.create_root(name=self.CONFIG.tld, - store_dir=ca_dir, - key_type="rsa2048") + self._ca = TestCA.create_root( + name=self.CONFIG.tld, store_dir=ca_dir, key_type="rsa2048" + ) self._ca.issue_certs(self.CONFIG.cert_specs) if self.have_openssl(): self._ca.create_hashdir(self.openssl) @@ -714,19 +784,19 @@ class Env: @property def http_port(self) -> int: - return self.CONFIG.ports.get('http', 0) + return self.CONFIG.ports.get("http", 0) @property def https_port(self) -> int: - return self.CONFIG.ports['https'] + return self.CONFIG.ports["https"] @property def https_only_tcp_port(self) -> int: - return self.CONFIG.ports['https-tcp-only'] + return self.CONFIG.ports["https-tcp-only"] @property def nghttpx_https_port(self) -> int: - return self.CONFIG.ports['nghttpx_https'] + return self.CONFIG.ports["nghttpx_https"] @property def h3_port(self) -> int: @@ -734,27 +804,35 @@ class Env: @property def proxy_port(self) -> int: - return self.CONFIG.ports['proxy'] + return self.CONFIG.ports["proxy"] @property def proxys_port(self) -> int: - return self.CONFIG.ports['proxys'] + return self.CONFIG.ports["proxys"] @property def ftp_port(self) -> int: - return self.CONFIG.ports['ftp'] + return self.CONFIG.ports["ftp"] @property def ftps_port(self) -> int: - return self.CONFIG.ports['ftps'] + return self.CONFIG.ports["ftps"] @property def h2proxys_port(self) -> int: - return self.CONFIG.ports['h2proxys'] + return self.CONFIG.ports["h2proxys"] - def pts_port(self, proto: str = 'http/1.1') -> int: + @property + def h3proxys_port(self) -> int: + return self.CONFIG.ports["h3proxys"] + + def pts_port(self, proto: str = "http/1.1") -> int: # proxy tunnel port - return self.CONFIG.ports['h2proxys' if proto == 'h2' else 'proxys'] + if proto == "h3": + return self.CONFIG.ports["h3proxys"] + if proto == "h2": + return self.CONFIG.ports["h2proxys"] + return self.CONFIG.ports["proxys"] @property def caddy(self) -> str: @@ -762,11 +840,11 @@ class Env: @property def caddy_https_port(self) -> int: - return self.CONFIG.ports['caddys'] + return self.CONFIG.ports["caddys"] @property def caddy_http_port(self) -> int: - return self.CONFIG.ports['caddy'] + return self.CONFIG.ports["caddy"] @property def danted(self) -> str: @@ -778,7 +856,7 @@ class Env: @property def ws_port(self) -> int: - return self.CONFIG.ports['ws'] + return self.CONFIG.ports["ws"] @property def curl(self) -> str: @@ -802,58 +880,65 @@ class Env: @property def slow_network(self) -> bool: - return "CURL_DBG_SOCK_WBLOCK" in os.environ or \ - "CURL_DBG_SOCK_WPARTIAL" in os.environ + return ( + "CURL_DBG_SOCK_WBLOCK" in os.environ + or "CURL_DBG_SOCK_WPARTIAL" in os.environ + ) @property def ci_run(self) -> bool: return "CURL_CI" in os.environ def port_for(self, alpn_proto: Optional[str] = None): - if alpn_proto is None or \ - alpn_proto in ['h2', 'http/1.1', 'http/1.0', 'http/0.9']: + if alpn_proto is None or alpn_proto in [ + "h2", + "http/1.1", + "http/1.0", + "http/0.9", + ]: return self.https_port - if alpn_proto in ['h3']: + if alpn_proto in ["h3"]: return self.h3_port return self.http_port def authority_for(self, domain: str, alpn_proto: Optional[str] = None): - return f'{domain}:{self.port_for(alpn_proto=alpn_proto)}' + return f"{domain}:{self.port_for(alpn_proto=alpn_proto)}" - def make_data_file(self, indir: str, fname: str, fsize: int, - line_length: int = 1024) -> str: + def make_data_file( + self, indir: str, fname: str, fsize: int, line_length: int = 1024 + ) -> str: if line_length < 11: - raise RuntimeError('line_length less than 11 not supported') + raise RuntimeError("line_length less than 11 not supported") fpath = os.path.join(indir, fname) s10 = "0123456789" s = round((line_length / 10) + 1) * s10 - s = s[0:line_length-11] - with open(fpath, 'w') as fd: + s = s[0 : line_length - 11] + with open(fpath, "w") as fd: for i in range(int(fsize / line_length)): fd.write(f"{i:09d}-{s}\n") remain = int(fsize % line_length) if remain != 0: i = int(fsize / line_length) + 1 - fd.write(f"{i:09d}-{s}"[0:remain-1] + "\n") + fd.write(f"{i:09d}-{s}"[0 : remain - 1] + "\n") return fpath def make_data_gzipbomb(self, indir: str, fname: str, fsize: int) -> str: fpath = os.path.join(indir, fname) - gzpath = f'{fpath}.gz' - varpath = f'{fpath}.var' + gzpath = f"{fpath}.gz" + varpath = f"{fpath}.var" - with open(fpath, 'w') as fd: - fd.write('not what we are looking for!\n') + with open(fpath, "w") as fd: + fd.write("not what we are looking for!\n") count = int(fsize / 1024) zero1k = bytearray(1024) - with gzip.open(gzpath, 'wb') as fd: + with gzip.open(gzpath, "wb") as fd: for _ in range(count): fd.write(zero1k) - with open(varpath, 'w') as fd: - fd.write(f'URI: {fname}\n') - fd.write('\n') - fd.write(f'URI: {fname}.gz\n') - fd.write('Content-Type: text/plain\n') - fd.write('Content-Encoding: x-gzip\n') - fd.write('\n') + with open(varpath, "w") as fd: + fd.write(f"URI: {fname}\n") + fd.write("\n") + fd.write(f"URI: {fname}.gz\n") + fd.write("Content-Type: text/plain\n") + fd.write("Content-Encoding: x-gzip\n") + fd.write("\n") return fpath diff --git a/tests/http/testenv/h2o.py b/tests/http/testenv/h2o.py new file mode 100644 index 0000000000..6a55f4882b --- /dev/null +++ b/tests/http/testenv/h2o.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# *************************************************************************** +# _ _ ____ _ +# 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 +# +########################################################################### +# +import logging +import os +import signal +import socket +import subprocess +import time +from datetime import datetime, timedelta +from typing import Dict, Optional + +from .curl import CurlClient +from .env import Env +from .ports import alloc_ports_and_do + +log = logging.getLogger(__name__) + + +class H2o: + def __init__(self, env: Env, name: str, domain: str, cred_name: str): + self.env = env + self._name = name + self._domain = domain + self._port = 0 # defaults to h3_port + self._cred_name = cred_name + self._loaded_cred_name = None + self._process = None + self._tmp_dir = os.path.join(self.env.gen_dir, self._name) + self._run_dir = os.path.join(self._tmp_dir, "run") + self._conf_file = os.path.join(self._run_dir, "h2o.conf") + self._error_log = os.path.join(self._run_dir, "h2o.log") + self._pid_file = os.path.join(self._run_dir, "h2o.pid") + self._stderr = os.path.join(self._run_dir, "h2o.stderr") + self._cmd = env.CONFIG.h2o + # For proxy subclasses + self._h1_port = None + self._h2_port = None + + @property + def port(self) -> int: + return self._port + + @property + def h1_port(self) -> Optional[int]: + return getattr(self, "_h1_port", None) + + @property + def h2_port(self) -> Optional[int]: + return getattr(self, "_h2_port", None) + + def clear_logs(self): + self._rmf(self._error_log) + self._rmf(self._stderr) + + def dump_logs(self): + lines = [] + lines.append(f"stderr of {self._name}") + lines.append("-------------------------------------------") + self._dump_file(self._stderr, lines) + lines.append("") + lines.append(f"errorlog of {self._name}") + lines.append("-------------------------------------------") + self._dump_file(self._error_log, lines) + lines.append("") + return lines + + def _rmf(self, path): + if os.path.isfile(path): + os.remove(path) + return + + def _dump_file(self, path, lines): + if os.path.isfile(path): + with open(path) as fd: + for line in fd: + lines.append(line.rstrip()) + + def _mkpath(self, path): + if not os.path.exists(path): + os.makedirs(path) + return + + def _log(self, level, msg): + getattr(log, level)(f"[{self._name}] {msg}") + + def is_running(self): + if self._process: + self._process.poll() + return self._process.returncode is None + return False + + def initial_start(self): + self._rmf(self._pid_file) + self._rmf(self._error_log) + self._mkpath(self._run_dir) + self.write_config() + + def start(self, wait_live=True): + self._mkpath(self._tmp_dir) + self._mkpath(self._run_dir) + if self._process: + self.stop() + self._loaded_cred_name = self._cred_name + self.write_config() + args = [self._cmd, "-c", self._conf_file] + ngerr = open(self._stderr, "a") + self._process = subprocess.Popen(args=args, stderr=ngerr) + if self._process.returncode is not None: + return False + if wait_live: + time.sleep(1) + # fail fast if h2o rejected the config and already exited + self._process.poll() + if self._process.returncode is not None: + self._log("error", + f"h2o exited early (rc={self._process.returncode})" + f" - check {self._stderr} for details") + self._process = None + return False + return not wait_live or self.wait_for_state( + live=True, timeout=timedelta(seconds=Env.SERVER_TIMEOUT) + ) + + def stop(self, wait_dead=True): + self._mkpath(self._tmp_dir) + if self._process: + self._process.terminate() + try: + self._process.wait(timeout=5) + except subprocess.TimeoutExpired: + self._process.kill() + self._process.wait(timeout=2) + self._process = None + return not wait_dead or self.wait_for_state( + live=False, timeout=timedelta(seconds=5) + ) + return True + + def restart(self): + self.stop() + return self.start() + + def reload(self, timeout: timedelta = timedelta(seconds=Env.SERVER_TIMEOUT)): + if self._process: + running = self._process + self._process = None + os.kill(running.pid, signal.SIGQUIT) + end_wait = datetime.now() + timedelta(seconds=5) + exited = False + if not self.start(wait_live=False): + self._process = running + return False + while datetime.now() < end_wait: + try: + self._log("debug", f"waiting for h2o({running.pid}) to exit.") + running.wait(1) + self._log( + "debug", + f"h2o({running.pid}) terminated -> {running.returncode}", + ) + exited = True + break + except subprocess.TimeoutExpired: + self._log("warning", f"h2o({running.pid}), not shut down yet.") + os.kill(running.pid, signal.SIGQUIT) + if not exited and datetime.now() >= end_wait: + self._log("error", f"h2o({running.pid}), terminate forcefully.") + os.kill(running.pid, signal.SIGKILL) + running.terminate() + running.wait(1) + return self.wait_for_state(live=True, timeout=timeout) + return False + + def wait_for_state( + self, + live: bool, + timeout: timedelta, + url: Optional[str] = None, + log_prefix: str = "h2o", + ): + curl = CurlClient(env=self.env, run_dir=self._tmp_dir) + try_until = datetime.now() + timeout + if url is None: + url = f"https://{self._domain}:{self._port}/" + while datetime.now() < try_until: + if live: + r = curl.http_get( + url=url, extra_args=["--trace", "curl.trace", "--trace-time"] + ) + if r.exit_code == 0: + return True + else: + r = curl.http_get(url=url) + if r.exit_code != 0: + return True + time.sleep(0.1) + if live: + self._log("error", f"Server still not responding after {timeout}") + else: + self._log("debug", f"Server still responding after {timeout}") + return False + + def write_config(self): + # To be overridden by subclasses + with open(self._conf_file, "w") as fd: + fd.write("# h2o test config\n") + + +class H2oServer(H2o): + """h2o HTTP/3 server for testing.""" + + PORT_SPECS = { + "h2o_https": socket.SOCK_STREAM, + } + + def __init__(self, env: Env): + super().__init__( + env=env, name="h2o-server", domain=env.domain1, cred_name=env.domain1 + ) + + def initial_start(self): + super().initial_start() + + def startup(ports: Dict[str, int]) -> bool: + self._port = ports["h2o_https"] + if self.start(): + self.env.update_ports(ports) + return True + self.stop() + self._port = 0 + return False + + return alloc_ports_and_do( + H2oServer.PORT_SPECS, startup, self.env.gen_root, max_tries=3 + ) + + def write_config(self): + creds = self.env.get_credentials(self._cred_name) + assert creds # convince pytype this is not None + doc_root = os.path.join(self.env.gen_dir, "docs") + self._mkpath(doc_root) + self._mkpath(self._run_dir) + # Create a simple test file + with open(os.path.join(doc_root, "data.json"), "w") as f: + f.write('{"message": "Hello from h2o HTTP/3 server"}\n') + with open(self._conf_file, "w") as fd: + fd.write(f"""# h2o HTTP/3 server configuration +server-name: "h2o-test-server" +num-threads: 1 + +listen: &ssl_listen + port: {self._port} + ssl: + certificate-file: {creds.cert_file} + key-file: {creds.pkey_file} + neverbleed: OFF + minimum-version: TLSv1.2 + ocsp-update-interval: 0 + +listen: + <<: *ssl_listen + type: quic + +hosts: + "{self._domain}": + paths: + "/": + file.dir: {doc_root} + +http2-reprioritize-blocking-assets: ON + +access-log: {self._run_dir}/access.log +error-log: {self._error_log} +""") + + +class H2oProxy(H2o): + """h2o MASQUE proxy for testing.""" + + def __init__(self, env: Env): + super().__init__( + env=env, + name="h2o-proxy", + domain=env.proxy_domain, + cred_name=env.proxy_domain, + ) + + def initial_start(self): + super().initial_start() + + def startup(ports: Dict[str, int]) -> bool: + self._port = ports["h3proxys"] + self._h2_port = ports["h2proxys"] + self._h1_port = ports["proxys"] + if self.start(): + self.env.update_ports(ports) + return True + self.stop() + self._port = 0 + self._h2_port = 0 + self._h1_port = 0 + return False + + return alloc_ports_and_do( + { + "h3proxys": socket.SOCK_DGRAM, + "h2proxys": socket.SOCK_STREAM, + "proxys": socket.SOCK_STREAM, + }, + startup, + self.env.gen_root, + max_tries=3, + ) + + def write_config(self): + creds = self.env.get_credentials(self._cred_name) + assert creds # convince pytype this is not None + self._mkpath(self._run_dir) + with open(self._conf_file, "w") as fd: + fd.write(f"""# h2o MASQUE proxy configuration +server-name: "h2o-test-proxy" +num-threads: 1 + +proxy.tunnel: ON + +# HTTP/1.1 proxy listener +listen: &h1_listen + port: {getattr(self, "_h1_port", self._port)} + ssl: + certificate-file: {creds.cert_file} + key-file: {creds.pkey_file} + neverbleed: OFF + minimum-version: TLSv1.2 + ocsp-update-interval: 0 + +# HTTP/2 proxy listener +listen: &h2_listen + port: {getattr(self, "_h2_port", self._port)} + ssl: + certificate-file: {creds.cert_file} + key-file: {creds.pkey_file} + neverbleed: OFF + minimum-version: TLSv1.2 + ocsp-update-interval: 0 + +# HTTP/3 proxy listener (main port) +listen: &h3_listen + port: {self._port} + ssl: + certificate-file: {creds.cert_file} + key-file: {creds.pkey_file} + neverbleed: OFF + minimum-version: TLSv1.2 + ocsp-update-interval: 0 + +# QUIC listener for HTTP/3 +listen: + <<: *h3_listen + type: quic + +hosts: + "{self._domain}": + paths: + "/": + proxy.connect: [+*] + proxy.ssl.verify-peer: OFF + "/.well-known/masque/udp": + proxy.connect-udp: [+*] + proxy.ssl.verify-peer: OFF + +http2-reprioritize-blocking-assets: ON + +access-log: {self._run_dir}/access.log +error-log: {self._error_log} +""") + + def wait_for_state( + self, + live: bool, + timeout: timedelta, + url: Optional[str] = None, + log_prefix: str = "h2o", + ): + curl = CurlClient(env=self.env, run_dir=self._tmp_dir) + try_until = datetime.now() + timeout + if url is None: + url = f"https://{self.env.proxy_domain}:{self._port}/" + while datetime.now() < try_until: + if live: + r = curl.http_get( + url=url, extra_args=["--trace", "curl.trace", "--trace-time"] + ) + if r.exit_code == 0: + return True + else: + r = curl.http_get(url=url) + if r.exit_code != 0: + return True + time.sleep(0.1) + if live: + self._log("error", f"Proxy still not responding after {timeout}") + else: + self._log("debug", f"Proxy still responding after {timeout}") + return False diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc index c8eccd27ad..c6c75c781d 100644 --- a/tests/unit/Makefile.inc +++ b/tests/unit/Makefile.inc @@ -47,4 +47,4 @@ TESTS_C = \ unit2600.c unit2601.c unit2602.c unit2603.c unit2604.c unit2605.c \ unit3200.c unit3205.c \ unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c unit3219.c \ - unit3300.c unit3301.c unit3302.c unit3303.c unit3304.c + unit3300.c unit3301.c unit3302.c unit3303.c unit3304.c unit3400.c diff --git a/tests/unit/unit3400.c b/tests/unit/unit3400.c new file mode 100644 index 0000000000..e48dd3c1a7 --- /dev/null +++ b/tests/unit/unit3400.c @@ -0,0 +1,268 @@ +/*************************************************************************** + * _ _ ____ _ + * 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 "unitcheck.h" + +#include "bufq.h" +#include "capsule.h" + +#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \ + !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) +static void queue_bytes(struct bufq *q, const unsigned char *src, size_t len) +{ + size_t nwritten = 0; + CURLcode result = Curl_bufq_write(q, src, len, &nwritten); + fail_unless(result == CURLE_OK, "queue failed"); + fail_unless(nwritten == len, "queue short write"); +} +#endif + +#if defined(USE_PROXY_HTTP3) && \ + !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) +static void check_capsule_hdr(size_t payload_len, + const unsigned char *expected, + size_t expected_len) +{ + unsigned char hdr[HTTP_CAPSULE_HEADER_MAX_SIZE]; + size_t hdr_len; + + memset(hdr, 0xA5, sizeof(hdr)); + hdr_len = Curl_capsule_encap_udp_hdr(hdr, sizeof(hdr), payload_len); + fail_unless(hdr_len == expected_len, "capsule header length mismatch"); + fail_unless(!memcmp(hdr, expected, expected_len), + "capsule header bytes mismatch"); +} + +static void test_capsule_encap_udp_hdr_boundaries(void) +{ + const unsigned char p0[] = { 0x00, 0x01, 0x00 }; + const unsigned char p62[] = { 0x00, 0x3F, 0x00 }; + const unsigned char p63[] = { 0x00, 0x40, 0x40, 0x00 }; + const unsigned char p64[] = { 0x00, 0x40, 0x41, 0x00 }; + const unsigned char p16382[] = { 0x00, 0x7F, 0xFF, 0x00 }; + const unsigned char p16383[] = { 0x00, 0x80, 0x00, 0x40, 0x00, 0x00 }; + const unsigned char p16384[] = { 0x00, 0x80, 0x00, 0x40, 0x01, 0x00 }; + + check_capsule_hdr(0, p0, sizeof(p0)); + check_capsule_hdr(62, p62, sizeof(p62)); + check_capsule_hdr(63, p63, sizeof(p63)); + check_capsule_hdr(64, p64, sizeof(p64)); + check_capsule_hdr(16382, p16382, sizeof(p16382)); + check_capsule_hdr(16383, p16383, sizeof(p16383)); + check_capsule_hdr(16384, p16384, sizeof(p16384)); +} + +#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ + +#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \ + !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) +static void check_capsule_result(struct bufq *q, + const unsigned char *capsule, size_t capslen, + size_t outlen, CURLcode expect_err, + size_t expect_nread) +{ + unsigned char out[32]; + CURLcode err = CURLE_OK; + size_t nread; + + memset(out, 0, sizeof(out)); + Curl_bufq_reset(q); + if(capsule && capslen) + queue_bytes(q, capsule, capslen); + + nread = Curl_capsule_process_udp_raw(NULL, NULL, q, out, outlen, &err); + fail_unless(err == expect_err, "unexpected capsule error"); + fail_unless(nread == expect_nread, "unexpected capsule read size"); +} + +static void test_capsule_encode_decode_roundtrip(void) +{ + struct dynbuf dyn; + struct bufq q; + unsigned char payload[128]; + unsigned char out[128]; + CURLcode result, err; + size_t payload_len; + size_t i, nread; + + for(i = 0; i < sizeof(payload); ++i) + payload[i] = (unsigned char)i; + + for(i = 0; i < 2; ++i) { + payload_len = i ? 64 : 7; + memset(out, 0, sizeof(out)); + + result = Curl_capsule_encap_udp_datagram(&dyn, payload, payload_len); + fail_unless(result == CURLE_OK, "failed to encapsulate UDP datagram"); + + Curl_bufq_init2(&q, 32, 8, BUFQ_OPT_NONE); + queue_bytes(&q, (const unsigned char *)curlx_dyn_ptr(&dyn), + curlx_dyn_len(&dyn)); + + err = CURLE_OK; + nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), + &err); + fail_unless(err == CURLE_OK, "failed to decode UDP datagram"); + fail_unless(nread == payload_len, "decoded payload length mismatch"); + fail_unless(!memcmp(out, payload, payload_len), + "decoded payload bytes mismatch"); + fail_unless(Curl_bufq_is_empty(&q), "decoded capsule must be consumed"); + + Curl_bufq_free(&q); + curlx_dyn_free(&dyn); + } +} + +static void test_capsule_sequential_decode(void) +{ + /* Verify that multiple back-to-back capsules in the same bufq are + each decoded in turn and the buffer is fully consumed. */ + struct bufq q; + unsigned char out[8]; + CURLcode err; + size_t nread; + /* Two back-to-back 3-byte UDP capsules */ + const unsigned char two_caps[] = { + 0x00, 0x04, 0x00, 0x11, 0x22, 0x33, /* capsule 1: [0x11,0x22,0x33] */ + 0x00, 0x04, 0x00, 0xAA, 0xBB, 0xCC /* capsule 2: [0xAA,0xBB,0xCC] */ + }; + + Curl_bufq_init2(&q, 32, 4, BUFQ_OPT_NONE); + + queue_bytes(&q, two_caps, sizeof(two_caps)); + + /* First capsule */ + memset(out, 0, sizeof(out)); + err = CURLE_OK; + nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err); + fail_unless(err == CURLE_OK, "sequential: first capsule decode failed"); + fail_unless(nread == 3, "sequential: first capsule size mismatch"); + fail_unless(out[0] == 0x11 && out[1] == 0x22 && out[2] == 0x33, + "sequential: first capsule bytes mismatch"); + + /* Second capsule */ + memset(out, 0, sizeof(out)); + err = CURLE_OK; + nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err); + fail_unless(err == CURLE_OK, "sequential: second capsule decode failed"); + fail_unless(nread == 3, "sequential: second capsule size mismatch"); + fail_unless(out[0] == 0xAA && out[1] == 0xBB && out[2] == 0xCC, + "sequential: second capsule bytes mismatch"); + + /* Buffer must be empty after both capsules */ + fail_unless(Curl_bufq_is_empty(&q), + "sequential: buffer must be empty after two capsules"); + + /* No more data - CURLE_AGAIN expected */ + err = CURLE_OK; + nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err); + fail_unless(err == CURLE_AGAIN, + "sequential: empty queue should return AGAIN"); + fail_unless(nread == 0, "sequential: empty queue should read zero bytes"); + + Curl_bufq_free(&q); +} + +static void test_capsule_decode_paths(void) +{ + struct bufq q; + unsigned char out[8]; + CURLcode err = CURLE_OK; + size_t nread; + const unsigned char invalid_type[] = { 0x01 }; + const unsigned char partial_len[] = { 0x00, 0x40 }; + const unsigned char invalid_context[] = { 0x00, 0x01, 0x01 }; + const unsigned char invalid_caps_len[] = { 0x00, 0x00, 0x00 }; + const unsigned char partial_payload[] = { 0x00, 0x04, 0x00, 0x11, 0x22 }; + const unsigned char payload_3b[] = { 0x00, 0x04, 0x00, 0x11, 0x22, 0x33 }; + const unsigned char payload_empty[] = { 0x00, 0x01, 0x00 }; + + Curl_bufq_init2(&q, 32, 4, BUFQ_OPT_NONE); + + check_capsule_result(&q, NULL, 0, 0, CURLE_BAD_FUNCTION_ARGUMENT, 0); + check_capsule_result(&q, NULL, 0, sizeof(out), CURLE_AGAIN, 0); + check_capsule_result(&q, invalid_type, sizeof(invalid_type), sizeof(out), + CURLE_RECV_ERROR, 0); + check_capsule_result(&q, partial_len, sizeof(partial_len), sizeof(out), + CURLE_AGAIN, 0); + check_capsule_result(&q, invalid_context, sizeof(invalid_context), + sizeof(out), CURLE_RECV_ERROR, 0); + check_capsule_result(&q, invalid_caps_len, sizeof(invalid_caps_len), + sizeof(out), CURLE_RECV_ERROR, 0); + check_capsule_result(&q, partial_payload, sizeof(partial_payload), + sizeof(out), CURLE_AGAIN, 0); + + /* oversized payload is rejected and discarded */ + Curl_bufq_reset(&q); + queue_bytes(&q, payload_3b, sizeof(payload_3b)); + nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, 2, &err); + fail_unless(err == CURLE_RECV_ERROR, + "expected RECV_ERROR for short output buffer"); + fail_unless(nread == 0, "expected zero read on short output buffer"); + fail_unless(Curl_bufq_is_empty(&q), + "oversized capsule must be discarded"); + + /* zero-length UDP payload is accepted and consumed */ + Curl_bufq_reset(&q); + queue_bytes(&q, payload_empty, sizeof(payload_empty)); + nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err); + fail_unless(err == CURLE_OK, "zero-length UDP payload should succeed"); + fail_unless(nread == 0, "zero-length UDP payload should read zero"); + fail_unless(Curl_bufq_is_empty(&q), "zero-length capsule must be consumed"); + + /* normal payload decode */ + Curl_bufq_reset(&q); + queue_bytes(&q, payload_3b, sizeof(payload_3b)); + memset(out, 0, sizeof(out)); + nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err); + fail_unless(err == CURLE_OK, "payload decode should succeed"); + fail_unless(nread == 3, "payload decode size mismatch"); + fail_unless(out[0] == 0x11 && out[1] == 0x22 && out[2] == 0x33, + "payload decode bytes mismatch"); + fail_unless(Curl_bufq_is_empty(&q), "payload capsule must be consumed"); + + Curl_bufq_free(&q); +} +#endif /* USE_NGTCP2 && !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ + +static CURLcode test_unit3400(const char *arg) +{ + UNITTEST_BEGIN_SIMPLE + + (void)arg; + +#if defined(USE_PROXY_HTTP3) && \ + !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + test_capsule_encap_udp_hdr_boundaries(); +#endif + +#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \ + !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + test_capsule_encode_decode_roundtrip(); + test_capsule_decode_paths(); + test_capsule_sequential_decode(); +#endif + + UNITTEST_END_SIMPLE +}