mirror of
https://github.com/curl/curl.git
synced 2026-07-24 13:57:16 +03:00
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:
parent
efc3f2309e
commit
e78b1b3ecc
66 changed files with 7401 additions and 473 deletions
3
.github/scripts/pyspelling.words
vendored
3
.github/scripts/pyspelling.words
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
67
configure.ac
67
configure.ac
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 \
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
31
docs/cmdline-opts/proxy-http3.md
Normal file
31
docs/cmdline-opts/proxy-http3.md
Normal 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`.
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
281
lib/capsule.c
Normal 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
77
lib/capsule.h
Normal 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
253
lib/cf-capsule.c
Normal 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
40
lib/cf-capsule.h
Normal 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 */
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
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
42
lib/cf-h3-proxy.h
Normal 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 */
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
151
lib/connect.c
151
lib/connect.c
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
17
lib/http.c
17
lib/http.c
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
391
lib/http_proxy.c
391
lib/http_proxy.c
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
56
lib/peer.c
56
lib/peer.c
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
27
lib/url.c
27
lib/url.c
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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
19
tests/data/test3400
Normal 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>
|
||||||
|
|
@ -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 "")
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,6 @@ danted = @DANTED@
|
||||||
[sshd]
|
[sshd]
|
||||||
sshd = @SSHD@
|
sshd = @SSHD@
|
||||||
sftpd = @SFTPD@
|
sftpd = @SFTPD@
|
||||||
|
|
||||||
|
[h2o]
|
||||||
|
h2o = @H2O@
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
689
tests/http/test_60_h3_proxy.py
Normal file
689
tests/http/test_60_h3_proxy.py
Normal 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)
|
||||||
|
|
@ -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}/',
|
||||||
|
|
|
||||||
|
|
@ -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
428
tests/http/testenv/h2o.py
Normal 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
|
||||||
|
|
@ -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
268
tests/unit/unit3400.c
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue