HTTP/3: add proxy CONNECT and MASQUE CONNECT-UDP support (ngtcp2 QUIC)
No binary (build failed)
—
(log yok)
| @@ -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 | |
| @@ -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 | |
| @@ -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} | |
| @@ -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: | |
| @@ -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 | |
| @@ -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 \ | |
| @@ -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`. |
| @@ -0,0 +1,31 @@ | |
| + | --- |
| + | c: Copyright (C) Daniel Stenberg, <[email protected]>, 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`. |
| @@ -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, | |
| @@ -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 | |
| @@ -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. | |
| @@ -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 | |
| @@ -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 | |
| @@ -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 | |
| @@ -62,6 +62,9 @@ Via curl's `configure` script you may specify: | |
| * `--with-test-nghttpx=<path-of-nghttpx>` if you have nghttpx to use | |
| somewhere outside your `$PATH`. | |
| + | * `--with-test-h2o=<path-of-h2o>` if you have h2o to use somewhere |
| + | outside your `$PATH`. |
| + | |
| * `--with-test-httpd=<httpd-install-path>` if you have an Apache httpd | |
| installed somewhere else. On Debian/Ubuntu it otherwise looks into | |
| `/usr/bin` and `/usr/sbin` to find those. | |
| @@ -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 | |
| @@ -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 \ | |
| @@ -0,0 +1,281 @@ | |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 <curl/curl.h> |
| + | #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 */ |
| @@ -0,0 +1,77 @@ | |
| + | #ifndef HEADER_CURL_CAPSULE_H |
| + | #define HEADER_CURL_CAPSULE_H |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 */ |
| @@ -0,0 +1,253 @@ | |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 <curl/curl.h> |
| + | #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 */ |
| @@ -0,0 +1,40 @@ | |
| + | #ifndef HEADER_CURL_CF_CAPSULE_H |
| + | #define HEADER_CURL_CF_CAPSULE_H |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 */ |
| @@ -25,6 +25,8 @@ | |
| #if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) | |
| + | |
| + | #include <curl/curl.h> |
| #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); | |
| @@ -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; | |
| @@ -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; |
| - | |
| - | 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; |
| - | } |
| - | |
| - | 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"); |
| - | } |
| + | struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| + | proxy_inspect_result res; |
| + | CURLcode result; |
| - | 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; |
| - | } |
| + | 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; |
| } | |
| - | |
| - | /* 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 |
| @@ -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; | |
| @@ -0,0 +1,3478 @@ | |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 <ngtcp2/ngtcp2.h> |
| + | #include <ngtcp2/ngtcp2_crypto.h> |
| + | #ifdef USE_OPENSSL |
| + | #include <openssl/err.h> |
| + | #if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) |
| + | #include <ngtcp2/ngtcp2_crypto_boringssl.h> |
| + | #elif defined(OPENSSL_QUIC_API2) |
| + | #include <ngtcp2/ngtcp2_crypto_ossl.h> |
| + | #else |
| + | #include <ngtcp2/ngtcp2_crypto_quictls.h> |
| + | #endif |
| + | #include "vtls/openssl.h" |
| + | #endif /* USE_OPENSSL */ |
| + | |
| + | #include <nghttp3/nghttp3.h> |
| + | |
| + | #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 <v1.22.0 */ |
| + | static int cb_ngtcp2_get_new_connection_id(ngtcp2_conn *tconn, ngtcp2_cid *cid, |
| + | uint8_t *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, 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 <v1.22.0 */ |
| + | NULL, /* remove_connection_id */ |
| + | ngtcp2_crypto_update_key_cb, |
| + | NULL, /* path_validation */ |
| + | NULL, /* select_preferred_addr */ |
| + | cb_ngtcp2_stream_reset, |
| + | NULL, /* extend_max_remote_streams_bidi */ |
| + | NULL, /* extend_max_remote_streams_uni */ |
| + | cb_ngtcp2_extend_max_stream_data, |
| + | NULL, /* dcid_status */ |
| + | NULL, /* handshake_confirmed */ |
| + | NULL, /* recv_new_token */ |
| + | ngtcp2_crypto_delete_crypto_aead_ctx_cb, |
| + | ngtcp2_crypto_delete_crypto_cipher_ctx_cb, |
| + | NULL, /* recv_datagram */ |
| + | NULL, /* ack_datagram */ |
| + | NULL, /* lost_datagram */ |
| + | ngtcp2_crypto_get_path_challenge_data_cb, |
| + | cb_ngtcp2_stream_stop_sending, |
| + | NULL, /* version_negotiation */ |
| + | cb_ngtcp2_recv_rx_key, /* recv_rx_key */ |
| + | NULL, /* recv_tx_key */ |
| + | NULL, /* early_data_rejected */ |
| + | #ifdef NGTCP2_CALLBACKS_V2 /* ngtcp2 v1.14.0+ */ |
| + | NULL, /* begin_path_validation */ |
| + | #endif /* NGTCP2_CALLBACKS_V2 */ |
| + | #ifdef NGTCP2_CALLBACKS_V3 /* ngtcp2 v1.22.0+ */ |
| + | NULL, /* recv_stateless_reset2 */ |
| + | cb_ngtcp2_get_new_connection_id2, /* get_new_connection_id2 */ |
| + | NULL, /* dcid_status2 */ |
| + | ngtcp2_crypto_get_path_challenge_data2_cb, /* get_path_challenge_data2 */ |
| + | #endif /* NGTCP2_CALLBACKS_V3 */ |
| + | }; |
| + | |
| + | #if defined(_MSC_VER) && defined(_DLL) |
| + | #pragma warning(pop) |
| + | #endif |
| + | |
| + | static CURLcode cf_ngtcp2_recv_pkts_proxy(const unsigned char *buf, |
| + | size_t buflen, size_t gso_size, |
| + | struct sockaddr_storage *remote_addr, |
| + | socklen_t remote_addrlen, int ecn, |
| + | void *userp) |
| + | { |
| + | struct proxy_pkt_io_ctx *pktx = userp; |
| + | struct cf_h3_proxy_ctx *proxy_ctx = pktx->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 |
| @@ -0,0 +1,42 @@ | |
| + | #ifndef HEADER_CURL_H3_PROXY_H |
| + | #define HEADER_CURL_H3_PROXY_H |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 */ |
| @@ -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 */ |
| @@ -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 */ | |
| @@ -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; | |
| @@ -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 <uv.h> header file. */ | |
| #cmakedefine HAVE_UV_H 1 | |
| @@ -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 */ | |
| @@ -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; | |
| @@ -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); | |
| @@ -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 |
| @@ -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,180 @@ 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) { |
| + | Curl_http_req_free(req); |
| + | req = NULL; |
| + | } |
| + | curlx_free(authority); |
| + | *preq = req; |
| + | 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) { | |
| @@ -238,37 +429,166 @@ out: | |
| 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 /* 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 |
| + | #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) | |
| @@ -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 */ | |
| @@ -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); | |
| @@ -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, | |
| @@ -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: | |
| @@ -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); | |
| @@ -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 | |
| @@ -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; |
| + | } |
| + | |
| + | 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; |
| + | |
| + | 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, &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, |
| - | &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, &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, |
| + | &remote->curl_sa_addr, |
| + | (socklen_t)remote->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; |
| + | } |
| 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 |
| @@ -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 */ | |
| @@ -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, | |
| @@ -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; | |
| @@ -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; | |
| @@ -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, | |
| @@ -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, | |
| @@ -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; | |
| @@ -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 |
| @@ -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 */ | |
| @@ -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, | |
| @@ -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 }, | |
| @@ -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 \ | |
| @@ -0,0 +1,19 @@ | |
| + | <?xml version="1.0" encoding="US-ASCII"?> |
| + | <testcase> |
| + | <info> |
| + | <keywords> |
| + | unittest |
| + | capsule |
| + | </keywords> |
| + | </info> |
| + | |
| + | <client> |
| + | <features> |
| + | unittest |
| + | </features> |
| + | <name> |
| + | capsule protocol encode and decode unit tests |
| + | </name> |
| + | </client> |
| + | |
| + | </testcase> |
| @@ -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 "") | |
| @@ -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: | |
| @@ -44,3 +44,6 @@ danted = @DANTED@ | |
| [sshd] | |
| sshd = @SSHD@ | |
| sftpd = @SFTPD@ | |
| + | |
| + | [h2o] |
| + | h2o = @H2O@ |
| @@ -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 |
| @@ -0,0 +1,689 @@ | |
| + | #!/usr/bin/env python3 |
| + | # -*- coding: utf-8 -*- |
| + | # *************************************************************************** |
| + | # _ _ ____ _ |
| + | # Project ___| | | | _ \| | |
| + | # / __| | | | |_) | | |
| + | # | (__| |_| | _ <| |___ |
| + | # \___|\___/|_| \_\_____| |
| + | # |
| + | # Copyright (C) Daniel Stenberg, <[email protected]>, 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) |
| @@ -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}/', | |
| @@ -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<version>\S+) (?P<os>\S+) (?P<libs>.*)$', line) |
| + | if line.startswith("curl "): |
| + | self.curl_props["version_string"] = line |
| + | m = re.match(r"^curl (?P<version>\S+) (?P<os>\S+) (?P<libs>.*)$", 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=<path>`' |
| + | return "httpd not configured, see `--with-test-httpd=<path>`" |
| 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"] |
| + | |
| + | @property |
| + | def h3proxys_port(self) -> int: |
| + | return self.CONFIG.ports["h3proxys"] |
| - | def pts_port(self, proto: str = 'http/1.1') -> int: |
| + | 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 | |
| @@ -0,0 +1,428 @@ | |
| + | #!/usr/bin/env python3 |
| + | # -*- coding: utf-8 -*- |
| + | # *************************************************************************** |
| + | # _ _ ____ _ |
| + | # Project ___| | | | _ \| | |
| + | # / __| | | | |_) | | |
| + | # | (__| |_| | _ <| |___ |
| + | # \___|\___/|_| \_\_____| |
| + | # |
| + | # Copyright (C) Daniel Stenberg, <[email protected]>, 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 |
| @@ -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 |
| @@ -0,0 +1,268 @@ | |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 |
| + | } |