cyassl
Cygwin
daniel
+datagrams
datatracker
dbg
+decapsulation
Debian
DEBUGBUILD
decrypt
EHLO
EINTR
else's
+encapsulation
encodings
enctype
endianness
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)
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()
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
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
)
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
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
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
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}
- 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:
- `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
proxy-digest.md \
proxy-header.md \
proxy-http2.md \
+ proxy-http3.md \
proxy-insecure.md \
proxy-key-type.md \
proxy-key.md \
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
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`.
--- /dev/null
+---
+c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, 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`.
`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.
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).
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,
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
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.
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
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
--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
* `--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.
#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 */
/*
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
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 \
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 \
--- /dev/null
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+
+#include "curl_setup.h"
+
+#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 */
--- /dev/null
+#ifndef HEADER_CURL_CAPSULE_H
+#define HEADER_CURL_CAPSULE_H
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+
+#include "curl_setup.h"
+
+#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 */
--- /dev/null
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "curl_setup.h"
+
+#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 */
--- /dev/null
+#ifndef HEADER_CURL_CF_CAPSULE_H
+#define HEADER_CURL_CF_CAPSULE_H
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "curl_setup.h"
+
+#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 */
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
+
+#include <curl/curl.h>
#include "urldata.h"
#include "curlx/dynbuf.h"
#include "sendf.h"
#include "http_proxy.h"
#include "select.h"
#include "progress.h"
+#include "multiif.h"
#include "cfilters.h"
#include "cf-h1-proxy.h"
#include "connect.h"
#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 */
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);
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)
ts->close_connection = FALSE;
ts->maybe_folded = FALSE;
ts->leading_unfold = FALSE;
+ ts->nsent = 0;
+ ts->headerlines = 0;
return CURLE_OK;
}
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();
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;
}
}
}
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;
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,
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;
}
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,
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);
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 */
} 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:
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;
result = tunnel_init(cf, data, &ts);
if(result)
return result;
- cf->ctx = ts;
+ pctx->ts = ts;
}
/* We want "seamless" operations through HTTP proxy tunnel */
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;
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) {
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;
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,
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;
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);
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;
#include "sendf.h"
#include "select.h"
#include "cf-h2-proxy.h"
+#include "capsule.h"
#define PROXY_H2_CHUNK_SIZE (16 * 1024)
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);
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;
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:
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();
BIT(rcvd_goaway);
BIT(sent_goaway);
BIT(nw_out_blocked);
+ BIT(udp_tunnel);
};
/* How to access `call_data` from a cf_h2 filter */
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);
}
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) {
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,
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:
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;
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;
}
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. */
}
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. */
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;
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)
}
#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
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;
--- /dev/null
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+
+#include "curl_setup.h"
+
+#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
--- /dev/null
+#ifndef HEADER_CURL_H3_PROXY_H
+#define HEADER_CURL_H3_PROXY_H
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+
+#include "curl_setup.h"
+
+#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 */
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 */
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 */
#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"
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)
}
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;
}
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;
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;
/* 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
#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"
{ &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 */
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;
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)) {
size_t namelen;
};
-/* keep them sorted by length! */
static const struct name_const H2_NON_FIELD[] = {
{ STRCONST("Host") },
{ STRCONST("Upgrade") },
{
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;
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);
}
#endif /* !CURL_DISABLE_HTTP && USE_NGHTTP2 */
+
+/* Do not leak this filter's call_data accessor in unity builds. */
+#undef CF_CTX_CALL_DATA
#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];
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) {
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 */
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;
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)
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) {
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";
case CURLPROXY_HTTPS2:
alpn = "h2";
break;
+ case CURLPROXY_HTTPS3:
+ alpn = "h3";
+ break;
default:
alpn = "http/1.1";
break;
if(!strcmp(alpn, "http/1.0")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.0");
- result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, 10);
+ result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, 10,
+ (bool)ctx->udp_tunnel);
if(result)
goto out;
}
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;
}
* 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;
}
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;
}
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)
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)
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 */
#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,
break;
case CURLPROXY_HTTPS:
case CURLPROXY_HTTPS2:
+ case CURLPROXY_HTTPS3:
pp.scheme = &Curl_scheme_https;
break;
case CURLPROXY_SOCKS4:
}
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);
#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,
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:
#include "headers.h"
#include "curlx/strerr.h"
#include "curlx/strparse.h"
+#include "peer.h"
/* Now for the protocols */
#include "ftp.h"
#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]) {
{
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;
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;
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;
proxyinfo->proxytype = proxytype;
error:
+ curlx_free(scheme);
curlx_free(proxyuser);
curlx_free(proxypasswd);
curl_url_cleanup(uhp);
#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
#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.
#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.
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 */
{
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);
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);
}
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;
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,
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;
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;
+ }
}
/**
}
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);
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);
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,
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;
#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) {
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
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 */
quiche_config_free(ctx->cfg);
ctx->cfg = NULL;
}
+ Curl_ssl_peer_cleanup(&ctx->peer);
}
static CURLcode cf_flush_egress(struct Curl_cfilter *cf,
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;
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,
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);
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,
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;
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,
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);
}
{
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,
Curl_peer_unlink(&peer->dest);
curlx_safefree(peer->sni);
curlx_safefree(peer->scache_key);
+ peer->transport = TRNSPRT_NONE;
peer->type = CURL_SSL_PEER_DNS;
}
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;
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 {
#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
{"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},
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;
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 */
C_PROXY_DIGEST,
C_PROXY_HEADER,
C_PROXY_HTTP2,
+ C_PROXY_HTTP3,
C_PROXY_INSECURE,
C_PROXY_KEY,
C_PROXY_KEY_TYPE,
{ " --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 },
\
test3300 test3301 test3302 test3303 test3304 \
\
+test3400 \
+\
test4000 test4001
EXTRA_DIST = $(TESTCASES) DISABLED data-xml1 data320.html \
--- /dev/null
+<?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>
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 "")
testenv/dnsd.py \
testenv/dante.py \
testenv/env.py \
+ testenv/h2o.py \
testenv/httpd.py \
testenv/mod_curltest/mod_curltest.c \
testenv/nghttpx.py \
test_40_socks.py \
test_50_scp.py \
test_51_sftp.py \
+ test_60_h3_proxy.py \
$(TESTENV)
clean-local:
[sshd]
sshd = @SSHD@
sftpd = @SFTPD@
+
+[h2o]
+h2o = @H2O@
-#***************************************************************************
+# ***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
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__)
# 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())
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
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()
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
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+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)
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,
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}/',
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-#***************************************************************************
+# ***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
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
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
# 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
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}"
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
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
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
# 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:
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):
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()
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:
@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
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)
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)
@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:
@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:
@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:
@property
def ws_port(self) -> int:
- return self.CONFIG.ports['ws']
+ return self.CONFIG.ports["ws"]
@property
def curl(self) -> str:
@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
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+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
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
--- /dev/null
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+
+#include "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
+}