HTTP/3: add proxy CONNECT and MASQUE CONNECT-UDP support (ngtcp2 QUIC)

This patch adds two major proxy capabilities to curl (ngtcp2 QUIC):
- HTTP/3 Proxy CONNECT: Tunnel HTTP/1.1 or HTTP/2 traffic through an
  HTTPS proxy that speaks HTTP/3 (QUIC) using the standard CONNECT
  method over an HTTP/3 connection.
- MASQUE CONNECT-UDP: Tunnel HTTP/3 (QUIC) traffic through an HTTP
  proxy (speaking HTTP/1.1, HTTP/2, or HTTP/3) using the extended
  CONNECT method with the CONNECT-UDP protocol (RFC9297 & RFC9298).

Public API additions:
- `CURLPROXY_HTTPS3`: new proxy type constant for HTTP/3 proxy
- `--proxy-http3`: new CLI flag to negotiate HTTP/3 with HTTPS proxy

The implementation adds two new filters:
- `H3-PROXY` - enables negotiating HTTP/3 (QUIC) to the proxy and
  running CONNECT/CONNECT-UDP through that proxy transport.
- `CAPSULE` - dedicated filter inserted between QUIC transport and
  HTTP-PROXY to handle datagram capsule encapsulation/decapsulation.

Here is how the curl filter chaining looks in different scenarios:
- HTTP/3 Proxy CONNECT (tunneling TCP protocols over QUIC proxy):
  conn -> HTTP/1.1 or HTTP/2  -> SSL -> HTTP-PROXY ->
                                 H3-PROXY -> HAPPY-EYEBALLS -> UDP
- MASQUE CONNECT-UDP (tunneling QUIC over any proxy):
  conn -> HTTP/3 -> CAPSULE -> HTTP-PROXY -> H3-PROXY ->
                               HAPPY-EYEBALLS -> UDP
  conn -> HTTP/3 -> CAPSULE -> HTTP-PROXY -> H1-PROXY or H2-PROXY ->
                               SSL -> HAPPY-EYEBALLS -> TCP

- Both features currently require the ngtcp2 QUIC backend.
- Both features are experimental (disabled by default). Enable with
  `--enable-proxy-http3`(autotools) or `-DUSE_PROXY_HTTP3=ON`(CMake).

Tests:
- tests/unit/unit3400.c: Unit tests for capsule protocol encode/decode
- tests/http/test_60_h3_proxy.py: Comprehensive pytest integration suite
- tests/http/testenv/h2o.py: Managing h2o instances with HTTP/1.1, HTTP/2,
  and HTTP/3 (QUIC) listeners, proxy.connect and proxy.connect-udp enabled.

References:
  RFC 9297 - HTTP Datagrams and the Capsule Protocol
  RFC 9298 - Proxying UDP in HTTP
  RFC 9000 §16 — Variable-Length Integer Encoding

Signed-off-by: Aritra Basu <aritrbas+gh@cisco.com>

Closes #21153
This commit is contained in:
Aritra Basu 2026-04-27 19:35:38 -04:00 committed by Daniel Stenberg
parent efc3f2309e
commit e78b1b3ecc
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
66 changed files with 7401 additions and 473 deletions

View file

@ -167,8 +167,10 @@ CWE
cyassl cyassl
Cygwin Cygwin
daniel daniel
datagrams
datatracker datatracker
dbg dbg
decapsulation
Debian Debian
DEBUGBUILD DEBUGBUILD
decrypt decrypt
@ -234,6 +236,7 @@ EGD
EHLO EHLO
EINTR EINTR
else's else's
encapsulation
encodings encodings
enctype enctype
endianness endianness

View file

@ -1118,6 +1118,8 @@ if(USE_SSLS_EXPORT)
endif() endif()
endif() endif()
option(USE_PROXY_HTTP3 "Enable experimental HTTP/3 proxy support" OFF)
option(USE_NGHTTP2 "Use nghttp2 library" ON) option(USE_NGHTTP2 "Use nghttp2 library" ON)
if(USE_NGHTTP2) if(USE_NGHTTP2)
find_package(NGHTTP2 MODULE) find_package(NGHTTP2 MODULE)
@ -1186,6 +1188,20 @@ if(USE_QUICHE)
endif() endif()
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)) if(NOT CURL_DISABLE_SRP AND (HAVE_GNUTLS_SRP OR HAVE_OPENSSL_SRP))
set(USE_TLS_SRP 1) set(USE_TLS_SRP 1)
endif() endif()
@ -2046,6 +2062,7 @@ curl_add_if("NTLM" CURL_ENABLE_NTLM AND
curl_add_if("TLS-SRP" USE_TLS_SRP) curl_add_if("TLS-SRP" USE_TLS_SRP)
curl_add_if("HTTP2" USE_NGHTTP2) curl_add_if("HTTP2" USE_NGHTTP2)
curl_add_if("HTTP3" USE_NGTCP2 OR USE_QUICHE) 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("MultiSSL" CURL_WITH_MULTI_SSL)
curl_add_if("HTTPS-proxy" NOT CURL_DISABLE_PROXY AND _ssl_enabled AND (USE_OPENSSL OR USE_GNUTLS 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 OR USE_SCHANNEL OR USE_RUSTLS OR USE_MBEDTLS OR

View file

@ -54,6 +54,30 @@ CURL_CHECK_OPTION_RT
CURL_CHECK_OPTION_HTTPSRR CURL_CHECK_OPTION_HTTPSRR
CURL_CHECK_OPTION_ECH CURL_CHECK_OPTION_ECH
CURL_CHECK_OPTION_SSLS_EXPORT 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 XC_CHECK_PATH_SEPARATOR
@ -318,6 +342,22 @@ AS_HELP_STRING([--with-test-caddy=PATH],[where to find caddy for testing]),
) )
AC_SUBST(CADDY) 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 if test -x /usr/sbin/vsftpd; then
VSFTPD=/usr/sbin/vsftpd VSFTPD=/usr/sbin/vsftpd
elif test -x /usr/local/sbin/vsftpd; then elif test -x /usr/local/sbin/vsftpd; then
@ -5028,6 +5068,28 @@ if test "$want_ssls_export" != "no"; then
fi fi
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 ************************************************************
dnl hiding of library internal symbols dnl hiding of library internal symbols
dnl dnl
@ -5141,6 +5203,10 @@ if test "$curl_psl_msg" = "enabled"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES PSL" SUPPORT_FEATURES="$SUPPORT_FEATURES PSL"
fi fi
if test "$USE_PROXY_HTTP3" = "1"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES PROXY-HTTP3"
fi
if test "$curl_gsasl_msg" = "enabled"; then if test "$curl_gsasl_msg" = "enabled"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES gsasl" SUPPORT_FEATURES="$SUPPORT_FEATURES gsasl"
fi fi
@ -5485,6 +5551,7 @@ AC_MSG_NOTICE([Configured to build curl/libcurl:
HTTP1: ${curl_h1_msg} HTTP1: ${curl_h1_msg}
HTTP2: ${curl_h2_msg} HTTP2: ${curl_h2_msg}
HTTP3: ${curl_h3_msg} HTTP3: ${curl_h3_msg}
Proxy-HTTP3: ${curl_proxy_http3_msg}
ECH: ${curl_ech_msg} ECH: ${curl_ech_msg}
HTTPS RR: ${curl_httpsrr_msg} HTTPS RR: ${curl_httpsrr_msg}
SSLS-EXPORT: ${curl_ssls_export_msg} SSLS-EXPORT: ${curl_ssls_export_msg}

View file

@ -43,6 +43,16 @@ Graduation requirements:
- Using HTTP/3 with the given build should perform without risking busy-loops - 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 ### The Rustls backend
Graduation requirements: Graduation requirements:

View file

@ -254,6 +254,7 @@ target_link_libraries(my_target PRIVATE CURL::libcurl)
- `USE_SSLS_EXPORT`: Enable experimental SSL session import/export. Default: `OFF` - `USE_SSLS_EXPORT`: Enable experimental SSL session import/export. Default: `OFF`
- `USE_WIN32_IDN`: Use WinIDN for IDN support. Default: `OFF` - `USE_WIN32_IDN`: Use WinIDN for IDN support. Default: `OFF`
- `USE_WIN32_LDAP`: Use Windows LDAP implementation. Default: `ON` - `USE_WIN32_LDAP`: Use Windows LDAP implementation. Default: `ON`
- `USE_PROXY_HTTP3`: Enable experimental HTTP/3 proxy support. Default: `OFF`
## Disabling features ## Disabling features

View file

@ -212,6 +212,7 @@ DPAGES = \
proxy-digest.md \ proxy-digest.md \
proxy-header.md \ proxy-header.md \
proxy-http2.md \ proxy-http2.md \
proxy-http3.md \
proxy-insecure.md \ proxy-insecure.md \
proxy-key-type.md \ proxy-key-type.md \
proxy-key.md \ proxy-key.md \

View file

@ -5,7 +5,7 @@ Long: proxy-http2
Tags: Versions HTTP/2 Tags: Versions HTTP/2
Protocols: HTTP Protocols: HTTP
Added: 8.1.0 Added: 8.1.0
Mutexed: Mutexed: proxy-http3
Requires: HTTP/2 Requires: HTTP/2
Help: Use HTTP/2 with HTTPS proxy Help: Use HTTP/2 with HTTPS proxy
Category: http proxy Category: http proxy
@ -22,3 +22,5 @@ Negotiate HTTP/2 with an HTTPS proxy. The proxy might still only offer HTTP/1
and then curl sticks to using that version. and then curl sticks to using that version.
This has no effect for any other kinds of proxies. This has no effect for any other kinds of proxies.
This option is mutually exclusive with `--proxy-http3`.

View file

@ -0,0 +1,31 @@
---
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`.

View file

@ -156,9 +156,9 @@ The currently existing filter types (curl 8.5.0) are:
`accept()`ed in a `listen()` `accept()`ed in a `listen()`
* `SSL`: filter that applies TLS en-/decryption and handshake. Manages the * `SSL`: filter that applies TLS en-/decryption and handshake. Manages the
underlying TLS backend implementation. underlying TLS backend implementation.
* `HTTP-PROXY`, `H1-PROXY`, `H2-PROXY`: the first manages the connection to an * `HTTP-PROXY`, `H1-PROXY`, `H2-PROXY`, `H3-PROXY`: the first manages the
HTTP proxy server and uses the other depending on which ALPN protocol has connection to an HTTP proxy server and uses the other depending on which
been negotiated. ALPN protocol has been negotiated.
* `SOCKS-PROXY`: filter for the various SOCKS proxy protocol variations * `SOCKS-PROXY`: filter for the various SOCKS proxy protocol variations
* `HAPROXY`: filter for the protocol of the same name, providing client IP * `HAPROXY`: filter for the protocol of the same name, providing client IP
information to a server. information to a server.
@ -166,7 +166,7 @@ The currently existing filter types (curl 8.5.0) are:
connection connection
* `HTTP/3`: filter for handling multiplexed transfers over an HTTP/3+QUIC * `HTTP/3`: filter for handling multiplexed transfers over an HTTP/3+QUIC
connection 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. 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 * `SETUP`: meta filter that manages the creation of sub-filter chains for a
specific transport (e.g. TCP or QUIC). specific transport (e.g. TCP or QUIC).
@ -220,6 +220,37 @@ as an `SSL` flagged filter is seen first. `conn3` is also encrypted as the
Similar checks can determine if a connection is multiplexed or not. 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 ## Filter Tracing
Filters may make use of special trace macros like `CURL_TRC_CF(data, cf, msg, Filters may make use of special trace macros like `CURL_TRC_CF(data, cf, msg,

View file

@ -298,6 +298,13 @@ supports HTTP NTLM
libcurl was built with support for NTLM delegation to a winbind helper. This libcurl was built with support for NTLM delegation to a winbind helper. This
feature was removed from curl in 8.8.0. 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` ## `PSL`
*features* mask bit: CURL_VERSION_PSL *features* mask bit: CURL_VERSION_PSL

View file

@ -58,7 +58,11 @@ HTTPS Proxy. (with OpenSSL, GnuTLS, mbedTLS, Rustls, Schannel or wolfSSL.)
This uses HTTP/1 by default. Setting CURLOPT_PROXYTYPE(3) to This uses HTTP/1 by default. Setting CURLOPT_PROXYTYPE(3) to
**CURLPROXY_HTTPS2** allows libcurl to negotiate using HTTP/2 with proxy. **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. SOCKS4 Proxy.

View file

@ -41,6 +41,12 @@ HTTPS Proxy using HTTP/1. (Added in 7.52.0 for OpenSSL and GnuTLS. Since
HTTPS Proxy and attempt to speak HTTP/2 over it. (Added in 8.1.0) 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 ## CURLPROXY_HTTP_1_0
HTTP 1.0 Proxy. This is similar to CURLPROXY_HTTP except it uses HTTP/1.0 for HTTP 1.0 Proxy. This is similar to CURLPROXY_HTTP except it uses HTTP/1.0 for

View file

@ -993,6 +993,7 @@ CURLPROXY_HTTP 7.10
CURLPROXY_HTTP_1_0 7.19.4 CURLPROXY_HTTP_1_0 7.19.4
CURLPROXY_HTTPS 7.52.0 CURLPROXY_HTTPS 7.52.0
CURLPROXY_HTTPS2 8.1.0 CURLPROXY_HTTPS2 8.1.0
CURLPROXY_HTTPS3 8.21.0
CURLPROXY_SOCKS4 7.10 CURLPROXY_SOCKS4 7.10
CURLPROXY_SOCKS4A 7.18.0 CURLPROXY_SOCKS4A 7.18.0
CURLPROXY_SOCKS5 7.10 CURLPROXY_SOCKS5 7.10

View file

@ -177,6 +177,7 @@
--proxy-digest 7.12.0 --proxy-digest 7.12.0
--proxy-header 7.37.0 --proxy-header 7.37.0
--proxy-http2 8.1.0 --proxy-http2 8.1.0
--proxy-http3 8.21.0
--proxy-insecure 7.52.0 --proxy-insecure 7.52.0
--proxy-key 7.52.0 --proxy-key 7.52.0
--proxy-key-type 7.52.0 --proxy-key-type 7.52.0

View file

@ -62,6 +62,9 @@ Via curl's `configure` script you may specify:
* `--with-test-nghttpx=<path-of-nghttpx>` if you have nghttpx to use * `--with-test-nghttpx=<path-of-nghttpx>` if you have nghttpx to use
somewhere outside your `$PATH`. 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 * `--with-test-httpd=<httpd-install-path>` if you have an Apache httpd
installed somewhere else. On Debian/Ubuntu it otherwise looks into installed somewhere else. On Debian/Ubuntu it otherwise looks into
`/usr/bin` and `/usr/sbin` to find those. `/usr/bin` and `/usr/sbin` to find those.

View file

@ -802,9 +802,11 @@ typedef CURLcode (*curl_ssl_ctx_callback)(CURL *curl, /* easy handle */
#define CURLPROXY_SOCKS5_HOSTNAME 7L /* Use the SOCKS5 protocol but pass along #define CURLPROXY_SOCKS5_HOSTNAME 7L /* Use the SOCKS5 protocol but pass along
the hostname rather than the IP the hostname rather than the IP
address. added in 7.18.0 */ address. added in 7.18.0 */
#define CURLPROXY_HTTPS3 8L /* HTTPS and attempt HTTP/3
added in 8.21.0 */
typedef enum { typedef enum {
CURLPROXY_LAST = 8 /* never use */ CURLPROXY_LAST = 9 /* never use */
} curl_proxytype; /* this enum was added in 7.10 */ } curl_proxytype; /* this enum was added in 7.10 */
/* /*
@ -1494,8 +1496,8 @@ typedef enum {
CURLOPT(CURLOPT_SHARE, CURLOPTTYPE_OBJECTPOINT, 100), CURLOPT(CURLOPT_SHARE, CURLOPTTYPE_OBJECTPOINT, 100),
/* indicates type of proxy. accepted values are CURLPROXY_HTTP (default), /* indicates type of proxy. accepted values are CURLPROXY_HTTP (default),
CURLPROXY_HTTPS, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A and CURLPROXY_HTTPS, CURLPROXY_HTTPS2, CURLPROXY_HTTPS3, CURLPROXY_SOCKS4,
CURLPROXY_SOCKS5. */ CURLPROXY_SOCKS4A and CURLPROXY_SOCKS5. */
CURLOPT(CURLOPT_PROXYTYPE, CURLOPTTYPE_VALUES, 101), CURLOPT(CURLOPT_PROXYTYPE, CURLOPTTYPE_VALUES, 101),
/* Set the Accept-Encoding string. Use this to tell a server you would like /* Set the Accept-Encoding string. Use this to tell a server you would like

View file

@ -150,8 +150,11 @@ LIB_CFILES = \
bufq.c \ bufq.c \
bufref.c \ bufref.c \
cf-dns.c \ cf-dns.c \
capsule.c \
cf-capsule.c \
cf-h1-proxy.c \ cf-h1-proxy.c \
cf-h2-proxy.c \ cf-h2-proxy.c \
cf-h3-proxy.c \
cf-haproxy.c \ cf-haproxy.c \
cf-https-connect.c \ cf-https-connect.c \
cf-ip-happy.c \ cf-ip-happy.c \
@ -282,8 +285,11 @@ LIB_HFILES = \
bufq.h \ bufq.h \
bufref.h \ bufref.h \
cf-dns.h \ cf-dns.h \
capsule.h \
cf-capsule.h \
cf-h1-proxy.h \ cf-h1-proxy.h \
cf-h2-proxy.h \ cf-h2-proxy.h \
cf-h3-proxy.h \
cf-haproxy.h \ cf-haproxy.h \
cf-https-connect.h \ cf-https-connect.h \
cf-ip-happy.h \ cf-ip-happy.h \

281
lib/capsule.c Normal file
View file

@ -0,0 +1,281 @@
/***************************************************************************
* _ _ ____ _
* 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 */

77
lib/capsule.h Normal file
View file

@ -0,0 +1,77 @@
#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 */

253
lib/cf-capsule.c Normal file
View file

@ -0,0 +1,253 @@
/***************************************************************************
* _ _ ____ _
* 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 */

40
lib/cf-capsule.h Normal file
View file

@ -0,0 +1,40 @@
#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 */

View file

@ -25,6 +25,8 @@
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) #if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include <curl/curl.h>
#include "urldata.h" #include "urldata.h"
#include "curlx/dynbuf.h" #include "curlx/dynbuf.h"
#include "sendf.h" #include "sendf.h"
@ -33,6 +35,7 @@
#include "http_proxy.h" #include "http_proxy.h"
#include "select.h" #include "select.h"
#include "progress.h" #include "progress.h"
#include "multiif.h"
#include "cfilters.h" #include "cfilters.h"
#include "cf-h1-proxy.h" #include "cf-h1-proxy.h"
#include "connect.h" #include "connect.h"
@ -40,7 +43,6 @@
#include "strcase.h" #include "strcase.h"
#include "curlx/strparse.h" #include "curlx/strparse.h"
typedef enum { typedef enum {
H1_TUNNEL_INIT, /* init/default/no tunnel state */ H1_TUNNEL_INIT, /* init/default/no tunnel state */
H1_TUNNEL_CONNECT, /* CONNECT request is being send */ H1_TUNNEL_CONNECT, /* CONNECT request is being send */
@ -72,6 +74,12 @@ struct h1_tunnel_state {
BIT(leading_unfold); 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) 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_ESTABLISHED);
@ -82,6 +90,12 @@ static bool tunnel_is_failed(struct h1_tunnel_state *ts)
return ts && (ts->tunnel_state == H1_TUNNEL_FAILED); 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, static CURLcode tunnel_reinit(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct h1_tunnel_state *ts) struct h1_tunnel_state *ts)
@ -97,6 +111,8 @@ static CURLcode tunnel_reinit(struct Curl_cfilter *cf,
ts->close_connection = FALSE; ts->close_connection = FALSE;
ts->maybe_folded = FALSE; ts->maybe_folded = FALSE;
ts->leading_unfold = FALSE; ts->leading_unfold = FALSE;
ts->nsent = 0;
ts->headerlines = 0;
return CURLE_OK; return CURLE_OK;
} }
@ -158,7 +174,9 @@ static void h1_tunnel_go_state(struct Curl_cfilter *cf,
case H1_TUNNEL_ESTABLISHED: case H1_TUNNEL_ESTABLISHED:
CURL_TRC_CF(data, cf, "new tunnel state '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.done = TRUE;
data->state.authproxy.multipass = FALSE; data->state.authproxy.multipass = FALSE;
FALLTHROUGH(); FALLTHROUGH();
@ -195,11 +213,12 @@ static void cf_tunnel_free(struct Curl_cfilter *cf,
struct Curl_easy *data) struct Curl_easy *data)
{ {
if(cf) { 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) { if(ts) {
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data); h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
tunnel_free(ts, data); tunnel_free(ts, data);
cf->ctx = NULL; pctx->ts = NULL;
} }
} }
} }
@ -217,17 +236,17 @@ static CURLcode start_CONNECT(struct Curl_cfilter *cf,
int http_minor; int http_minor;
CURLcode result; CURLcode result;
DEBUGASSERT(data);
/* This only happens if we have looped here due to authentication reasons, /* 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. */ and we do not really use the newly cloned URL here then. Free it. */
curlx_safefree(data->req.newurl); curlx_safefree(data->req.newurl);
result = Curl_http_proxy_create_CONNECT(&req, cf, data, result = Curl_http_proxy_create_tunnel_request(&req, cf, data, ts->dest,
ts->dest, ts->httpversion); PROXY_HTTP_V1,
h1_proxy_is_udp(cf));
if(result) if(result)
goto out; goto out;
infof(data, "Establish HTTP proxy tunnel to %s", req->authority);
curlx_dyn_reset(&ts->request_data); curlx_dyn_reset(&ts->request_data);
ts->nsent = 0; ts->nsent = 0;
ts->headerlines = 0; ts->headerlines = 0;
@ -280,6 +299,92 @@ out:
return result; 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, static CURLcode on_resp_header(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct h1_tunnel_state *ts, struct h1_tunnel_state *ts,
@ -418,7 +523,13 @@ static CURLcode single_header(struct Curl_cfilter *cf,
return result; 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) if(result)
return result; return result;
@ -460,6 +571,13 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
} }
if(!nread) { 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 && if(data->set.proxyauth && data->state.authproxy.avail &&
data->req.hd_proxy_auth) { data->req.hd_proxy_auth) {
/* proxy auth was requested and there was proxy auth available, /* proxy auth was requested and there was proxy auth available,
@ -551,12 +669,16 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
ts->maybe_folded = TRUE; ts->maybe_folded = TRUE;
} }
if(result)
return result;
} /* while there is buffer left and loop is requested */ } /* while there is buffer left and loop is requested */
if(error) if(error)
result = CURLE_RECV_ERROR; result = CURLE_RECV_ERROR;
*done = (ts->keepon == KEEPON_DONE); *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 /* Deal with the possibly already received authenticate
headers. 'newurl' is set to a new URL if we must loop. */ headers. 'newurl' is set to a new URL if we must loop. */
result = Curl_http_auth_act(data); result = Curl_http_auth_act(data);
@ -637,7 +759,7 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf,
infof(data, "Connect me again please"); infof(data, "Connect me again please");
Curl_conn_cf_close(cf, data); Curl_conn_cf_close(cf, data);
result = Curl_conn_cf_connect(cf->next, data, &done); result = Curl_conn_cf_connect(cf->next, data, &done);
goto out; return result;
} }
else { else {
/* staying on this connection, reset state */ /* staying on this connection, reset state */
@ -653,17 +775,36 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf,
} while(data->req.newurl); } while(data->req.newurl);
DEBUGASSERT(ts->tunnel_state == H1_TUNNEL_RESPONSE); DEBUGASSERT(ts->tunnel_state == H1_TUNNEL_RESPONSE);
if(data->info.httpproxycode / 100 != 2) { if(h1_proxy_is_udp(cf)) {
/* a non-2xx response and we have no next URL to try. */ /* RFC 9298: Accept 101 Upgrade for HTTP/1.1 and
curlx_safefree(data->req.newurl); * 2xx responses for HTTP/2 and HTTP/3 proxies. */
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data); if(data->info.httpproxycode / 100 != 2 &&
failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode); data->info.httpproxycode != 101) {
return CURLE_COULDNT_CONNECT; 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! */ /* 2xx response, SUCCESS! */
/* 101 Switching Protocol for CONNECT-UDP */
h1_tunnel_go_state(cf, ts, H1_TUNNEL_ESTABLISHED, data); h1_tunnel_go_state(cf, ts, H1_TUNNEL_ESTABLISHED, data);
infof(data, "CONNECT tunnel established, response %d", if(h1_proxy_is_udp(cf))
data->info.httpproxycode); 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; result = CURLE_OK;
out: out:
@ -677,7 +818,8 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
bool *done) bool *done)
{ {
CURLcode result; 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) { if(cf->connected) {
*done = TRUE; *done = TRUE;
@ -694,7 +836,7 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
result = tunnel_init(cf, data, &ts); result = tunnel_init(cf, data, &ts);
if(result) if(result)
return result; return result;
cf->ctx = ts; pctx->ts = ts;
} }
/* We want "seamless" operations through HTTP proxy tunnel */ /* We want "seamless" operations through HTTP proxy tunnel */
@ -705,14 +847,13 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
curlx_safefree(data->req.hd_proxy_auth); curlx_safefree(data->req.hd_proxy_auth);
out: out:
*done = (result == CURLE_OK) && tunnel_is_established(cf->ctx); *done = (result == CURLE_OK) && tunnel_is_established(pctx->ts);
if(*done) { if(*done) {
cf->connected = TRUE; cf->connected = TRUE;
/* The real request will follow the CONNECT, reset request partially */ /* The real request will follow the CONNECT, reset request partially */
Curl_req_soft_reset(&data->req, data); Curl_req_soft_reset(&data->req, data);
Curl_client_reset(data); Curl_client_reset(data);
Curl_pgrsReset(data); Curl_pgrsReset(data);
cf_tunnel_free(cf, data); cf_tunnel_free(cf, data);
} }
return result; return result;
@ -722,7 +863,8 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct easy_pollset *ps) 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; CURLcode result = CURLE_OK;
if(!cf->connected) { if(!cf->connected) {
@ -742,37 +884,49 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf,
else else
result = Curl_pollset_set_out_only(data, ps, sock); 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; 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, static void cf_h1_proxy_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data) struct Curl_easy *data)
{ {
CURL_TRC_CF(data, cf, "destroy"); CURL_TRC_CF(data, cf, "destroy");
cf_tunnel_free(cf, data); cf_tunnel_free(cf, data);
curlx_safefree(cf->ctx);
} }
static void cf_h1_proxy_close(struct Curl_cfilter *cf, static void cf_h1_proxy_close(struct Curl_cfilter *cf,
struct Curl_easy *data) struct Curl_easy *data)
{ {
struct cf_h1_proxy_ctx *pctx = cf->ctx;
CURL_TRC_CF(data, cf, "close"); CURL_TRC_CF(data, cf, "close");
if(cf) { cf->connected = FALSE;
cf->connected = FALSE; if(pctx && pctx->ts)
if(cf->ctx) { h1_tunnel_go_state(cf, pctx->ts, H1_TUNNEL_INIT, data);
h1_tunnel_go_state(cf, cf->ctx, H1_TUNNEL_INIT, data); if(cf->next)
} cf->next->cft->do_close(cf->next, data);
if(cf->next)
cf->next->cft->do_close(cf->next, data);
}
} }
static CURLcode cf_h1_proxy_query(struct Curl_cfilter *cf, static CURLcode cf_h1_proxy_query(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
int query, int *pres1, void *pres2) 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) { switch(query) {
case CF_QUERY_HOST_PORT: case CF_QUERY_HOST_PORT:
if(!ts || !ts->dest)
break;
*pres1 = (int)ts->dest->port; *pres1 = (int)ts->dest->port;
*((const char **)pres2) = ts->dest->hostname; *((const char **)pres2) = ts->dest->hostname;
return CURLE_OK; return CURLE_OK;
@ -799,7 +953,7 @@ struct Curl_cftype Curl_cft_h1_proxy = {
cf_h1_proxy_close, cf_h1_proxy_close,
Curl_cf_def_shutdown, Curl_cf_def_shutdown,
cf_h1_proxy_adjust_pollset, cf_h1_proxy_adjust_pollset,
Curl_cf_def_data_pending, cf_h1_proxy_data_pending,
Curl_cf_def_send, Curl_cf_def_send,
Curl_cf_def_recv, Curl_cf_def_recv,
Curl_cf_def_cntrl, Curl_cf_def_cntrl,
@ -811,9 +965,11 @@ struct Curl_cftype Curl_cft_h1_proxy = {
CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at, CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest, struct Curl_peer *dest,
int httpversion) int httpversion,
bool udp_tunnel)
{ {
struct Curl_cfilter *cf; struct Curl_cfilter *cf;
struct cf_h1_proxy_ctx *pctx;
struct h1_tunnel_state *ts; struct h1_tunnel_state *ts;
CURLcode result; CURLcode result;
@ -834,9 +990,18 @@ CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
curlx_dyn_init(&ts->request_data, DYN_HTTP_REQUEST); curlx_dyn_init(&ts->request_data, DYN_HTTP_REQUEST);
Curl_httpchunk_init(data, &ts->ch, TRUE); Curl_httpchunk_init(data, &ts->ch, TRUE);
result = Curl_cf_create(&cf, &Curl_cft_h1_proxy, ts); pctx = curlx_calloc(1, sizeof(*pctx));
if(result) if(!pctx) {
result = CURLE_OUT_OF_MEMORY;
goto out; 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; ts = NULL;
Curl_conn_cf_insert_after(cf_at, cf); Curl_conn_cf_insert_after(cf_at, cf);

View file

@ -32,7 +32,8 @@ struct Curl_peer;
CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at, CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest, struct Curl_peer *dest,
int httpversion); int httpversion,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_h1_proxy; extern struct Curl_cftype Curl_cft_h1_proxy;

View file

@ -42,6 +42,7 @@
#include "sendf.h" #include "sendf.h"
#include "select.h" #include "select.h"
#include "cf-h2-proxy.h" #include "cf-h2-proxy.h"
#include "capsule.h"
#define PROXY_H2_CHUNK_SIZE (16 * 1024) #define PROXY_H2_CHUNK_SIZE (16 * 1024)
@ -96,6 +97,20 @@ static CURLcode tunnel_stream_init(struct tunnel_stream *ts,
return CURLE_OK; 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) static void tunnel_stream_clear(struct tunnel_stream *ts)
{ {
Curl_http_resp_free(ts->resp); Curl_http_resp_free(ts->resp);
@ -109,9 +124,11 @@ static void tunnel_stream_clear(struct tunnel_stream *ts)
static void h2_tunnel_go_state(struct Curl_cfilter *cf, static void h2_tunnel_go_state(struct Curl_cfilter *cf,
struct tunnel_stream *ts, struct tunnel_stream *ts,
h2_tunnel_state new_state, h2_tunnel_state new_state,
struct Curl_easy *data) struct Curl_easy *data,
bool udp_tunnel)
{ {
(void)cf; (void)cf;
(void)udp_tunnel;
if(ts->state == new_state) if(ts->state == new_state)
return; return;
@ -127,7 +144,7 @@ static void h2_tunnel_go_state(struct Curl_cfilter *cf,
switch(new_state) { switch(new_state) {
case H2_TUNNEL_INIT: case H2_TUNNEL_INIT:
CURL_TRC_CF(data, cf, "[%d] new tunnel state 'init'", ts->stream_id); CURL_TRC_CF(data, cf, "[%d] new tunnel state 'init'", ts->stream_id);
tunnel_stream_clear(ts); tunnel_stream_reset(ts);
break; break;
case H2_TUNNEL_CONNECT: case H2_TUNNEL_CONNECT:
@ -143,7 +160,8 @@ static void h2_tunnel_go_state(struct Curl_cfilter *cf,
case H2_TUNNEL_ESTABLISHED: case H2_TUNNEL_ESTABLISHED:
CURL_TRC_CF(data, cf, "[%d] new tunnel state 'established'", CURL_TRC_CF(data, cf, "[%d] new tunnel state 'established'",
ts->stream_id); 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.done = TRUE;
data->state.authproxy.multipass = FALSE; data->state.authproxy.multipass = FALSE;
FALLTHROUGH(); FALLTHROUGH();
@ -175,6 +193,7 @@ struct cf_h2_proxy_ctx {
BIT(rcvd_goaway); BIT(rcvd_goaway);
BIT(sent_goaway); BIT(sent_goaway);
BIT(nw_out_blocked); BIT(nw_out_blocked);
BIT(udp_tunnel);
}; };
/* How to access `call_data` from a cf_h2 filter */ /* How to access `call_data` from a cf_h2 filter */
@ -211,7 +230,8 @@ static void drain_tunnel(struct Curl_cfilter *cf,
struct cf_h2_proxy_ctx *ctx = cf->ctx; struct cf_h2_proxy_ctx *ctx = cf->ctx;
(void)cf; (void)cf;
if(!tunnel->closed && !tunnel->reset && 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); Curl_multi_mark_dirty(data);
} }
@ -749,15 +769,15 @@ static CURLcode submit_CONNECT(struct Curl_cfilter *cf,
CURLcode result; CURLcode result;
struct httpreq *req = NULL; 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) if(result)
goto out; goto out;
result = Curl_creader_set_null(data); result = Curl_creader_set_null(data);
if(result) if(result)
goto out; 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, result = proxy_h2_submit(&ts->stream_id, cf, data, ctx->h2, req,
NULL, ts, tunnel_send_callback, cf); NULL, ts, tunnel_send_callback, cf);
if(result) { if(result) {
@ -777,41 +797,30 @@ static CURLcode inspect_response(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct tunnel_stream *ts) struct tunnel_stream *ts)
{ {
CURLcode result = CURLE_OK; struct cf_h2_proxy_ctx *ctx = cf->ctx;
struct dynhds_entry *auth_reply = NULL; proxy_inspect_result res;
(void)cf; CURLcode result;
DEBUGASSERT(ts->resp); result = Curl_http_proxy_inspect_tunnel_response(
if(ts->resp->status / 100 == 2) { cf, data, ts->resp, (bool)ctx->udp_tunnel, &res);
infof(data, "CONNECT tunnel established, response %d", ts->resp->status); if(result)
h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data); return result;
return CURLE_OK; 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;
} }
return result;
if(ts->resp->status == 401) {
auth_reply = Curl_dynhds_cget(&ts->resp->headers, "WWW-Authenticate");
}
else if(ts->resp->status == 407) {
auth_reply = Curl_dynhds_cget(&ts->resp->headers, "Proxy-Authenticate");
}
if(auth_reply) {
CURL_TRC_CF(data, cf, "[0] CONNECT: fwd auth header '%s'",
auth_reply->value);
result = Curl_http_input_auth(data, ts->resp->status == 407,
auth_reply->value);
if(result)
return result;
if(data->req.newurl) {
/* Indicator that we should try again */
curlx_safefree(data->req.newurl);
h2_tunnel_go_state(cf, ts, H2_TUNNEL_INIT, data);
return CURLE_OK;
}
}
/* Seems to have failed */
return CURLE_COULDNT_CONNECT;
} }
static CURLcode H2_CONNECT(struct Curl_cfilter *cf, static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
@ -831,7 +840,8 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
result = submit_CONNECT(cf, data, ts); result = submit_CONNECT(cf, data, ts);
if(result) if(result)
goto out; 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(); FALLTHROUGH();
case H2_TUNNEL_CONNECT: case H2_TUNNEL_CONNECT:
@ -840,12 +850,14 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
if(!result) if(!result)
result = proxy_h2_progress_egress(cf, data); result = proxy_h2_progress_egress(cf, data);
if(result && result != CURLE_AGAIN) { 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; break;
} }
if(ts->has_final_response) { 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 { else {
result = CURLE_OK; result = CURLE_OK;
@ -874,7 +886,8 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
out: out:
if((result && (result != CURLE_AGAIN)) || ctx->tunnel.closed) 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; return result;
} }
@ -1231,7 +1244,8 @@ static CURLcode cf_h2_proxy_recv(struct Curl_cfilter *cf,
result = Curl_1st_fatal(result, proxy_h2_progress_egress(cf, data)); result = Curl_1st_fatal(result, proxy_h2_progress_egress(cf, data));
out: 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))) { (!result || (result == CURLE_AGAIN))) {
/* data pending and no fatal error to report. Need to trigger /* data pending and no fatal error to report. Need to trigger
* draining to avoid stalling when no socket events happen. */ * draining to avoid stalling when no socket events happen. */
@ -1297,7 +1311,8 @@ static CURLcode cf_h2_proxy_send(struct Curl_cfilter *cf,
} }
out: 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))) { (!result || (result == CURLE_AGAIN))) {
/* data pending and no fatal error to report. Need to trigger /* data pending and no fatal error to report. Need to trigger
* draining to avoid stalling when no socket events happen. */ * draining to avoid stalling when no socket events happen. */
@ -1477,7 +1492,8 @@ struct Curl_cftype Curl_cft_h2_proxy = {
CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf, CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest) struct Curl_peer *dest,
bool udp_tunnel)
{ {
struct Curl_cfilter *cf_h2_proxy = NULL; struct Curl_cfilter *cf_h2_proxy = NULL;
struct cf_h2_proxy_ctx *ctx; struct cf_h2_proxy_ctx *ctx;
@ -1488,6 +1504,7 @@ CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf,
if(!ctx) if(!ctx)
goto out; goto out;
Curl_peer_link(&ctx->dest, dest); Curl_peer_link(&ctx->dest, dest);
ctx->udp_tunnel = udp_tunnel;
result = Curl_cf_create(&cf_h2_proxy, &Curl_cft_h2_proxy, ctx); result = Curl_cf_create(&cf_h2_proxy, &Curl_cft_h2_proxy, ctx);
if(result) if(result)
@ -1501,3 +1518,6 @@ out:
} }
#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_PROXY && USE_NGHTTP2 */ #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

View file

@ -29,7 +29,8 @@
CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf, CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest); struct Curl_peer *dest,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_h2_proxy; extern struct Curl_cftype Curl_cft_h2_proxy;

3478
lib/cf-h3-proxy.c Normal file

File diff suppressed because it is too large Load diff

42
lib/cf-h3-proxy.h Normal file
View file

@ -0,0 +1,42 @@
#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 */

View file

@ -1023,3 +1023,28 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
Curl_conn_cf_insert_after(cf_at, cf); Curl_conn_cf_insert_after(cf_at, cf);
return CURLE_OK; 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 */

View file

@ -52,6 +52,15 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data, struct Curl_easy *data,
uint8_t transport); 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; extern struct Curl_cftype Curl_cft_ip_happy;
#endif /* HEADER_CURL_IP_HAPPY_H */ #endif /* HEADER_CURL_IP_HAPPY_H */

View file

@ -63,6 +63,7 @@
#include "curlx/inet_ntop.h" #include "curlx/inet_ntop.h"
#include "curlx/strparse.h" #include "curlx/strparse.h"
#include "vtls/vtls.h" /* for vtls cfilters */ #include "vtls/vtls.h" /* for vtls cfilters */
#include "vquic/vquic.h" /* for QUIC cfilters */
#include "progress.h" #include "progress.h"
#include "conncache.h" #include "conncache.h"
#include "multihandle.h" #include "multihandle.h"
@ -341,6 +342,66 @@ struct cf_setup_ctx {
uint8_t transport; 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, static CURLcode cf_setup_connect(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
bool *done) bool *done)
@ -364,7 +425,35 @@ connect_sub_chain:
} }
if(ctx->state < CF_SETUP_CNNCT_EYEBALLS) { 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) if(result)
return result; return result;
ctx->state = CF_SETUP_CNNCT_EYEBALLS; ctx->state = CF_SETUP_CNNCT_EYEBALLS;
@ -402,25 +491,9 @@ connect_sub_chain:
} }
if(ctx->state < CF_SETUP_CNNCT_HTTP_PROXY && cf->conn->bits.httpproxy) { if(ctx->state < CF_SETUP_CNNCT_HTTP_PROXY && cf->conn->bits.httpproxy) {
#ifdef USE_SSL result = cf_setup_add_http_proxy(cf, data, ctx);
if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype) && if(result)
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) { return result;
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 */
ctx->state = CF_SETUP_CNNCT_HTTP_PROXY; ctx->state = CF_SETUP_CNNCT_HTTP_PROXY;
if(!cf->next || !cf->next->connected) if(!cf->next || !cf->next->connected)
goto connect_sub_chain; goto connect_sub_chain;
@ -445,21 +518,41 @@ connect_sub_chain:
goto connect_sub_chain; goto connect_sub_chain;
} }
if(ctx->state < CF_SETUP_CNNCT_SSL) { /* Adding Curl_cf_quic_insert_after() because now we
#ifdef USE_SSL need the next filter to be QUIC/HTTP/3 (which has SSL) */
if((ctx->ssl_mode == CURL_CF_SSL_ENABLE || #if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
(ctx->ssl_mode != CURL_CF_SSL_DISABLE && defined(USE_PROXY_HTTP3)
cf->conn->scheme->flags & PROTOPT_SSL)) && /* we want SSL */ if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy &&
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */ cf->conn->bits.tunnel_proxy &&
result = Curl_cf_ssl_insert_after(cf, data); (data->state.http_neg.wanted == CURL_HTTP_V3x)) {
if(ctx->state < CF_SETUP_CNNCT_SSL) {
result = Curl_cf_quic_insert_after(cf);
if(result) if(result)
return result; return result;
ctx->state = CF_SETUP_CNNCT_SSL;
} }
#endif /* USE_SSL */
ctx->state = CF_SETUP_CNNCT_SSL;
if(!cf->next || !cf->next->connected) if(!cf->next || !cf->next->connected)
goto connect_sub_chain; 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; ctx->state = CF_SETUP_DONE;
cf->connected = TRUE; cf->connected = TRUE;

View file

@ -712,6 +712,9 @@ ${SIZEOF_TIME_T_CODE}
/* if libuv is in use */ /* if libuv is in use */
#cmakedefine USE_LIBUV 1 #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. */ /* Define to 1 if you have the <uv.h> header file. */
#cmakedefine HAVE_UV_H 1 #cmakedefine HAVE_UV_H 1

View file

@ -35,6 +35,7 @@
#include "http_proxy.h" #include "http_proxy.h"
#include "cf-h1-proxy.h" #include "cf-h1-proxy.h"
#include "cf-h2-proxy.h" #include "cf-h2-proxy.h"
#include "cf-h3-proxy.h"
#include "cf-haproxy.h" #include "cf-haproxy.h"
#include "cf-https-connect.h" #include "cf-https-connect.h"
#include "cf-ip-happy.h" #include "cf-ip-happy.h"
@ -578,6 +579,9 @@ static struct trc_cft_def trc_cfts[] = {
{ &Curl_cft_h1_proxy, TRC_CT_PROXY }, { &Curl_cft_h1_proxy, TRC_CT_PROXY },
#ifdef USE_NGHTTP2 #ifdef USE_NGHTTP2
{ &Curl_cft_h2_proxy, TRC_CT_PROXY }, { &Curl_cft_h2_proxy, TRC_CT_PROXY },
#endif
#if defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3)
{ &Curl_cft_h3_proxy, TRC_CT_PROXY },
#endif #endif
{ &Curl_cft_http_proxy, TRC_CT_PROXY }, { &Curl_cft_http_proxy, TRC_CT_PROXY },
#endif /* !CURL_DISABLE_HTTP */ #endif /* !CURL_DISABLE_HTTP */

View file

@ -1757,6 +1757,12 @@ CURLcode Curl_add_custom_headers(struct Curl_easy *data,
else else
h[0] = data->set.headers; h[0] = data->set.headers;
break; break;
case HEADER_CONNECT_UDP:
if(data->set.sep_headers)
h[0] = data->set.proxyheaders;
else
h[0] = data->set.headers;
break;
} }
#else #else
(void)is_connect; (void)is_connect;
@ -2721,7 +2727,11 @@ static CURLcode http_check_new_conn(struct Curl_easy *data)
alpn = Curl_conn_get_alpn_negotiated(data, conn); alpn = Curl_conn_get_alpn_negotiated(data, conn);
if(alpn && !strcmp("h3", alpn)) { 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"; info_version = "HTTP/3";
} }
else if(alpn && !strcmp("h2", alpn)) { else if(alpn && !strcmp("h2", alpn)) {
@ -4847,7 +4857,6 @@ struct name_const {
size_t namelen; size_t namelen;
}; };
/* keep them sorted by length! */
static const struct name_const H2_NON_FIELD[] = { static const struct name_const H2_NON_FIELD[] = {
{ STRCONST("Host") }, { STRCONST("Host") },
{ STRCONST("Upgrade") }, { STRCONST("Upgrade") },
@ -4861,10 +4870,8 @@ static bool h2_permissible_field(struct dynhds_entry *e)
{ {
size_t i; size_t i;
for(i = 0; i < CURL_ARRAYSIZE(H2_NON_FIELD); ++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 && 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 FALSE;
} }
return TRUE; return TRUE;

View file

@ -83,8 +83,6 @@ char *Curl_checkProxyheaders(struct Curl_easy *data,
CURLcode Curl_add_timecondition(struct Curl_easy *data, struct dynbuf *req); CURLcode Curl_add_timecondition(struct Curl_easy *data, struct dynbuf *req);
CURLcode Curl_add_custom_headers(struct Curl_easy *data, bool is_connect, CURLcode Curl_add_custom_headers(struct Curl_easy *data, bool is_connect,
int httpversion, struct dynbuf *req); 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); void Curl_http_to_fold(struct dynbuf *bf);

View file

@ -3021,3 +3021,6 @@ char *curl_pushheader_byname(struct curl_pushheaders *h, const char *name)
} }
#endif /* !CURL_DISABLE_HTTP && USE_NGHTTP2 */ #endif /* !CURL_DISABLE_HTTP && USE_NGHTTP2 */
/* Do not leak this filter's call_data accessor in unity builds. */
#undef CF_CTX_CALL_DATA

View file

@ -33,13 +33,15 @@
#include "cfilters.h" #include "cfilters.h"
#include "cf-h1-proxy.h" #include "cf-h1-proxy.h"
#include "cf-h2-proxy.h" #include "cf-h2-proxy.h"
#include "cf-h3-proxy.h"
#include "cf-capsule.h"
#include "connect.h" #include "connect.h"
#include "vauth/vauth.h" #include "vauth/vauth.h"
#include "curlx/strparse.h" #include "curlx/strparse.h"
static CURLcode dynhds_add_custom(struct Curl_easy *data, static CURLcode dynhds_add_custom(struct Curl_easy *data,
bool is_connect, int httpversion, bool is_connect, int httpversion,
struct dynhds *hds) bool is_udp, struct dynhds *hds)
{ {
struct connectdata *conn = data->conn; struct connectdata *conn = data->conn;
struct curl_slist *h[2]; struct curl_slist *h[2];
@ -49,10 +51,12 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data,
enum Curl_proxy_use proxy; enum Curl_proxy_use proxy;
if(is_connect) if(is_connect && !is_udp)
proxy = HEADER_CONNECT; proxy = HEADER_CONNECT;
else if(is_connect && is_udp)
proxy = HEADER_CONNECT_UDP;
else else
proxy = conn->bits.httpproxy && !conn->bits.tunnel_proxy ? proxy = (conn->bits.httpproxy && !conn->bits.tunnel_proxy) ?
HEADER_PROXY : HEADER_SERVER; HEADER_PROXY : HEADER_SERVER;
switch(proxy) { switch(proxy) {
@ -72,6 +76,12 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data,
else else
h[0] = data->set.headers; h[0] = data->set.headers;
break; 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 */ /* loop through one or two lists */
@ -166,15 +176,30 @@ struct cf_proxy_ctx {
struct Curl_peer *dest; /* tunnel destination */ struct Curl_peer *dest; /* tunnel destination */
uint8_t proxytype; uint8_t proxytype;
BIT(sub_filter_installed); 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, CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
struct Curl_cfilter *cf, struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest, struct Curl_peer *dest,
int httpversion) proxy_http_ver ver)
{ {
char *authority = NULL; char *authority = NULL;
int httpversion = proxy_http_ver_major(ver);
CURLcode result; CURLcode result;
struct httpreq *req = NULL; struct httpreq *req = NULL;
@ -201,7 +226,7 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
goto out; goto out;
/* If user is not overriding Host: header, we add for HTTP/1.x */ /* 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"))) { !Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) {
result = Curl_dynhds_cadd(&req->headers, "Host", authority); result = Curl_dynhds_cadd(&req->headers, "Host", authority);
if(result) if(result)
@ -223,14 +248,15 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
goto out; goto out;
} }
if(httpversion < 20 && if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) { !Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) {
result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive"); result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive");
if(result) if(result)
goto out; goto out;
} }
result = dynhds_add_custom(data, TRUE, httpversion, &req->headers); result = dynhds_add_custom(data, TRUE, httpversion,
FALSE, &req->headers);
out: out:
if(result && req) { if(result && req) {
@ -242,33 +268,327 @@ out:
return result; return result;
} }
CURLcode Curl_http_proxy_create_CONNECTUDP(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
proxy_http_ver ver)
{
const char *proxy_scheme = "http";
const char *proxy_host = cf->conn->http_proxy.peer->hostname;
int httpversion = proxy_http_ver_major(ver);
char *authority = NULL;
char *path = NULL;
char *encoded_host = NULL;
struct httpreq *req = NULL;
bool proxy_ipv6_ip;
CURLcode result;
if(cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS ||
cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS2 ||
cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS3)
proxy_scheme = "https";
proxy_ipv6_ip = cf->conn->http_proxy.peer->ipv6 != 0;
authority = curl_maprintf("%s%s%s:%d",
proxy_ipv6_ip ? "[" : "",
proxy_host,
proxy_ipv6_ip ? "]" : "",
cf->conn->http_proxy.peer->port);
if(!authority) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
if(dest->ipv6) {
/* RFC 9298: colons in IPv6 addresses MUST be percent-encoded
* in the URI template (e.g. "2001:db8::1" -> "2001%3Adb8%3A%3A1") */
const char *s = dest->hostname;
char *d;
size_t hlen = strlen(s);
encoded_host = curlx_malloc(hlen * 3 + 1);
if(!encoded_host) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
d = encoded_host;
while(*s) {
if(*s == ':') {
*d++ = '%';
*d++ = '3';
*d++ = 'A';
}
else
*d++ = *s;
s++;
}
*d = '\0';
path = curl_maprintf("/.well-known/masque/udp/%s/%u/",
encoded_host, (unsigned int)dest->port);
}
else {
path = curl_maprintf("/.well-known/masque/udp/%s/%u/",
dest->hostname, (unsigned int)dest->port);
}
if(!path) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
if(ver == PROXY_HTTP_V1) {
result = Curl_http_req_make(&req, "GET", sizeof("GET")-1,
proxy_scheme, strlen(proxy_scheme),
authority, strlen(authority),
path, strlen(path));
if(result)
goto out;
}
else if(ver == PROXY_HTTP_V2 || ver == PROXY_HTTP_V3) {
result = Curl_http_req_make(&req, "CONNECT", sizeof("CONNECT") - 1,
proxy_scheme, strlen(proxy_scheme),
authority, strlen(authority),
path, strlen(path));
if(result)
goto out;
}
else {
result = CURLE_FAILED_INIT;
goto out;
}
/* Setup the proxy-authorization header, if any */
result = Curl_http_output_auth(data, cf->conn, req->method, HTTPREQ_GET,
req->authority, NULL, TRUE);
if(result)
goto out;
/* If user is not overriding Host: header, we add for HTTP/1.x */
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) {
result = Curl_dynhds_cadd(&req->headers, "Host", authority);
if(result)
goto out;
}
if(data->req.hd_proxy_auth) {
result = Curl_dynhds_h1_cadd_line(&req->headers,
data->req.hd_proxy_auth);
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("User-Agent")) &&
data->set.str[STRING_USERAGENT] && *data->set.str[STRING_USERAGENT]) {
result = Curl_dynhds_cadd(&req->headers, "User-Agent",
data->set.str[STRING_USERAGENT]);
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) {
result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive");
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1) {
result = Curl_dynhds_cadd(&req->headers, "Connection", "Upgrade");
if(result)
goto out;
result = Curl_dynhds_cadd(&req->headers, "Upgrade", "connect-udp");
if(result)
goto out;
result = Curl_dynhds_cadd(&req->headers, "Capsule-Protocol", "?1");
if(result)
goto out;
}
else {
result = Curl_dynhds_cadd(&req->headers, ":Protocol", "connect-udp");
if(result)
goto out;
if(ver >= PROXY_HTTP_V2) {
result = Curl_dynhds_cadd(&req->headers, "Capsule-Protocol", "?1");
if(result)
goto out;
}
}
result = dynhds_add_custom(data, TRUE, httpversion,
TRUE, &req->headers);
out:
if(result && req) {
Curl_http_req_free(req);
req = NULL;
}
curlx_free(authority);
curlx_free(path);
curlx_free(encoded_host);
*preq = req;
return result;
}
CURLcode Curl_http_proxy_create_tunnel_request(
struct httpreq **preq, struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_peer *dest,
proxy_http_ver ver, bool udp_tunnel)
{
CURLcode result;
if(udp_tunnel)
result = Curl_http_proxy_create_CONNECTUDP(preq, cf, data, dest, ver);
else
result = Curl_http_proxy_create_CONNECT(preq, cf, data, dest, ver);
if(result)
return result;
if(udp_tunnel)
infof(data, "Establishing %s proxy UDP tunnel to %s:%s",
(ver == PROXY_HTTP_V2) ? "HTTP/2" :
(ver == PROXY_HTTP_V3) ? "HTTP/3" : "HTTP",
data->state.up.hostname, data->state.up.port);
else
infof(data, "Establishing %s proxy tunnel to %s",
(ver == PROXY_HTTP_V2) ? "HTTP/2" :
(ver == PROXY_HTTP_V3) ? "HTTP/3" : "HTTP",
(*preq)->authority);
return CURLE_OK;
}
CURLcode Curl_http_proxy_inspect_tunnel_response(
struct Curl_cfilter *cf, struct Curl_easy *data,
struct http_resp *resp, bool udp_tunnel,
proxy_inspect_result *presult)
{
struct dynhds_entry *capsule_protocol = NULL;
struct dynhds_entry *auth_reply = NULL;
size_t i, header_count;
CURLcode result = CURLE_OK;
DEBUGASSERT(resp);
header_count = Curl_dynhds_count(&resp->headers);
if(udp_tunnel)
infof(data, "CONNECT-UDP Response Status %d", resp->status);
else
infof(data, "CONNECT Response Status %d", resp->status);
infof(data, "Response Headers (%zu total):", header_count);
for(i = 0; i < header_count; i++) {
struct dynhds_entry *entry = Curl_dynhds_getn(&resp->headers, i);
if(entry)
infof(data, " %s: %s", entry->name, entry->value);
}
if(resp->status == 401) {
auth_reply = Curl_dynhds_cget(&resp->headers, "WWW-Authenticate");
}
else if(resp->status == 407) {
auth_reply = Curl_dynhds_cget(&resp->headers, "Proxy-Authenticate");
}
if(auth_reply) {
CURL_TRC_CF(data, cf, "[0] CONNECT%s: fwd auth header '%s'",
udp_tunnel ? "-UDP" : "", auth_reply->value);
result = Curl_http_input_auth(data, resp->status == 407,
auth_reply->value);
if(result)
return result;
if(data->req.newurl) {
curlx_safefree(data->req.newurl);
*presult = PROXY_INSPECT_AUTH_RETRY;
return CURLE_OK;
}
}
if(udp_tunnel) {
if(resp->status / 100 == 2) {
capsule_protocol = Curl_dynhds_cget(&resp->headers,
"capsule-protocol");
if(capsule_protocol) {
if(strncmp(capsule_protocol->value, "?1", 2) == 0 &&
!capsule_protocol->value[2]) {
infof(data, "CONNECT-UDP tunnel established, response %d",
resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
failf(data, "Failed to establish CONNECT-UDP tunnel, response %d, "
"unsupported capsule-protocol value '%s'",
resp->status, capsule_protocol->value);
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
else {
/* NOTE proxies may not set capsule protocol in the headers */
infof(data, "CONNECT-UDP tunnel established, response %d "
"but no capsule-protocol header found", resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
}
else {
failf(data, "Failed to establish CONNECT-UDP tunnel, "
"response %d", resp->status);
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
}
if(resp->status / 100 == 2) {
infof(data, "CONNECT tunnel established, response %d", resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
static CURLcode http_proxy_cf_connect(struct Curl_cfilter *cf, static CURLcode http_proxy_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
bool *done) bool *done)
{ {
struct cf_proxy_ctx *ctx = cf->ctx; struct cf_proxy_ctx *ctx = cf->ctx;
CURLcode result; CURLcode result;
const char *tunnel_type; /* Determine tunnel type once and reuse */
tunnel_type = ctx->udp_tunnel ? "CONNECT-UDP" : "CONNECT";
if(cf->connected) { if(cf->connected) {
*done = TRUE; *done = TRUE;
return CURLE_OK; return CURLE_OK;
} }
CURL_TRC_CF(data, cf, "connect"); CURL_TRC_CF(data, cf, "%s", tunnel_type);
connect_sub: connect_sub:
result = cf->next->cft->do_connect(cf->next, data, done); /* in case of h3_proxy, cf->next will be NULL initially */
if(result || !*done) if(cf->next) {
return result; result = cf->next->cft->do_connect(cf->next, data, done);
if(result || !*done)
return result;
}
*done = FALSE; *done = FALSE;
if(!ctx->sub_filter_installed) { 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) if(alpn)
infof(data, "CONNECT: '%s' negotiated", alpn); infof(data, "%s: '%s' negotiated", tunnel_type, alpn);
else if(!alpn) { else if(!alpn) {
/* No ALPN, proxytype rules. Fake ALPN */ /* No ALPN, proxytype rules. Fake ALPN */
infof(data, "CONNECT: no ALPN negotiated"); infof(data, "%s: no ALPN negotiated", tunnel_type);
switch(ctx->proxytype) { switch(ctx->proxytype) {
case CURLPROXY_HTTP_1_0: case CURLPROXY_HTTP_1_0:
alpn = "http/1.0"; alpn = "http/1.0";
@ -276,6 +596,9 @@ connect_sub:
case CURLPROXY_HTTPS2: case CURLPROXY_HTTPS2:
alpn = "h2"; alpn = "h2";
break; break;
case CURLPROXY_HTTPS3:
alpn = "h3";
break;
default: default:
alpn = "http/1.1"; alpn = "http/1.1";
break; break;
@ -284,7 +607,8 @@ connect_sub:
if(!strcmp(alpn, "http/1.0")) { if(!strcmp(alpn, "http/1.0")) {
CURL_TRC_CF(data, cf, "installing subfilter for 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) if(result)
goto out; goto out;
} }
@ -292,20 +616,32 @@ connect_sub:
int httpversion = (ctx->proxytype == CURLPROXY_HTTP_1_0) ? 10 : 11; int httpversion = (ctx->proxytype == CURLPROXY_HTTP_1_0) ? 10 : 11;
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.%d", CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.%d",
httpversion % 10); 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) if(result)
goto out; goto out;
} }
#ifdef USE_NGHTTP2 #ifdef USE_NGHTTP2
else if(!strcmp(alpn, "h2")) { else if(!strcmp(alpn, "h2")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/2"); 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) if(result)
goto out; goto out;
} }
#endif #endif /* USE_NGHTTP2 */
#if defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) && \
defined(USE_NGTCP2) && defined(USE_OPENSSL)
else if(!strcmp(alpn, "h3")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/3");
result = Curl_cf_h3_proxy_insert_after(cf, data, ctx->dest,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
#endif /* USE_PROXY_HTTP3 && USE_NGHTTP3 && USE_NGTCP2 && USE_OPENSSL */
else { 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; result = CURLE_COULDNT_CONNECT;
goto out; goto out;
} }
@ -321,6 +657,19 @@ connect_sub:
* This means the protocol tunnel is established, we are done. * This means the protocol tunnel is established, we are done.
*/ */
DEBUGASSERT(ctx->sub_filter_installed); 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; result = CURLE_OK;
} }
@ -404,7 +753,8 @@ struct Curl_cftype Curl_cft_http_proxy = {
CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest, struct Curl_peer *dest,
uint8_t proxytype) uint8_t proxytype,
bool udp_tunnel)
{ {
struct Curl_cfilter *cf; struct Curl_cfilter *cf;
struct cf_proxy_ctx *ctx = NULL; struct cf_proxy_ctx *ctx = NULL;
@ -421,6 +771,7 @@ CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at,
} }
Curl_peer_link(&ctx->dest, dest); Curl_peer_link(&ctx->dest, dest);
ctx->proxytype = proxytype; ctx->proxytype = proxytype;
ctx->udp_tunnel = udp_tunnel;
result = Curl_cf_create(&cf, &Curl_cft_http_proxy, ctx); result = Curl_cf_create(&cf, &Curl_cft_http_proxy, ctx);
if(result) if(result)

View file

@ -32,14 +32,47 @@
enum Curl_proxy_use { enum Curl_proxy_use {
HEADER_SERVER, /* direct to server */ HEADER_SERVER, /* direct to server */
HEADER_PROXY, /* regular request to proxy */ 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, CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
struct Curl_cfilter *cf, struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest, 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 */ /* Default proxy timeout in milliseconds */
#define PROXY_TIMEOUT (3600 * 1000) #define PROXY_TIMEOUT (3600 * 1000)
@ -47,13 +80,17 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data, struct Curl_easy *data,
struct Curl_peer *dest, struct Curl_peer *dest,
uint8_t proxytype); uint8_t proxytype,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_http_proxy; extern struct Curl_cftype Curl_cft_http_proxy;
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ #endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#define IS_HTTPS_PROXY(t) (((t) == CURLPROXY_HTTPS) || \ #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 */ #endif /* HEADER_CURL_HTTP_PROXY_H */

View file

@ -536,6 +536,37 @@ out:
#define UNIX_SOCKET_PREFIX "localhost" #define UNIX_SOCKET_PREFIX "localhost"
#endif #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, CURLcode Curl_peer_from_proxy_url(CURLU *uh,
struct Curl_easy *data, struct Curl_easy *data,
const char *url, const char *url,
@ -570,6 +601,7 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh,
break; break;
case CURLPROXY_HTTPS: case CURLPROXY_HTTPS:
case CURLPROXY_HTTPS2: case CURLPROXY_HTTPS2:
case CURLPROXY_HTTPS3:
pp.scheme = &Curl_scheme_https; pp.scheme = &Curl_scheme_https;
break; break;
case CURLPROXY_SOCKS4: case CURLPROXY_SOCKS4:
@ -592,29 +624,9 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh,
} }
else { else {
pp.scheme = Curl_get_scheme(scheme); pp.scheme = Curl_get_scheme(scheme);
if(pp.scheme == &Curl_scheme_https) { result = Curl_scheme_to_proxytype(data, scheme, &proxytype, url);
proxytype = (proxytype != CURLPROXY_HTTPS2) ? if(result)
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;
goto out; goto out;
}
} }
DEBUGASSERT(pp.scheme); DEBUGASSERT(pp.scheme);

View file

@ -94,6 +94,11 @@ CURLcode Curl_peer_from_connect_to(struct Curl_easy *data,
#ifndef CURL_DISABLE_PROXY #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, CURLcode Curl_peer_from_proxy_url(CURLU *uh,
struct Curl_easy *data, struct Curl_easy *data,
const char *url, const char *url,

View file

@ -1042,8 +1042,12 @@ static CURLcode setopt_long_proxy(struct Curl_easy *data, CURLoption option,
case CURLOPT_PROXYAUTH: case CURLOPT_PROXYAUTH:
return httpauth(data, TRUE, (unsigned long)arg); return httpauth(data, TRUE, (unsigned long)arg);
case CURLOPT_PROXYTYPE: case CURLOPT_PROXYTYPE:
if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_SOCKS5_HOSTNAME)) if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_HTTPS3))
return CURLE_BAD_FUNCTION_ARGUMENT; return CURLE_BAD_FUNCTION_ARGUMENT;
#ifndef USE_PROXY_HTTP3
if(arg == CURLPROXY_HTTPS3)
return CURLE_NOT_BUILT_IN;
#endif
s->proxytype = (unsigned char)arg; s->proxytype = (unsigned char)arg;
break; break;
case CURLOPT_SOCKS5_AUTH: case CURLOPT_SOCKS5_AUTH:

View file

@ -99,6 +99,7 @@
#include "headers.h" #include "headers.h"
#include "curlx/strerr.h" #include "curlx/strerr.h"
#include "curlx/strparse.h" #include "curlx/strparse.h"
#include "peer.h"
/* Now for the protocols */ /* Now for the protocols */
#include "ftp.h" #include "ftp.h"
@ -1316,7 +1317,12 @@ static struct connectdata *allocate_conn(struct Curl_easy *data)
#endif #endif
conn->ip_version = data->set.ipver; conn->ip_version = data->set.ipver;
conn->bits.connect_only = (bool)data->set.connect_only; 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 */ /* Store the local bind parameters that will be used for this connection */
if(data->set.str[STRING_DEVICE]) { if(data->set.str[STRING_DEVICE]) {
@ -1793,6 +1799,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
{ {
char *proxyuser = NULL; char *proxyuser = NULL;
char *proxypasswd = NULL; char *proxypasswd = NULL;
char *scheme = NULL;
CURLcode result = CURLE_OK; CURLcode result = CURLE_OK;
/* Set the start proxy type for url scheme guessing */ /* Set the start proxy type for url scheme guessing */
uint8_t proxytype = for_pre_proxy ? CURLPROXY_SOCKS4 : data->set.proxytype; uint8_t proxytype = for_pre_proxy ? CURLPROXY_SOCKS4 : data->set.proxytype;
@ -1807,7 +1814,21 @@ static CURLcode parse_proxy(struct Curl_easy *data,
these made up ones for proxies. Guess scheme for URLs without it. */ these made up ones for proxies. Guess scheme for URLs without it. */
uc = curl_url_set(uhp, CURLUPART_URL, proxy, uc = curl_url_set(uhp, CURLUPART_URL, proxy,
CURLU_NON_SUPPORT_SCHEME | CURLU_GUESS_SCHEME); 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, failf(data, "Unsupported proxy syntax in \'%s\': %s", proxy,
curl_url_strerror(uc)); curl_url_strerror(uc));
result = CURLE_COULDNT_RESOLVE_PROXY; result = CURLE_COULDNT_RESOLVE_PROXY;
@ -1824,6 +1845,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
case CURLPROXY_HTTP_1_0: case CURLPROXY_HTTP_1_0:
case CURLPROXY_HTTPS: case CURLPROXY_HTTPS:
case CURLPROXY_HTTPS2: case CURLPROXY_HTTPS2:
case CURLPROXY_HTTPS3:
if(for_pre_proxy) { if(for_pre_proxy) {
failf(data, "Unsupported pre-proxy type for \'%s\'", proxy); failf(data, "Unsupported pre-proxy type for \'%s\'", proxy);
result = CURLE_COULDNT_RESOLVE_PROXY; result = CURLE_COULDNT_RESOLVE_PROXY;
@ -1878,6 +1900,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
proxyinfo->proxytype = proxytype; proxyinfo->proxytype = proxytype;
error: error:
curlx_free(scheme);
curlx_free(proxyuser); curlx_free(proxyuser);
curlx_free(proxypasswd); curlx_free(proxypasswd);
curl_url_cleanup(uhp); curl_url_cleanup(uhp);

View file

@ -491,6 +491,9 @@ static const struct feat features_table[] = {
#ifdef USE_NTLM #ifdef USE_NTLM
FEATURE("NTLM", NULL, CURL_VERSION_NTLM), FEATURE("NTLM", NULL, CURL_VERSION_NTLM),
#endif #endif
#ifdef USE_PROXY_HTTP3
FEATURE("PROXY-HTTP3", NULL, 0),
#endif
#ifdef USE_LIBPSL #ifdef USE_LIBPSL
FEATURE("PSL", NULL, CURL_VERSION_PSL), FEATURE("PSL", NULL, CURL_VERSION_PSL),
#endif #endif

View file

@ -72,6 +72,7 @@
#define QUIC_MAX_STREAMS (256 * 1024) #define QUIC_MAX_STREAMS (256 * 1024)
#define QUIC_HANDSHAKE_TIMEOUT (10 * NGTCP2_SECONDS) #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, /* 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. * and grow that immediately to max when no rate limit is in place.
@ -95,6 +96,7 @@
#define H3_STREAM_SEND_BUFFER_MAX (10 * 1024 * 1024) #define H3_STREAM_SEND_BUFFER_MAX (10 * 1024 * 1024)
#define H3_STREAM_SEND_CHUNKS \ #define H3_STREAM_SEND_CHUNKS \
(H3_STREAM_SEND_BUFFER_MAX / H3_STREAM_CHUNK_SIZE) (H3_STREAM_SEND_BUFFER_MAX / H3_STREAM_CHUNK_SIZE)
#define QUIC_TUNNEL_INGRESS_PKT_LIMIT 1000
/* /*
* Store ngtcp2 version info in this buffer. * Store ngtcp2 version info in this buffer.
@ -139,6 +141,8 @@ struct cf_ngtcp2_ctx {
is accepted by peer */ is accepted by peer */
CURLcode tls_vrfy_result; /* result of TLS peer verification */ CURLcode tls_vrfy_result; /* result of TLS peer verification */
int qlogfd; int qlogfd;
unsigned char *tunnel_inbuf; /* ingress buffer for tunneled packets */
size_t tunnel_inbuf_len;
BIT(initialized); BIT(initialized);
BIT(tls_handshake_complete); /* TLS handshake is done */ BIT(tls_handshake_complete); /* TLS handshake is done */
BIT(use_earlydata); /* Using 0RTT data */ BIT(use_earlydata); /* Using 0RTT data */
@ -156,6 +160,8 @@ static void cf_ngtcp2_ctx_init(struct cf_ngtcp2_ctx *ctx)
{ {
DEBUGASSERT(!ctx->initialized); DEBUGASSERT(!ctx->initialized);
ctx->qlogfd = -1; ctx->qlogfd = -1;
ctx->tunnel_inbuf = NULL;
ctx->tunnel_inbuf_len = 0;
ctx->version = NGTCP2_PROTO_VER_MAX; ctx->version = NGTCP2_PROTO_VER_MAX;
Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE, Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
H3_STREAM_POOL_SPARES); H3_STREAM_POOL_SPARES);
@ -173,6 +179,8 @@ static void cf_ngtcp2_ctx_free(struct cf_ngtcp2_ctx *ctx)
curlx_dyn_free(&ctx->scratch); curlx_dyn_free(&ctx->scratch);
Curl_uint32_hash_destroy(&ctx->streams); Curl_uint32_hash_destroy(&ctx->streams);
Curl_ssl_peer_cleanup(&ctx->peer); Curl_ssl_peer_cleanup(&ctx->peer);
curlx_safefree(ctx->tunnel_inbuf);
ctx->tunnel_inbuf_len = 0;
} }
curlx_free(ctx); curlx_free(ctx);
} }
@ -493,7 +501,7 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx,
static CURLcode init_ngh3_conn(struct Curl_cfilter *cf, static CURLcode init_ngh3_conn(struct Curl_cfilter *cf,
struct Curl_easy *data); 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 Curl_cfilter *cf = user_data;
struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL; struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL;
@ -863,7 +871,7 @@ static ngtcp2_callbacks ng_callbacks = {
ngtcp2_crypto_client_initial_cb, ngtcp2_crypto_client_initial_cb,
NULL, /* recv_client_initial */ NULL, /* recv_client_initial */
ngtcp2_crypto_recv_crypto_data_cb, ngtcp2_crypto_recv_crypto_data_cb,
cf_ngtcp2_handshake_completed, cb_ngtcp2_handshake_completed,
NULL, /* recv_version_negotiation */ NULL, /* recv_version_negotiation */
ngtcp2_crypto_encrypt_cb, ngtcp2_crypto_encrypt_cb,
ngtcp2_crypto_decrypt_cb, ngtcp2_crypto_decrypt_cb,
@ -982,6 +990,11 @@ static CURLcode cf_ngtcp2_adjust_pollset(struct Curl_cfilter *cf,
if(!ctx->qconn) if(!ctx->qconn)
return CURLE_OK; 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); Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send);
if(!want_send && !Curl_bufq_is_empty(&ctx->q.sendbuf)) if(!want_send && !Curl_bufq_is_empty(&ctx->q.sendbuf))
want_send = TRUE; want_send = TRUE;
@ -1904,8 +1917,72 @@ static CURLcode cf_progress_ingress(struct Curl_cfilter *cf,
rctx.pktx = pktx; rctx.pktx = pktx;
rctx.pkt_count = 0; 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); cf_ngtcp2_recv_pkts, &rctx);
}
else {
/* Tunneled QUIC (CONNECT-UDP through proxy) */
unsigned char *buf;
size_t max_udp_payload = QUIC_TUNNEL_INBUF_SIZE;
size_t pkt_limit = QUIC_TUNNEL_INGRESS_PKT_LIMIT;
size_t nread;
struct sockaddr_storage remote_addr;
socklen_t remote_addrlen;
if(ctx->qconn) {
size_t max_path_payload;
max_path_payload =
ngtcp2_conn_get_path_max_tx_udp_payload_size(ctx->qconn);
if(max_path_payload > max_udp_payload)
max_udp_payload = max_path_payload;
}
if(ctx->tunnel_inbuf_len < max_udp_payload) {
unsigned char *newbuf =
(unsigned char *)curlx_realloc(ctx->tunnel_inbuf, max_udp_payload);
if(!newbuf)
return CURLE_OUT_OF_MEMORY;
ctx->tunnel_inbuf = newbuf;
ctx->tunnel_inbuf_len = max_udp_payload;
}
buf = ctx->tunnel_inbuf;
while(pkt_limit--) {
result = Curl_conn_cf_recv(cf->next, data, (char *)buf,
ctx->tunnel_inbuf_len, &nread);
if(result == CURLE_AGAIN) {
/* no more data available at the moment */
return CURLE_OK;
}
if(result) {
CURL_TRC_CF(data, cf, "ingress, recv from tunnel failed: %d",
result);
return result;
}
if(nread == 0) {
/* tunnel closed */
return CURLE_OK;
}
memcpy(&remote_addr, ctx->connected_path.remote.addr,
ctx->connected_path.remote.addrlen);
remote_addrlen = (socklen_t)ctx->connected_path.remote.addrlen;
result = cf_ngtcp2_recv_pkts(buf, nread, nread, &remote_addr,
remote_addrlen, 0, &rctx);
if(result)
return result;
if(!ctx->q.got_first_byte) {
ctx->q.got_first_byte = TRUE;
ctx->q.first_byte_at = ctx->q.last_op;
}
ctx->q.last_io = ctx->q.last_op;
}
return CURLE_OK;
}
} }
/** /**
@ -2189,6 +2266,7 @@ static void cf_ngtcp2_ctx_close(struct cf_ngtcp2_ctx *ctx)
} }
ctx->qlogfd = -1; ctx->qlogfd = -1;
Curl_vquic_tls_cleanup(&ctx->tls); Curl_vquic_tls_cleanup(&ctx->tls);
Curl_ssl_peer_cleanup(&ctx->peer);
vquic_ctx_free(&ctx->q); vquic_ctx_free(&ctx->q);
if(ctx->h3conn) { if(ctx->h3conn) {
nghttp3_conn_del(ctx->h3conn); nghttp3_conn_del(ctx->h3conn);
@ -2220,6 +2298,12 @@ static CURLcode cf_ngtcp2_shutdown(struct Curl_cfilter *cf,
return CURLE_OK; return CURLE_OK;
} }
if(!cf->next) {
Curl_bufq_reset(&ctx->q.sendbuf);
*done = TRUE;
return CURLE_OK;
}
CF_DATA_SAVE(save, cf, data); CF_DATA_SAVE(save, cf, data);
*done = FALSE; *done = FALSE;
pktx_init(&pktx, cf, data); pktx_init(&pktx, cf, data);
@ -2648,30 +2732,81 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
if(result) if(result)
return result; return result;
if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL)) /* Query socket and remote address from sub-chain */
return CURLE_QUIC_CONNECT_ERROR; if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL)) {
ctx->q.local_addrlen = sizeof(ctx->q.local_addr); /* No direct socket - must be tunneled QUIC (CONNECT-UDP through proxy) */
rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr, ctx->q.sockfd = CURL_SOCKET_BAD;
&ctx->q.local_addrlen); }
if(rv == -1)
return CURLE_QUIC_CONNECT_ERROR;
ngtcp2_addr_init(&ctx->connected_path.local, if(ctx->q.sockfd != CURL_SOCKET_BAD) {
(struct sockaddr *)&ctx->q.local_addr, /* Direct UDP socket - get local address for ngtcp2 */
ctx->q.local_addrlen); ctx->q.local_addrlen = sizeof(ctx->q.local_addr);
ngtcp2_addr_init(&ctx->connected_path.remote, rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr,
&sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen); &ctx->q.local_addrlen);
if(rv == -1)
return CURLE_QUIC_CONNECT_ERROR;
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid, ngtcp2_addr_init(&ctx->connected_path.local,
&ctx->connected_path, (struct sockaddr *)&ctx->q.local_addr,
NGTCP2_PROTO_VER_V1, &ng_callbacks, ctx->q.local_addrlen);
&ctx->settings, &ctx->transport_params, ngtcp2_addr_init(&ctx->connected_path.remote,
Curl_ngtcp2_mem(), cf); &sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ctx->conn_ref.get_conn = get_conn; rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
ctx->conn_ref.user_data = cf; &ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
}
else {
/* Tunneled QUIC (e.g. CONNECT-UDP): get remote address
from the connected filter below */
const struct Curl_sockaddr_ex *remote = NULL;
if(cf->next->cft->query(cf->next, data, CF_QUERY_REMOTE_ADDR, NULL,
CURL_UNCONST(&remote)))
return CURLE_QUIC_CONNECT_ERROR;
if(!remote)
return CURLE_QUIC_CONNECT_ERROR;
memset(&ctx->q.local_addr, 0, sizeof(ctx->q.local_addr));
switch(remote->family) {
case AF_INET:
((struct sockaddr_in *)&ctx->q.local_addr)->sin_family = AF_INET;
ctx->q.local_addrlen = sizeof(struct sockaddr_in);
break;
#ifdef USE_IPV6
case AF_INET6:
((struct sockaddr_in6 *)&ctx->q.local_addr)->sin6_family = AF_INET6;
ctx->q.local_addrlen = sizeof(struct sockaddr_in6);
break;
#endif
default:
return CURLE_QUIC_CONNECT_ERROR;
}
ngtcp2_addr_init(&ctx->connected_path.local,
(struct sockaddr *)&ctx->q.local_addr,
ctx->q.local_addrlen);
ngtcp2_addr_init(&ctx->connected_path.remote,
&remote->curl_sa_addr,
(socklen_t)remote->addrlen);
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
&ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
}
result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, &ALPN_SPEC_H3, result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, &ALPN_SPEC_H3,
cf_ngtcp2_tls_ctx_setup, &ctx->tls, cf_ngtcp2_tls_ctx_setup, &ctx->tls,
@ -2720,8 +2855,8 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf,
return CURLE_OK; return CURLE_OK;
} }
/* Connect the UDP filter first */ /* Connect the sub-chain */
if(!cf->next->connected) { if(cf->next && !cf->next->connected) {
result = Curl_conn_cf_connect(cf->next, data, done); result = Curl_conn_cf_connect(cf->next, data, done);
if(result || !*done) if(result || !*done)
return result; return result;
@ -2803,11 +2938,14 @@ out:
#ifdef CURLVERBOSE #ifdef CURLVERBOSE
if(result) { 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)) if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip))
infof(data, "QUIC connect to %s port %u failed: %s", infof(data, "QUIC connect to %s port %u failed: %s",
ip.remote_ip, ip.remote_port, curl_easy_strerror(result)); ip.remote_ip, ip.remote_port, curl_easy_strerror(result));
}
} }
#endif #endif
if(!result && ctx->qconn) { if(!result && ctx->qconn) {
@ -3003,4 +3141,33 @@ out:
return result; 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 #endif
/* Do not leak this filter's call_data accessor in unity builds. */
#undef CF_CTX_CALL_DATA

View file

@ -54,6 +54,8 @@ CURLcode Curl_cf_ngtcp2_create(struct Curl_cfilter **pcf,
struct Curl_easy *data, struct Curl_easy *data,
struct connectdata *conn, struct connectdata *conn,
struct Curl_sockaddr_ex *addr); struct Curl_sockaddr_ex *addr);
CURLcode Curl_cf_ngtcp2_insert_after(struct Curl_cfilter *cf_at);
#endif #endif
#endif /* HEADER_CURL_VQUIC_CURL_NGTCP2_H */ #endif /* HEADER_CURL_VQUIC_CURL_NGTCP2_H */

View file

@ -156,6 +156,7 @@ static void cf_quiche_ctx_close(struct cf_quiche_ctx *ctx)
quiche_config_free(ctx->cfg); quiche_config_free(ctx->cfg);
ctx->cfg = NULL; ctx->cfg = NULL;
} }
Curl_ssl_peer_cleanup(&ctx->peer);
} }
static CURLcode cf_flush_egress(struct Curl_cfilter *cf, static CURLcode cf_flush_egress(struct Curl_cfilter *cf,

View file

@ -72,6 +72,8 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx,
return CURLE_FAILED_INIT; return CURLE_FAILED_INIT;
#endif #endif
(void)session_reuse_cb; (void)session_reuse_cb;
if(peer->dest)
Curl_ssl_peer_cleanup(peer);
result = Curl_ssl_peer_init(peer, cf, tls_id, TRNSPRT_QUIC); result = Curl_ssl_peer_init(peer, cf, tls_id, TRNSPRT_QUIC);
if(result) if(result)
return result; return result;

View file

@ -261,6 +261,44 @@ out:
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, static CURLcode vquic_send_packets(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
struct cf_quic_ctx *qctx, struct cf_quic_ctx *qctx,
@ -310,7 +348,22 @@ CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data,
blen = qctx->split_len; 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) {
if(result == CURLE_AGAIN) { if(result == CURLE_AGAIN) {
Curl_bufq_skip(&qctx->sendbuf, sent); Curl_bufq_skip(&qctx->sendbuf, sent);
@ -699,6 +752,16 @@ CURLcode Curl_qlogdir(struct Curl_easy *data,
return CURLE_OK; 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, CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf,
struct Curl_easy *data, struct Curl_easy *data,
struct connectdata *conn, struct connectdata *conn,
@ -737,10 +800,6 @@ CURLcode Curl_conn_may_http3(struct Curl_easy *data,
failf(data, "HTTP/3 is not supported over a SOCKS proxy"); failf(data, "HTTP/3 is not supported over a SOCKS proxy");
return CURLE_URL_MALFORMAT; 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 #endif
return CURLE_OK; return CURLE_OK;

View file

@ -39,6 +39,8 @@ CURLcode Curl_qlogdir(struct Curl_easy *data,
size_t scidlen, size_t scidlen,
int *qlogfdp); int *qlogfdp);
CURLcode Curl_cf_quic_insert_after(struct Curl_cfilter *cf_at);
CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf, CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf,
struct Curl_easy *data, struct Curl_easy *data,
struct connectdata *conn, struct connectdata *conn,

View file

@ -3713,8 +3713,11 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
return result; return result;
} }
if(data->set.fdebug && data->set.verbose) { if(data->set.fdebug && data->set.verbose &&
/* the SSL trace callback is only used for verbose logging */ (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(octx->ssl_ctx, ossl_trace);
SSL_CTX_set_msg_callback_arg(octx->ssl_ctx, cf); SSL_CTX_set_msg_callback_arg(octx->ssl_ctx, cf);
} }
@ -4007,12 +4010,20 @@ static CURLcode ossl_connect_step1(struct Curl_cfilter *cf,
{ {
struct ssl_connect_data *connssl = cf->ctx; struct ssl_connect_data *connssl = cf->ctx;
struct ossl_ctx *octx = (struct ossl_ctx *)connssl->backend; struct ossl_ctx *octx = (struct ossl_ctx *)connssl->backend;
char tls_id[80];
BIO *bio; BIO *bio;
CURLcode result; CURLcode result;
DEBUGASSERT(ssl_connect_1 == connssl->connecting_state); DEBUGASSERT(ssl_connect_1 == connssl->connecting_state);
DEBUGASSERT(octx); 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, result = Curl_ossl_ctx_init(octx, cf, data, &connssl->peer,
connssl->alpn, NULL, NULL, connssl->alpn, NULL, NULL,
ossl_new_session_cb, cf, ossl_new_session_cb, cf,

View file

@ -1197,6 +1197,7 @@ void Curl_ssl_peer_cleanup(struct ssl_peer *peer)
Curl_peer_unlink(&peer->dest); Curl_peer_unlink(&peer->dest);
curlx_safefree(peer->sni); curlx_safefree(peer->sni);
curlx_safefree(peer->scache_key); curlx_safefree(peer->scache_key);
peer->transport = TRNSPRT_NONE;
peer->type = CURL_SSL_PEER_DNS; peer->type = CURL_SSL_PEER_DNS;
} }
@ -1206,6 +1207,8 @@ static void cf_close(struct Curl_cfilter *cf, struct Curl_easy *data)
if(connssl) { if(connssl) {
connssl->ssl_impl->close(cf, data); connssl->ssl_impl->close(cf, data);
connssl->state = ssl_connection_none; connssl->state = ssl_connection_none;
connssl->connecting_state = ssl_connect_1;
connssl->prefs_checked = FALSE;
Curl_ssl_peer_cleanup(&connssl->peer); Curl_ssl_peer_cleanup(&connssl->peer);
} }
cf->connected = FALSE; cf->connected = FALSE;

View file

@ -133,9 +133,6 @@ struct ssl_connect_data {
BIT(input_pending); /* data for SSL_read() may be available */ 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 */ /* Definitions for SSL Implementations */
struct Curl_ssl { struct Curl_ssl {
@ -209,3 +206,9 @@ CURLcode Curl_on_session_reuse(struct Curl_cfilter *cf,
#endif /* USE_SSL */ #endif /* USE_SSL */
#endif /* HEADER_CURL_VTLS_INT_H */ #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

View file

@ -250,6 +250,7 @@ static const struct LongShort aliases[]= {
{"proxy-digest", ARG_BOOL, ' ', C_PROXY_DIGEST}, {"proxy-digest", ARG_BOOL, ' ', C_PROXY_DIGEST},
{"proxy-header", ARG_STRG, ' ', C_PROXY_HEADER}, {"proxy-header", ARG_STRG, ' ', C_PROXY_HEADER},
{"proxy-http2", ARG_BOOL, ' ', C_PROXY_HTTP2}, {"proxy-http2", ARG_BOOL, ' ', C_PROXY_HTTP2},
{"proxy-http3", ARG_BOOL, ' ', C_PROXY_HTTP3},
{"proxy-insecure", ARG_BOOL, ' ', C_PROXY_INSECURE}, {"proxy-insecure", ARG_BOOL, ' ', C_PROXY_INSECURE},
{"proxy-key", ARG_FILE|ARG_TLS, ' ', C_PROXY_KEY}, {"proxy-key", ARG_FILE|ARG_TLS, ' ', C_PROXY_KEY},
{"proxy-key-type", ARG_STRG|ARG_TLS, ' ', C_PROXY_KEY_TYPE}, {"proxy-key-type", ARG_STRG|ARG_TLS, ' ', C_PROXY_KEY_TYPE},
@ -2024,6 +2025,18 @@ static ParameterError opt_bool(struct OperationConfig *config,
config->proxyver = toggle ? CURLPROXY_HTTPS2 : CURLPROXY_HTTPS; config->proxyver = toggle ? CURLPROXY_HTTPS2 : CURLPROXY_HTTPS;
break; 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 */ case C_APPEND: /* --append */
config->ftp_append = toggle; config->ftp_append = toggle;
break; break;
@ -2895,7 +2908,8 @@ static ParameterError opt_string(struct OperationConfig *config,
case C_PROXY: /* --proxy */ case C_PROXY: /* --proxy */
/* --proxy */ /* --proxy */
err = getstr(&config->proxy, nextarg, ALLOW_BLANK); 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; config->proxyver = CURLPROXY_HTTP;
break; break;
case C_REQUEST: /* --request */ case C_REQUEST: /* --request */

View file

@ -201,6 +201,7 @@ typedef enum {
C_PROXY_DIGEST, C_PROXY_DIGEST,
C_PROXY_HEADER, C_PROXY_HEADER,
C_PROXY_HTTP2, C_PROXY_HTTP2,
C_PROXY_HTTP3,
C_PROXY_INSECURE, C_PROXY_INSECURE,
C_PROXY_KEY, C_PROXY_KEY,
C_PROXY_KEY_TYPE, C_PROXY_KEY_TYPE,

View file

@ -542,6 +542,9 @@ const struct helptxt helptext[] = {
{ " --proxy-http2", { " --proxy-http2",
"Use HTTP/2 with HTTPS proxy", "Use HTTP/2 with HTTPS proxy",
CURLHELP_HTTP | CURLHELP_PROXY }, CURLHELP_HTTP | CURLHELP_PROXY },
{ " --proxy-http3",
"Use HTTP/3 with HTTPS proxy",
CURLHELP_HTTP | CURLHELP_PROXY },
{ " --proxy-insecure", { " --proxy-insecure",
"Skip HTTPS proxy cert verification", "Skip HTTPS proxy cert verification",
CURLHELP_PROXY | CURLHELP_TLS }, CURLHELP_PROXY | CURLHELP_TLS },

View file

@ -289,6 +289,8 @@ test3216 test3217 test3218 test3219 test3220 \
\ \
test3300 test3301 test3302 test3303 test3304 \ test3300 test3301 test3302 test3303 test3304 \
\ \
test3400 \
\
test4000 test4001 test4000 test4001
EXTRA_DIST = $(TESTCASES) DISABLED data-xml1 data320.html \ EXTRA_DIST = $(TESTCASES) DISABLED data-xml1 data320.html \

19
tests/data/test3400 Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
unittest
capsule
</keywords>
</info>
<client>
<features>
unittest
</features>
<name>
capsule protocol encode and decode unit tests
</name>
</client>
</testcase>

View file

@ -28,6 +28,12 @@ if(NOT CADDY)
endif() endif()
mark_as_advanced(CADDY) 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 find_program(VSFTPD "vsftpd") # /usr/sbin/vsftpd
if(NOT VSFTPD) if(NOT VSFTPD)
set(VSFTPD "") set(VSFTPD "")

View file

@ -31,6 +31,7 @@ TESTENV = \
testenv/dnsd.py \ testenv/dnsd.py \
testenv/dante.py \ testenv/dante.py \
testenv/env.py \ testenv/env.py \
testenv/h2o.py \
testenv/httpd.py \ testenv/httpd.py \
testenv/mod_curltest/mod_curltest.c \ testenv/mod_curltest/mod_curltest.c \
testenv/nghttpx.py \ testenv/nghttpx.py \
@ -72,6 +73,7 @@ EXTRA_DIST = \
test_40_socks.py \ test_40_socks.py \
test_50_scp.py \ test_50_scp.py \
test_51_sftp.py \ test_51_sftp.py \
test_60_h3_proxy.py \
$(TESTENV) $(TESTENV)
clean-local: clean-local:

View file

@ -44,3 +44,6 @@ danted = @DANTED@
[sshd] [sshd]
sshd = @SSHD@ sshd = @SSHD@
sftpd = @SFTPD@ sftpd = @SFTPD@
[h2o]
h2o = @H2O@

View file

@ -1,4 +1,4 @@
#*************************************************************************** # ***************************************************************************
# _ _ ____ _ # _ _ ____ _
# Project ___| | | | _ \| | # Project ___| | | | _ \| |
# / __| | | | |_) | | # / __| | | | |_) | |
@ -31,9 +31,10 @@ from typing import Generator, Union
import pytest import pytest
from testenv.env import EnvConfig 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 import Env, Httpd, Nghttpx, NghttpxFwd, NghttpxQuic, Sshd
from testenv.h2o import H2oProxy, H2oServer
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -42,51 +43,47 @@ def pytest_report_header(config):
# Env inits its base properties only once, we can report them here # Env inits its base properties only once, we can report them here
env = Env() env = Env()
report = [ report = [
f'Testing curl {env.curl_version()}', f"Testing curl {env.curl_version()}",
f' platform: {platform.platform()}', f" platform: {platform.platform()}",
f' curl: Version: {env.curl_version_string()}', f" curl: Version: {env.curl_version_string()}",
f' curl: Features: {env.curl_features_string()}', f" curl: Features: {env.curl_features_string()}",
f' curl: Protocols: {env.curl_protocols_string()}', f" curl: Protocols: {env.curl_protocols_string()}",
f' httpd: {env.httpd_version()}', f" httpd: {env.httpd_version()}",
f' httpd-proxy: {env.httpd_version()}' f" httpd-proxy: {env.httpd_version()}",
] ]
if env.have_h3(): if env.have_h3():
report.extend([ report.extend([f" nghttpx: {env.nghttpx_version()}"])
f' nghttpx: {env.nghttpx_version()}' if env.have_h2o():
]) report.extend([f" h2o: {env.h2o_version()}"])
if env.has_caddy(): if env.has_caddy():
report.extend([ report.extend([f" Caddy: {env.caddy_version()}"])
f' Caddy: {env.caddy_version()}'
])
if env.has_vsftpd(): if env.has_vsftpd():
report.extend([ report.extend([f" VsFTPD: {env.vsftpd_version()}"])
f' VsFTPD: {env.vsftpd_version()}' buildinfo_fn = os.path.join(env.build_dir, "buildinfo.txt")
])
buildinfo_fn = os.path.join(env.build_dir, 'buildinfo.txt')
if os.path.exists(buildinfo_fn): 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: for line in file_in:
line = line.strip() line = line.strip()
if line and not line.startswith('#'): if line and not line.startswith("#"):
report.extend([line]) 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: def env_config(pytestconfig, testrun_uid, worker_id) -> EnvConfig:
return EnvConfig(pytestconfig=pytestconfig, return EnvConfig(
testrun_uid=testrun_uid, pytestconfig=pytestconfig, testrun_uid=testrun_uid, worker_id=worker_id
worker_id=worker_id) )
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope="session", autouse=True)
def env(pytestconfig, env_config) -> Env: def env(pytestconfig, env_config) -> Env:
env = Env(pytestconfig=pytestconfig, env_config=env_config) env = Env(pytestconfig=pytestconfig, env_config=env_config)
level = logging.DEBUG if env.verbose > 0 else logging.INFO level = logging.DEBUG if env.verbose > 0 else logging.INFO
logging.getLogger('').setLevel(level=level) logging.getLogger("").setLevel(level=level)
if not env.curl_has_protocol('http'): if not env.curl_has_protocol("http"):
pytest.skip("curl built without HTTP support") 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") pytest.skip("curl built without HTTPS support")
if env.setup_incomplete(): if env.setup_incomplete():
pytest.skip(env.incomplete_reason()) pytest.skip(env.incomplete_reason())
@ -95,23 +92,23 @@ def env(pytestconfig, env_config) -> Env:
return env return env
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def httpd(env) -> Generator[Httpd, None, None]: def httpd(env) -> Generator[Httpd, None, None]:
httpd = Httpd(env=env) httpd = Httpd(env=env)
if not httpd.exists(): if not httpd.exists():
pytest.skip(f'httpd not found: {env.httpd}') pytest.skip(f"httpd not found: {env.httpd}")
httpd.clear_logs() httpd.clear_logs()
assert httpd.initial_start() assert httpd.initial_start()
yield httpd yield httpd
httpd.stop() httpd.stop()
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def nghttpx(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]: def nghttpx(env, httpd) -> Generator[Union[Nghttpx, bool], None, None]:
nghttpx = NghttpxQuic(env=env) nghttpx = NghttpxQuic(env=env)
if nghttpx.exists(): if nghttpx.exists():
if not nghttpx.supports_h3() and env.have_h3_curl(): 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() nghttpx.clear_logs()
assert nghttpx.initial_start() assert nghttpx.initial_start()
yield nghttpx yield nghttpx
@ -120,8 +117,8 @@ def nghttpx(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]:
yield False yield False
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def nghttpx_fwd(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]: def nghttpx_fwd(env, httpd) -> Generator[Union[Nghttpx, bool], None, None]:
nghttpx = NghttpxFwd(env=env) nghttpx = NghttpxFwd(env=env)
if nghttpx.exists(): if nghttpx.exists():
nghttpx.clear_logs() nghttpx.clear_logs()
@ -132,37 +129,63 @@ def nghttpx_fwd(env, httpd) -> Generator[Union[Nghttpx,bool], None, None]:
yield False yield False
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def sshd(env: Env) -> Generator[Union[Sshd,bool], None, None]: def sshd(env: Env) -> Generator[Union[Sshd, bool], None, None]:
if env.has_sshd(): if env.has_sshd():
sshd = Sshd(env=env) sshd = Sshd(env=env)
assert sshd.initial_start(), f'{sshd.dump_log()}' assert sshd.initial_start(), f"{sshd.dump_log()}"
yield sshd yield sshd
sshd.stop() sshd.stop()
else: else:
yield False yield False
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def configures_httpd(env, httpd) -> Generator[bool, None, None]: def configures_httpd(env, httpd) -> Generator[bool, None, None]:
# include this fixture as test parameter if the test configures httpd itself # include this fixture as test parameter if the test configures httpd itself
yield True yield True
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def configures_nghttpx(env, httpd) -> Generator[bool, None, None]: def configures_nghttpx(env, httpd) -> Generator[bool, None, None]:
# include this fixture as test parameter if the test configures nghttpx itself # include this fixture as test parameter if the test configures nghttpx itself
yield True yield True
@pytest.fixture(autouse=True, scope='function') @pytest.fixture(autouse=True, scope="function")
def server_reset(request, env, httpd, nghttpx): def server_reset(request, env, httpd, nghttpx):
# make sure httpd is in default configuration when a test starts # 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.reset_config()
httpd.reload_if_config_changed() httpd.reload_if_config_changed()
if env.have_h3() and \ if (
'nghttpx' in request.node._fixtureinfo.argnames and \ env.have_h3()
'configures_nghttpx' not in request.node._fixtureinfo.argnames: and "nghttpx" in request.node._fixtureinfo.argnames
and "configures_nghttpx" not in request.node._fixtureinfo.argnames
):
nghttpx.reset_config() nghttpx.reset_config()
nghttpx.reload_if_config_changed() 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

View file

@ -0,0 +1,689 @@
#!/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)

View file

@ -688,7 +688,12 @@ class CurlClient:
proxy_name = '[::1]' if use_ipv6 else \ proxy_name = '[::1]' if use_ipv6 else \
self._server_addr if use_ip else self.env.proxy_domain self._server_addr if use_ip else self.env.proxy_domain
if proxys: 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 = [ xargs = [
'--proxy', f'https://{proxy_name}:{pport}/', '--proxy', f'https://{proxy_name}:{pport}/',
'--proxy-cacert', self.env.ca.cert_file, '--proxy-cacert', self.env.ca.cert_file,
@ -697,6 +702,8 @@ class CurlClient:
xargs.extend(['--resolve', f'{proxy_name}:{pport}:{self._server_addr}']) xargs.extend(['--resolve', f'{proxy_name}:{pport}:{self._server_addr}'])
if proto == 'h2': if proto == 'h2':
xargs.append('--proxy-http2') xargs.append('--proxy-http2')
elif proto == 'h3':
xargs.append('--proxy-http3')
else: else:
xargs = [ xargs = [
'--proxy', f'http://{proxy_name}:{self.env.proxy_port}/', '--proxy', f'http://{proxy_name}:{self.env.proxy_port}/',

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#*************************************************************************** # ***************************************************************************
# _ _ ____ _ # _ _ ____ _
# Project ___| | | | _ \| | # Project ___| | | | _ \| |
# / __| | | | |_) | | # / __| | | | |_) | |
@ -54,20 +54,21 @@ def init_config_from(conf_path):
TESTS_HTTPD_PATH = os.path.dirname(os.path.dirname(__file__)) TESTS_HTTPD_PATH = os.path.dirname(os.path.dirname(__file__))
PROJ_PATH = os.path.dirname(os.path.dirname(TESTS_HTTPD_PATH)) PROJ_PATH = os.path.dirname(os.path.dirname(TESTS_HTTPD_PATH))
TOP_PATH = os.path.join(os.getcwd(), os.path.pardir) 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): 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): 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 TOP_PATH = PROJ_PATH
CONFIG_PATH = ALT_CONFIG_PATH CONFIG_PATH = ALT_CONFIG_PATH
DEF_CONFIG = init_config_from(CONFIG_PATH) DEF_CONFIG = init_config_from(CONFIG_PATH)
CURL = os.path.join(TOP_PATH, 'src', 'curl') CURL = os.path.join(TOP_PATH, "src", "curl")
CURLINFO = os.path.join(TOP_PATH, 'src', 'curlinfo') CURLINFO = os.path.join(TOP_PATH, "src", "curlinfo")
class NghttpxUtil: class NghttpxUtil:
CMD = None CMD = None
VERSION_FULL = None VERSION_FULL = None
@ -76,34 +77,37 @@ class NghttpxUtil:
if cmd is None: if cmd is None:
return None return None
if cls.VERSION_FULL is None or cmd != cls.CMD: if cls.VERSION_FULL is None or cmd != cls.CMD:
p = subprocess.run(args=[cmd, '--version'], p = subprocess.run(args=[cmd, "--version"], capture_output=True, text=True)
capture_output=True, text=True)
if p.returncode != 0: 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 cls.CMD = cmd
for line in p.stdout.splitlines(keepends=False): for line in p.stdout.splitlines(keepends=False):
if line.startswith('nghttpx '): if line.startswith("nghttpx "):
cls.VERSION_FULL = line cls.VERSION_FULL = line
if cls.VERSION_FULL is None: 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 return cls.VERSION_FULL
@staticmethod @staticmethod
def version_with_h3(version): 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: class EnvConfig:
def __init__(
def __init__(self, pytestconfig: Optional[pytest.Config] = None, self,
testrun_uid=None, pytestconfig: Optional[pytest.Config] = None,
worker_id=None): testrun_uid=None,
worker_id=None,
):
self.pytestconfig = pytestconfig self.pytestconfig = pytestconfig
self.testrun_uid = testrun_uid 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.tests_dir = TESTS_HTTPD_PATH
self.gen_root = self.gen_dir = os.path.join(self.tests_dir, 'gen') self.gen_root = self.gen_dir = os.path.join(self.tests_dir, "gen")
if self.worker_id != 'master': if self.worker_id != "master":
self.gen_dir = os.path.join(self.gen_dir, self.worker_id) 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.project_dir = os.path.dirname(os.path.dirname(self.tests_dir))
self.build_dir = TOP_PATH self.build_dir = TOP_PATH
@ -111,57 +115,56 @@ class EnvConfig:
# check cur and its features # check cur and its features
self.curl = CURL self.curl = CURL
self.curlinfo = CURLINFO self.curlinfo = CURLINFO
if 'CURL' in os.environ: if "CURL" in os.environ:
self.curl = os.environ['CURL'] self.curl = os.environ["CURL"]
self.curl_props = { self.curl_props = {
'version_string': '', "version_string": "",
'version': '', "version": "",
'os': '', "os": "",
'fullname': '', "fullname": "",
'features_string': '', "features_string": "",
'features': set(), "features": set(),
'protocols_string': '', "protocols_string": "",
'protocols': set(), "protocols": set(),
'libs': set(), "libs": set(),
'lib_versions': set(), "lib_versions": set(),
} }
self.curl_is_debug = False self.curl_is_debug = False
self.curl_protos = [] self.curl_protos = []
p = subprocess.run(args=[self.curl, '-V'], p = subprocess.run(args=[self.curl, "-V"], capture_output=True, text=True)
capture_output=True, text=True)
if p.returncode != 0: if p.returncode != 0:
raise RuntimeError(f'{self.curl} -V failed with exit code: {p.returncode}') raise RuntimeError(f"{self.curl} -V failed with exit code: {p.returncode}")
if p.stderr.startswith('WARNING:'): if p.stderr.startswith("WARNING:"):
self.curl_is_debug = True self.curl_is_debug = True
for line in p.stdout.splitlines(keepends=False): for line in p.stdout.splitlines(keepends=False):
if line.startswith('curl '): if line.startswith("curl "):
self.curl_props['version_string'] = line self.curl_props["version_string"] = line
m = re.match(r'^curl (?P<version>\S+) (?P<os>\S+) (?P<libs>.*)$', line) m = re.match(r"^curl (?P<version>\S+) (?P<os>\S+) (?P<libs>.*)$", line)
if m: if m:
self.curl_props['fullname'] = m.group(0) self.curl_props["fullname"] = m.group(0)
self.curl_props['version'] = m.group('version') self.curl_props["version"] = m.group("version")
self.curl_props['os'] = m.group('os') self.curl_props["os"] = m.group("os")
self.curl_props['lib_versions'] = { self.curl_props["lib_versions"] = {
lib.lower() for lib in m.group('libs').split(' ') lib.lower() for lib in m.group("libs").split(" ")
} }
self.curl_props['libs'] = { self.curl_props["libs"] = {
re.sub(r'/[a-z0-9.-]*', '', lib) for lib in self.curl_props['lib_versions'] re.sub(r"/[a-z0-9.-]*", "", lib)
for lib in self.curl_props["lib_versions"]
} }
if line.startswith('Features: '): if line.startswith("Features: "):
self.curl_props['features_string'] = line[10:] self.curl_props["features_string"] = line[10:]
self.curl_props['features'] = { self.curl_props["features"] = {
feat.lower() for feat in line[10:].split(' ') feat.lower() for feat in line[10:].split(" ")
} }
if line.startswith('Protocols: '): if line.startswith("Protocols: "):
self.curl_props['protocols_string'] = line[11:] self.curl_props["protocols_string"] = line[11:]
self.curl_props['protocols'] = { self.curl_props["protocols"] = {
prot.lower() for prot in line[11:].split(' ') prot.lower() for prot in line[11:].split(" ")
} }
p = subprocess.run(args=[self.curlinfo], p = subprocess.run(args=[self.curlinfo], capture_output=True, text=True)
capture_output=True, text=True)
if p.returncode != 0: 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_is_verbose = 'verbose-strings: ON' in p.stdout
self.curl_can_cert_status = 'cert-status: 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.curl_override_dns = 'override-dns: ON' in p.stdout
@ -169,18 +172,18 @@ class EnvConfig:
self.ports = {} self.ports = {}
self.httpd = self.config['httpd']['httpd'] self.httpd = self.config["httpd"]["httpd"]
self.apxs = self.config['httpd']['apxs'] self.apxs = self.config["httpd"]["apxs"]
if len(self.apxs) == 0: if len(self.apxs) == 0:
self.apxs = None self.apxs = None
self._httpd_version = None self._httpd_version = None
self.examples_pem = { self.examples_pem = {
'key': 'xxx', "key": "xxx",
'cert': 'xxx', "cert": "xxx",
} }
self.htdocs_dir = os.path.join(self.gen_dir, 'htdocs') self.htdocs_dir = os.path.join(self.gen_dir, "htdocs")
self.tld = 'http.curl.se' self.tld = "http.curl.se"
self.domain1 = f"one.{self.tld}" self.domain1 = f"one.{self.tld}"
self.domain1brotli = f"brotli.one.{self.tld}" self.domain1brotli = f"brotli.one.{self.tld}"
self.domain2 = f"two.{self.tld}" self.domain2 = f"two.{self.tld}"
@ -188,22 +191,43 @@ class EnvConfig:
self.proxy_domain = f"proxy.{self.tld}" self.proxy_domain = f"proxy.{self.tld}"
self.expired_domain = f"expired.{self.tld}" self.expired_domain = f"expired.{self.tld}"
self.cert_specs = [ self.cert_specs = [
CertificateSpec(domains=[self.domain1, self.domain1brotli, 'localhost', '127.0.0.1'], key_type='rsa2048'), CertificateSpec(
CertificateSpec(name='domain1-no-ip', domains=[self.domain1, self.domain1brotli], key_type='rsa2048'), domains=[self.domain1, self.domain1brotli, "localhost", "127.0.0.1"],
CertificateSpec(name='domain1-very-bad', domains=[self.domain1, 'dns:127.0.0.1'], key_type='rsa2048'), key_type="rsa2048",
CertificateSpec(domains=[self.domain2], key_type='rsa2048'), ),
CertificateSpec(domains=[self.ftp_domain], key_type='rsa2048'), CertificateSpec(
CertificateSpec(domains=[self.proxy_domain, '127.0.0.1'], key_type='rsa2048'), name="domain1-no-ip",
CertificateSpec(domains=[self.expired_domain], key_type='rsa2048', domains=[self.domain1, self.domain1brotli],
valid_from=timedelta(days=-100), valid_to=timedelta(days=-10)), key_type="rsa2048",
CertificateSpec(name="clientsX", sub_specs=[ ),
CertificateSpec(name="user1", client=True), 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' self.openssl = "openssl"
p = subprocess.run(args=[self.openssl, 'version'], p = subprocess.run(
capture_output=True, text=True) args=[self.openssl, "version"], capture_output=True, text=True
)
if p.returncode != 0: if p.returncode != 0:
# no openssl in path # no openssl in path
self.openssl = None self.openssl = None
@ -211,7 +235,7 @@ class EnvConfig:
else: else:
self.openssl_version = p.stdout.strip() self.openssl_version = p.stdout.strip()
self.nghttpx = self.config['nghttpx']['nghttpx'] self.nghttpx = self.config["nghttpx"]["nghttpx"]
if len(self.nghttpx.strip()) == 0: if len(self.nghttpx.strip()) == 0:
self.nghttpx = None self.nghttpx = None
self._nghttpx_version = None self._nghttpx_version = None
@ -220,30 +244,58 @@ class EnvConfig:
self._nghttpx_version = NghttpxUtil.version(self.nghttpx) self._nghttpx_version = NghttpxUtil.version(self.nghttpx)
self.nghttpx_with_h3 = NghttpxUtil.version_with_h3(self._nghttpx_version) 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 self._caddy_version = None
if len(self.caddy.strip()) == 0: if len(self.caddy.strip()) == 0:
self.caddy = None 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: if self.caddy is not None:
p = subprocess.run(args=[self.caddy, 'version'], p = subprocess.run(
capture_output=True, text=True) args=[self.caddy, "version"], capture_output=True, text=True
)
if p.returncode != 0: if p.returncode != 0:
# not a working caddy # not a working caddy
self.caddy = None 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: if m:
self._caddy_version = m.group(1) self._caddy_version = m.group(1)
else: 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'] self.vsftpd = self.config["vsftpd"]["vsftpd"]
if self.vsftpd == '': if self.vsftpd == "":
self.vsftpd = None self.vsftpd = None
self._vsftpd_version = None self._vsftpd_version = None
if self.vsftpd is not None: if self.vsftpd is not None:
with tempfile.TemporaryFile('w+') as tmp: with tempfile.TemporaryFile("w+") as tmp:
p = subprocess.run(args=[self.vsftpd, '-v'], p = subprocess.run(
capture_output=True, text=True, stdin=tmp) args=[self.vsftpd, "-v"], capture_output=True, text=True, stdin=tmp
)
if p.returncode != 0: if p.returncode != 0:
# not a working vsftpd # not a working vsftpd
self.vsftpd = None self.vsftpd = None
@ -256,80 +308,83 @@ class EnvConfig:
# any data there instead. # any data there instead.
tmp.seek(0) tmp.seek(0)
ver_text = tmp.read() 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: if m:
self._vsftpd_version = m.group(1) self._vsftpd_version = m.group(1)
elif len(p.stderr) == 0: elif len(p.stderr) == 0:
# vsftp does not use stdout or stderr for printing its version... -.- # vsftp does not use stdout or stderr for printing its version... -.-
self._vsftpd_version = 'unknown' self._vsftpd_version = "unknown"
else: 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'] self.danted = self.config["danted"]["danted"]
if self.danted == '': if self.danted == "":
self.danted = None self.danted = None
self._danted_version = None self._danted_version = None
if self.danted is not None: if self.danted is not None:
p = subprocess.run(args=[self.danted, '-v'], p = subprocess.run(args=[self.danted, "-v"], capture_output=True, text=True)
capture_output=True, text=True)
assert p.returncode == 0 assert p.returncode == 0
if p.returncode != 0: if p.returncode != 0:
# not a working vsftpd # not a working vsftpd
self.danted = None 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: 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: if m:
self._danted_version = m.group(1) self._danted_version = m.group(1)
else: else:
self.danted = None 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'] self.sshd = self.config["sshd"]["sshd"]
if self.sshd == '': if self.sshd == "":
self.sshd = None self.sshd = None
self._sshd_version = None self._sshd_version = None
if self.sshd is not None: if self.sshd is not None:
p = subprocess.run(args=[self.sshd, '-V'], p = subprocess.run(args=[self.sshd, "-V"], capture_output=True, text=True)
capture_output=True, text=True)
assert p.returncode == 0 assert p.returncode == 0
if p.returncode != 0: if p.returncode != 0:
self.sshd = None self.sshd = None
else: else:
m = re.match(r'^OpenSSH_(\d+\.\d+.*),.*', p.stderr) m = re.match(r"^OpenSSH_(\d+\.\d+.*),.*", p.stderr)
assert m, f'version: {p.stderr}' assert m, f"version: {p.stderr}"
if m: if m:
self._sshd_version = m.group(1) self._sshd_version = m.group(1)
else: else:
self.sshd = None 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: if self.sshd:
self.sftpd = self.config['sshd']['sftpd'] self.sftpd = self.config["sshd"]["sftpd"]
if self.sftpd == '': if self.sftpd == "":
self.sftpd = None self.sftpd = None
else: else:
self.sftpd = None self.sftpd = None
self._tcpdump = shutil.which('tcpdump') self._tcpdump = shutil.which("tcpdump")
@property @property
def httpd_version(self): def httpd_version(self):
if self._httpd_version is None and self.apxs is not None: if self._httpd_version is None and self.apxs is not None:
try: try:
p = subprocess.run(args=[self.apxs, '-q', 'HTTPD_VERSION'], p = subprocess.run(
capture_output=True, text=True) args=[self.apxs, "-q", "HTTPD_VERSION"],
capture_output=True,
text=True,
)
if p.returncode != 0: 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: else:
self._httpd_version = p.stdout.strip() self._httpd_version = p.stdout.strip()
except Exception: except Exception:
log.exception(f'{self.apxs} failed to run') log.exception(f"{self.apxs} failed to run")
return self._httpd_version return self._httpd_version
def versiontuple(self, v): def versiontuple(self, v):
v = re.sub(r'(\d+\.\d+(\.\d+)?)(-\S+)?', r'\1', v) v = re.sub(r"(\d+\.\d+(\.\d+)?)(-\S+)?", r"\1", v)
return tuple(map(int, v.split('.'))) return tuple(map(int, v.split(".")))
def httpd_is_at_least(self, minv): def httpd_is_at_least(self, minv):
if self.httpd_version is None: if self.httpd_version is None:
@ -344,15 +399,17 @@ class EnvConfig:
return hv >= self.versiontuple(minv) return hv >= self.versiontuple(minv)
def is_complete(self) -> bool: def is_complete(self) -> bool:
return os.path.isfile(self.httpd) and \ return (
self.apxs is not None and \ os.path.isfile(self.httpd)
os.path.isfile(self.apxs) and self.apxs is not None
and os.path.isfile(self.apxs)
)
def get_incomplete_reason(self) -> Optional[str]: def get_incomplete_reason(self) -> Optional[str]:
if self.httpd is None or len(self.httpd.strip()) == 0: 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): 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: if self.apxs is None:
return "command apxs not found (commonly provided in apache2-dev)" return "command apxs not found (commonly provided in apache2-dev)"
if not os.path.isfile(self.apxs): if not os.path.isfile(self.apxs):
@ -371,18 +428,21 @@ class EnvConfig:
def vsftpd_version(self): def vsftpd_version(self):
return self._vsftpd_version return self._vsftpd_version
@property
def h2o_version(self):
return self._h2o_version
@property @property
def tcpdmp(self) -> Optional[str]: def tcpdmp(self) -> Optional[str]:
return self._tcpdump return self._tcpdump
def clear_locks(self): 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): if os.path.exists(ca_lock):
os.remove(ca_lock) os.remove(ca_lock)
class Env: class Env:
SERVER_TIMEOUT = 30 # seconds to wait for server to come up/reload SERVER_TIMEOUT = 30 # seconds to wait for server to come up/reload
CONFIG = EnvConfig() CONFIG = EnvConfig()
@ -407,98 +467,106 @@ class Env:
def have_h3_server() -> bool: def have_h3_server() -> bool:
return Env.CONFIG.nghttpx_with_h3 return Env.CONFIG.nghttpx_with_h3
@staticmethod
def have_h2o() -> bool:
return Env.CONFIG.h2o is not None
@staticmethod @staticmethod
def have_ssl_curl() -> bool: 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 @staticmethod
def have_h2_curl() -> bool: def have_h2_curl() -> bool:
return 'http2' in Env.CONFIG.curl_props['features'] return "http2" in Env.CONFIG.curl_props["features"]
@staticmethod @staticmethod
def have_h3_curl() -> bool: def have_h3_curl() -> bool:
return 'http3' in Env.CONFIG.curl_props['features'] return "http3" in Env.CONFIG.curl_props["features"]
@staticmethod @staticmethod
def have_compressed_curl() -> bool: def have_compressed_curl() -> bool:
return 'brotli' in Env.CONFIG.curl_props['libs'] or \ return (
'zlib' in Env.CONFIG.curl_props['libs'] or \ "brotli" in Env.CONFIG.curl_props["libs"]
'zstd' in Env.CONFIG.curl_props['libs'] or "zlib" in Env.CONFIG.curl_props["libs"]
or "zstd" in Env.CONFIG.curl_props["libs"]
)
@staticmethod @staticmethod
def curl_uses_lib(libname: str) -> bool: 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 @staticmethod
def curl_uses_any_libs(libs: List[str]) -> bool: def curl_uses_any_libs(libs: List[str]) -> bool:
for libname in libs: 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 True
return False return False
@staticmethod @staticmethod
def curl_uses_ossl_quic() -> bool: def curl_uses_ossl_quic() -> bool:
if Env.have_h3_curl(): 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 return False
@staticmethod @staticmethod
def curl_version_string() -> str: def curl_version_string() -> str:
return Env.CONFIG.curl_props['version_string'] return Env.CONFIG.curl_props["version_string"]
@staticmethod @staticmethod
def curl_features_string() -> str: def curl_features_string() -> str:
return Env.CONFIG.curl_props['features_string'] return Env.CONFIG.curl_props["features_string"]
@staticmethod @staticmethod
def curl_has_feature(feature: str) -> bool: 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 @staticmethod
def curl_protocols_string() -> str: def curl_protocols_string() -> str:
return Env.CONFIG.curl_props['protocols_string'] return Env.CONFIG.curl_props["protocols_string"]
@staticmethod @staticmethod
def curl_has_protocol(protocol: str) -> bool: 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 @staticmethod
def curl_lib_version(libname: str) -> str: def curl_lib_version(libname: str) -> str:
prefix = f'{libname.lower()}/' prefix = f"{libname.lower()}/"
for lversion in Env.CONFIG.curl_props['lib_versions']: for lversion in Env.CONFIG.curl_props["lib_versions"]:
if lversion.startswith(prefix): if lversion.startswith(prefix):
return lversion[len(prefix):] return lversion[len(prefix) :]
return 'unknown' return "unknown"
@staticmethod @staticmethod
def curl_lib_version_at_least(libname: str, min_version) -> bool: def curl_lib_version_at_least(libname: str, min_version) -> bool:
lversion = Env.curl_lib_version(libname) lversion = Env.curl_lib_version(libname)
if lversion != 'unknown': if lversion != "unknown":
return Env.CONFIG.versiontuple(min_version) <= \ return Env.CONFIG.versiontuple(min_version) <= Env.CONFIG.versiontuple(
Env.CONFIG.versiontuple(lversion) lversion
)
return False return False
@staticmethod @staticmethod
def curl_lib_version_before(libname: str, lib_version) -> bool: def curl_lib_version_before(libname: str, lib_version) -> bool:
lversion = Env.curl_lib_version(libname) lversion = Env.curl_lib_version(libname)
if lversion != 'unknown': if lversion != "unknown":
if m := re.match(r'(\d+\.\d+\.\d+).*', lversion): if m := re.match(r"(\d+\.\d+\.\d+).*", lversion):
lversion = m.group(1) lversion = m.group(1)
return Env.CONFIG.versiontuple(lib_version) > \ return Env.CONFIG.versiontuple(lib_version) > Env.CONFIG.versiontuple(
Env.CONFIG.versiontuple(lversion) lversion
)
return False return False
@staticmethod @staticmethod
def curl_os() -> str: def curl_os() -> str:
return Env.CONFIG.curl_props['os'] return Env.CONFIG.curl_props["os"]
@staticmethod @staticmethod
def curl_fullname() -> str: def curl_fullname() -> str:
return Env.CONFIG.curl_props['fullname'] return Env.CONFIG.curl_props["fullname"]
@staticmethod @staticmethod
def curl_version() -> str: def curl_version() -> str:
return Env.CONFIG.curl_props['version'] return Env.CONFIG.curl_props["version"]
@staticmethod @staticmethod
def curl_is_debug() -> bool: def curl_is_debug() -> bool:
@ -528,32 +596,31 @@ class Env:
@staticmethod @staticmethod
def curl_can_h3_early_data() -> bool: def curl_can_h3_early_data() -> bool:
return Env.curl_can_early_data() and \ return Env.curl_can_early_data() and Env.curl_uses_lib("ngtcp2")
Env.curl_uses_lib('ngtcp2')
@staticmethod @staticmethod
def http_protos() -> List[str]: def http_protos() -> List[str]:
# http protocols we can test # http protocols we can test
if Env.have_h2_curl(): if Env.have_h2_curl():
if Env.have_h3(): if Env.have_h3():
return ['http/1.1', 'h2', 'h3'] return ["http/1.1", "h2", "h3"]
return ['http/1.1', 'h2'] return ["http/1.1", "h2"]
return ['http/1.1'] return ["http/1.1"]
@staticmethod @staticmethod
def http_h1_h2_protos() -> List[str]: def http_h1_h2_protos() -> List[str]:
# http 1+2 protocols we can test # http 1+2 protocols we can test
if Env.have_h2_curl(): if Env.have_h2_curl():
return ['http/1.1', 'h2'] return ["http/1.1", "h2"]
return ['http/1.1'] return ["http/1.1"]
@staticmethod @staticmethod
def http_mplx_protos() -> List[str]: def http_mplx_protos() -> List[str]:
# http multiplexing protocols we can test # http multiplexing protocols we can test
if Env.have_h2_curl(): if Env.have_h2_curl():
if Env.have_h3(): if Env.have_h3():
return ['h2', 'h3'] return ["h2", "h3"]
return ['h2'] return ["h2"]
return [] return []
@staticmethod @staticmethod
@ -572,6 +639,10 @@ class Env:
def caddy_version() -> str: def caddy_version() -> str:
return Env.CONFIG.caddy_version return Env.CONFIG.caddy_version
@staticmethod
def h2o_version() -> str:
return Env.CONFIG.h2o_version
@staticmethod @staticmethod
def caddy_is_at_least(minv) -> bool: def caddy_is_at_least(minv) -> bool:
return Env.CONFIG.caddy_is_at_least(minv) return Env.CONFIG.caddy_is_at_least(minv)
@ -611,21 +682,20 @@ class Env:
def __init__(self, pytestconfig=None, env_config=None): def __init__(self, pytestconfig=None, env_config=None):
if env_config: if env_config:
Env.CONFIG = env_config Env.CONFIG = env_config
self._verbose = pytestconfig.option.verbose \ self._verbose = pytestconfig.option.verbose if pytestconfig is not None else 0
if pytestconfig is not None else 0
self._ca = None self._ca = None
self._test_timeout = 300.0 if self._verbose > 1 else 60.0 # seconds self._test_timeout = 300.0 if self._verbose > 1 else 60.0 # seconds
def issue_certs(self): def issue_certs(self):
if self._ca is None: if self._ca is None:
# ca_dir = os.path.join(self.CONFIG.gen_root, 'ca') # 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) 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): with FileLock(lock_file):
self._ca = TestCA.create_root(name=self.CONFIG.tld, self._ca = TestCA.create_root(
store_dir=ca_dir, name=self.CONFIG.tld, store_dir=ca_dir, key_type="rsa2048"
key_type="rsa2048") )
self._ca.issue_certs(self.CONFIG.cert_specs) self._ca.issue_certs(self.CONFIG.cert_specs)
if self.have_openssl(): if self.have_openssl():
self._ca.create_hashdir(self.openssl) self._ca.create_hashdir(self.openssl)
@ -714,19 +784,19 @@ class Env:
@property @property
def http_port(self) -> int: def http_port(self) -> int:
return self.CONFIG.ports.get('http', 0) return self.CONFIG.ports.get("http", 0)
@property @property
def https_port(self) -> int: def https_port(self) -> int:
return self.CONFIG.ports['https'] return self.CONFIG.ports["https"]
@property @property
def https_only_tcp_port(self) -> int: def https_only_tcp_port(self) -> int:
return self.CONFIG.ports['https-tcp-only'] return self.CONFIG.ports["https-tcp-only"]
@property @property
def nghttpx_https_port(self) -> int: def nghttpx_https_port(self) -> int:
return self.CONFIG.ports['nghttpx_https'] return self.CONFIG.ports["nghttpx_https"]
@property @property
def h3_port(self) -> int: def h3_port(self) -> int:
@ -734,27 +804,35 @@ class Env:
@property @property
def proxy_port(self) -> int: def proxy_port(self) -> int:
return self.CONFIG.ports['proxy'] return self.CONFIG.ports["proxy"]
@property @property
def proxys_port(self) -> int: def proxys_port(self) -> int:
return self.CONFIG.ports['proxys'] return self.CONFIG.ports["proxys"]
@property @property
def ftp_port(self) -> int: def ftp_port(self) -> int:
return self.CONFIG.ports['ftp'] return self.CONFIG.ports["ftp"]
@property @property
def ftps_port(self) -> int: def ftps_port(self) -> int:
return self.CONFIG.ports['ftps'] return self.CONFIG.ports["ftps"]
@property @property
def h2proxys_port(self) -> int: def h2proxys_port(self) -> int:
return self.CONFIG.ports['h2proxys'] return self.CONFIG.ports["h2proxys"]
def pts_port(self, proto: str = 'http/1.1') -> int: @property
def h3proxys_port(self) -> int:
return self.CONFIG.ports["h3proxys"]
def pts_port(self, proto: str = "http/1.1") -> int:
# proxy tunnel port # 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 @property
def caddy(self) -> str: def caddy(self) -> str:
@ -762,11 +840,11 @@ class Env:
@property @property
def caddy_https_port(self) -> int: def caddy_https_port(self) -> int:
return self.CONFIG.ports['caddys'] return self.CONFIG.ports["caddys"]
@property @property
def caddy_http_port(self) -> int: def caddy_http_port(self) -> int:
return self.CONFIG.ports['caddy'] return self.CONFIG.ports["caddy"]
@property @property
def danted(self) -> str: def danted(self) -> str:
@ -778,7 +856,7 @@ class Env:
@property @property
def ws_port(self) -> int: def ws_port(self) -> int:
return self.CONFIG.ports['ws'] return self.CONFIG.ports["ws"]
@property @property
def curl(self) -> str: def curl(self) -> str:
@ -802,58 +880,65 @@ class Env:
@property @property
def slow_network(self) -> bool: def slow_network(self) -> bool:
return "CURL_DBG_SOCK_WBLOCK" in os.environ or \ return (
"CURL_DBG_SOCK_WPARTIAL" in os.environ "CURL_DBG_SOCK_WBLOCK" in os.environ
or "CURL_DBG_SOCK_WPARTIAL" in os.environ
)
@property @property
def ci_run(self) -> bool: def ci_run(self) -> bool:
return "CURL_CI" in os.environ return "CURL_CI" in os.environ
def port_for(self, alpn_proto: Optional[str] = None): def port_for(self, alpn_proto: Optional[str] = None):
if alpn_proto is None or \ if alpn_proto is None or alpn_proto in [
alpn_proto in ['h2', 'http/1.1', 'http/1.0', 'http/0.9']: "h2",
"http/1.1",
"http/1.0",
"http/0.9",
]:
return self.https_port return self.https_port
if alpn_proto in ['h3']: if alpn_proto in ["h3"]:
return self.h3_port return self.h3_port
return self.http_port return self.http_port
def authority_for(self, domain: str, alpn_proto: Optional[str] = None): 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, def make_data_file(
line_length: int = 1024) -> str: self, indir: str, fname: str, fsize: int, line_length: int = 1024
) -> str:
if line_length < 11: 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) fpath = os.path.join(indir, fname)
s10 = "0123456789" s10 = "0123456789"
s = round((line_length / 10) + 1) * s10 s = round((line_length / 10) + 1) * s10
s = s[0:line_length-11] s = s[0 : line_length - 11]
with open(fpath, 'w') as fd: with open(fpath, "w") as fd:
for i in range(int(fsize / line_length)): for i in range(int(fsize / line_length)):
fd.write(f"{i:09d}-{s}\n") fd.write(f"{i:09d}-{s}\n")
remain = int(fsize % line_length) remain = int(fsize % line_length)
if remain != 0: if remain != 0:
i = int(fsize / line_length) + 1 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 return fpath
def make_data_gzipbomb(self, indir: str, fname: str, fsize: int) -> str: def make_data_gzipbomb(self, indir: str, fname: str, fsize: int) -> str:
fpath = os.path.join(indir, fname) fpath = os.path.join(indir, fname)
gzpath = f'{fpath}.gz' gzpath = f"{fpath}.gz"
varpath = f'{fpath}.var' varpath = f"{fpath}.var"
with open(fpath, 'w') as fd: with open(fpath, "w") as fd:
fd.write('not what we are looking for!\n') fd.write("not what we are looking for!\n")
count = int(fsize / 1024) count = int(fsize / 1024)
zero1k = bytearray(1024) zero1k = bytearray(1024)
with gzip.open(gzpath, 'wb') as fd: with gzip.open(gzpath, "wb") as fd:
for _ in range(count): for _ in range(count):
fd.write(zero1k) fd.write(zero1k)
with open(varpath, 'w') as fd: with open(varpath, "w") as fd:
fd.write(f'URI: {fname}\n') fd.write(f"URI: {fname}\n")
fd.write('\n') fd.write("\n")
fd.write(f'URI: {fname}.gz\n') fd.write(f"URI: {fname}.gz\n")
fd.write('Content-Type: text/plain\n') fd.write("Content-Type: text/plain\n")
fd.write('Content-Encoding: x-gzip\n') fd.write("Content-Encoding: x-gzip\n")
fd.write('\n') fd.write("\n")
return fpath return fpath

428
tests/http/testenv/h2o.py Normal file
View file

@ -0,0 +1,428 @@
#!/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

View file

@ -47,4 +47,4 @@ TESTS_C = \
unit2600.c unit2601.c unit2602.c unit2603.c unit2604.c unit2605.c \ unit2600.c unit2601.c unit2602.c unit2603.c unit2604.c unit2605.c \
unit3200.c unit3205.c \ unit3200.c unit3205.c \
unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c unit3219.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

268
tests/unit/unit3400.c Normal file
View file

@ -0,0 +1,268 @@
/***************************************************************************
* _ _ ____ _
* 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
}