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

This patch adds two major capabilities to cURL's proxy infrastructure:

(1) HTTP/3 Proxy CONNECT (--proxy-http3): Allows tunneling HTTP/1.1 or
    HTTP/2 traffic through an HTTPS proxy speaking HTTP/3 (QUIC), using
    the standard CONNECT method over an HTTP/3 connection.

(2) MASQUE CONNECT-UDP (--proxyudptunnel): Implements RFC 9297 (HTTP
    Datagrams and the Capsule Protocol) and RFC 9298 (Proxying UDP in
    HTTP) to tunnel QUIC/HTTP/3 traffic through an HTTP proxy using the
    extended CONNECT method with the connect-udp protocol. The proxy
    may itself speak HTTP/1.1, HTTP/2, or HTTP/3.

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).

Public API additions:
- CURLPROXY_HTTPS3: new proxy type constant for HTTP/3 proxy
- CURLOPT_HTTPPROXYUDPTUNNEL: new option to enable CONNECT-UDP tunneling
- --proxy-http3: new CLI flag to negotiate HTTP/3 with an HTTPS proxy
- --proxyudptunnel: new CLI flag to request a UDP tunnel via CONNECT-UDP

Tests:
- tests/unit/unit3220.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

Signed-off-by: Aritra Basu <aritrbas@cisco.com>
This commit is contained in:
Aritra Basu 2026-03-23 01:59:34 -04:00
parent e0dd6eb4a4
commit d190a8ed24
No known key found for this signature in database
GPG key ID: 4191C420B170DC51
69 changed files with 7289 additions and 393 deletions

View file

@ -134,6 +134,7 @@ Config
config
conncache
connectdata
connectionless
CookieInfo
Coverity
CPUs
@ -167,6 +168,7 @@ CWE
cyassl
Cygwin
daniel
datagrams
datatracker
dbg
Debian

View file

@ -1118,6 +1118,8 @@ if(USE_SSLS_EXPORT)
endif()
endif()
option(USE_PROXY_HTTP3 "Enable experimental HTTP/3 proxy support" OFF)
option(USE_NGHTTP2 "Use nghttp2 library" ON)
if(USE_NGHTTP2)
find_package(NGHTTP2 MODULE)
@ -1186,6 +1188,20 @@ if(USE_QUICHE)
endif()
endif()
if(USE_PROXY_HTTP3)
if(CURL_DISABLE_PROXY)
message(FATAL_ERROR "USE_PROXY_HTTP3 requires proxy support")
elseif(CURL_DISABLE_HTTP)
message(FATAL_ERROR "USE_PROXY_HTTP3 requires HTTP support")
elseif(NOT USE_NGTCP2 OR NOT USE_NGHTTP3)
message(FATAL_ERROR "USE_PROXY_HTTP3 requires ngtcp2 + nghttp3")
elseif(NOT USE_OPENSSL)
message(FATAL_ERROR "USE_PROXY_HTTP3 currently requires OpenSSL")
else()
message(STATUS "HTTP/3 proxy support enabled (experimental)")
endif()
endif()
if(NOT CURL_DISABLE_SRP AND (HAVE_GNUTLS_SRP OR HAVE_OPENSSL_SRP))
set(USE_TLS_SRP 1)
endif()
@ -2044,6 +2060,7 @@ curl_add_if("NTLM" CURL_ENABLE_NTLM AND
curl_add_if("TLS-SRP" USE_TLS_SRP)
curl_add_if("HTTP2" USE_NGHTTP2)
curl_add_if("HTTP3" USE_NGTCP2 OR USE_QUICHE)
curl_add_if("PROXY-HTTP3" USE_PROXY_HTTP3)
curl_add_if("MultiSSL" CURL_WITH_MULTI_SSL)
curl_add_if("HTTPS-proxy" NOT CURL_DISABLE_PROXY AND _ssl_enabled AND (USE_OPENSSL OR USE_GNUTLS
OR USE_SCHANNEL OR USE_RUSTLS OR USE_MBEDTLS OR

View file

@ -54,6 +54,30 @@ CURL_CHECK_OPTION_RT
CURL_CHECK_OPTION_HTTPSRR
CURL_CHECK_OPTION_ECH
CURL_CHECK_OPTION_SSLS_EXPORT
AC_MSG_CHECKING([whether to enable HTTP/3 proxy support])
OPT_PROXY_HTTP3="default"
AC_ARG_ENABLE(proxy-http3,
AS_HELP_STRING([--enable-proxy-http3],[Enable experimental HTTP/3 proxy support])
AS_HELP_STRING([--disable-proxy-http3],[Disable experimental HTTP/3 proxy support]),
OPT_PROXY_HTTP3=$enableval)
case "$OPT_PROXY_HTTP3" in
no)
want_proxy_http3="no"
curl_proxy_http3_msg="no (--enable-proxy-http3)"
AC_MSG_RESULT([no])
;;
default)
want_proxy_http3="no"
curl_proxy_http3_msg="no (--enable-proxy-http3)"
AC_MSG_RESULT([no])
;;
*)
want_proxy_http3="yes"
curl_proxy_http3_msg="enabled (--disable-proxy-http3)"
AC_MSG_RESULT([yes])
;;
esac
USE_PROXY_HTTP3=0
XC_CHECK_PATH_SEPARATOR
@ -318,6 +342,22 @@ AS_HELP_STRING([--with-test-caddy=PATH],[where to find caddy for testing]),
)
AC_SUBST(CADDY)
if test -x /usr/local/bin/h2o; then
H2O=/usr/local/bin/h2o
elif test -x /usr/bin/h2o; then
H2O=/usr/bin/h2o
elif test -x "`brew --prefix 2>/dev/null`/bin/h2o"; then
H2O=`brew --prefix`/bin/h2o
fi
AC_ARG_WITH(test-h2o,dnl
AS_HELP_STRING([--with-test-h2o=PATH],[where to find h2o for testing]),
H2O=$withval
if test "x$H2O" = "xno"; then
H2O=""
fi
)
AC_SUBST(H2O)
if test -x /usr/sbin/vsftpd; then
VSFTPD=/usr/sbin/vsftpd
elif test -x /usr/local/sbin/vsftpd; then
@ -5018,6 +5058,28 @@ if test "$want_ssls_export" != "no"; then
fi
fi
dnl *************************************************************
dnl check whether experimental HTTP/3 proxy support is enabled
dnl
if test "$want_proxy_http3" = "yes"; then
AC_MSG_CHECKING([whether HTTP/3 proxy support is available])
if test "$CURL_DISABLE_PROXY" = "1"; then
AC_MSG_ERROR([--enable-proxy-http3 requires proxy support])
elif test "$CURL_DISABLE_HTTP" = "1"; then
AC_MSG_ERROR([--enable-proxy-http3 requires HTTP support])
elif test "$USE_NGTCP2_H3" != "1"; then
AC_MSG_ERROR([--enable-proxy-http3 requires ngtcp2 + nghttp3])
elif test "x$OPENSSL_ENABLED" != "x1"; then
AC_MSG_ERROR([--enable-proxy-http3 currently requires OpenSSL])
else
AC_DEFINE(USE_PROXY_HTTP3, 1, [if HTTP/3 proxy support is available])
USE_PROXY_HTTP3=1
AC_MSG_RESULT([yes])
experimental="$experimental PROXY-HTTP3"
fi
fi
dnl ************************************************************
dnl hiding of library internal symbols
dnl
@ -5131,6 +5193,10 @@ if test "$curl_psl_msg" = "enabled"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES PSL"
fi
if test "$USE_PROXY_HTTP3" = "1"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES PROXY-HTTP3"
fi
if test "$curl_gsasl_msg" = "enabled"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES gsasl"
fi
@ -5475,6 +5541,7 @@ AC_MSG_NOTICE([Configured to build curl/libcurl:
HTTP1: ${curl_h1_msg}
HTTP2: ${curl_h2_msg}
HTTP3: ${curl_h3_msg}
Proxy-HTTP3: ${curl_proxy_http3_msg}
ECH: ${curl_ech_msg}
HTTPS RR: ${curl_httpsrr_msg}
SSLS-EXPORT: ${curl_ssls_export_msg}

View file

@ -43,6 +43,16 @@ Graduation requirements:
- Using HTTP/3 with the given build should perform without risking busy-loops
### HTTP/3 proxy and CONNECT-UDP support
Support for HTTP/3 proxy and CONNECT-UDP tunneling is experimental and
requires an explicit build-time opt-in (`--enable-proxy-http3` for
autotools, `-DUSE_PROXY_HTTP3=ON` for CMake).
Graduation requirements:
- implementation stability over time with no known severe regressions
### The Rustls backend
Graduation requirements:

View file

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

View file

@ -212,6 +212,7 @@ DPAGES = \
proxy-digest.md \
proxy-header.md \
proxy-http2.md \
proxy-http3.md \
proxy-insecure.md \
proxy-key-type.md \
proxy-key.md \
@ -231,6 +232,7 @@ DPAGES = \
proxy.md \
proxy1.0.md \
proxytunnel.md \
proxyudptunnel.md \
pubkey.md \
quote.md \
random-file.md \

View file

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

View file

@ -0,0 +1,31 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: proxy-http3
Tags: Versions HTTP/3
Protocols: HTTP
Added: 8.20.0
Mutexed: proxy-http2
Requires: HTTP/3
Help: Use HTTP/3 with HTTPS proxy
Category: http proxy
Multi: boolean
See-also:
- proxy
- proxy-http2
Example:
- --proxy-http3 -x proxy $URL
---
# `--proxy-http3`
Negotiate HTTP/3 with an HTTPS proxy.
Fails to perform the transfer if the given proxy does not support HTTP/3.
This has no effect for any other kinds of proxies.
This option is mutually exclusive with `--proxy-http2`.
This feature is experimental and requires a build with HTTP/3 proxy support
enabled. For autotools builds, use `--enable-proxy-http3`. For CMake builds,
use `-DUSE_PROXY_HTTP3=ON`.

View file

@ -6,6 +6,7 @@ Short: p
Help: HTTP proxy tunnel (using CONNECT)
Category: proxy
Added: 7.3
Mutexed: proxyudptunnel
Multi: boolean
See-also:
- proxy
@ -22,3 +23,5 @@ number curl wants to tunnel through to.
To suppress proxy CONNECT response headers when curl is set to output headers
use --suppress-connect-headers.
This option is mutually exclusive with `--proxyudptunnel`.

View file

@ -0,0 +1,31 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: proxyudptunnel
Help: HTTP proxy tunnel (using CONNECT-UDP)
Category: proxy
Added: 8.20.0
Mutexed: proxytunnel
Requires: HTTP/3
Multi: boolean
See-also:
- proxy
- proxytunnel
Example:
- --proxyudptunnel -x http://proxy $URL
---
# `--proxyudptunnel`
When an HTTP proxy is used with --proxy, this option makes curl tunnel
the traffic through the proxy using the CONNECT-UDP method. The tunnel
is established by sending a CONNECT-UDP request to the proxy, asking it
to relay UDP traffic to the remote host and port. This requires that
the proxy supports the CONNECT-UDP method and allows access to the
requested destination.
This option is mutually exclusive with `--proxytunnel`.
This feature is experimental and requires a build with HTTP/3 proxy support
enabled. For autotools builds, use `--enable-proxy-http3`. For CMake builds,
use `-DUSE_PROXY_HTTP3=ON`.

View file

@ -156,9 +156,9 @@ The currently existing filter types (curl 8.5.0) are:
`accept()`ed in a `listen()`
* `SSL`: filter that applies TLS en-/decryption and handshake. Manages the
underlying TLS backend implementation.
* `HTTP-PROXY`, `H1-PROXY`, `H2-PROXY`: the first manages the connection to an
HTTP proxy server and uses the other depending on which ALPN protocol has
been negotiated.
* `HTTP-PROXY`, `H1-PROXY`, `H2-PROXY`, `H3-PROXY`: the first manages the
connection to an HTTP proxy server and uses the other depending on which
ALPN protocol has been negotiated.
* `SOCKS-PROXY`: filter for the various SOCKS proxy protocol variations
* `HAPROXY`: filter for the protocol of the same name, providing client IP
information to a server.
@ -166,7 +166,7 @@ The currently existing filter types (curl 8.5.0) are:
connection
* `HTTP/3`: filter for handling multiplexed transfers over an HTTP/3+QUIC
connection
* `HAPPY-EYEBALLS`: meta filter that implements IPv4/IPv6 "happy eyeballing".
* `HAPPY-EYEBALLS`: meta filter that implements IPv4/IPv6 "happy eyeballs".
It creates up to 2 sub-filters that race each other for a connection.
* `SETUP`: meta filter that manages the creation of sub-filter chains for a
specific transport (e.g. TCP or QUIC).
@ -220,6 +220,36 @@ as an `SSL` flagged filter is seen first. `conn3` is also encrypted as the
Similar checks can determine if a connection is multiplexed or not.
## Adding CONNECT-UDP support
HTTP/3 on top of HTTP/1.1:
```
conn --> HTTP/3 --> HTTP-PROXY --> H1-PROXY --> SSL --> HAPPY-EYEBALLS --> TCP
```
HTTP/3 on top of HTTP/2:
```
conn --> HTTP/3 --> HTTP-PROXY --> H2-PROXY --> SSL --> HAPPY-EYEBALLS --> TCP
```
## Adding H3-PROXY support
HTTP/1.1 on top of HTTP/3:
```
conn --> HTTP/1.1 --> SSL --> HTTP-PROXY --> H3-PROXY --> UDP
```
HTTP/2 on top of HTTP/3:
```
conn --> HTTP/2 --> SSL --> HTTP-PROXY --> H3-PROXY --> UDP
```
HTTP/3 on top of HTTP/3:
```
conn --> HTTP/3 --> HTTP-PROXY --> H3-PROXY --> UDP
```
NOTE:
This (H3-PROXY) does not have HAPPY-EYEBALLS support
## Filter Tracing
Filters may make use of special trace macros like `CURL_TRC_CF(data, cf, msg,

View file

@ -471,6 +471,10 @@ See CURLOPT_HTTPPOST(3)
Tunnel through the HTTP proxy. CURLOPT_HTTPPROXYTUNNEL(3)
## CURLOPT_HTTPPROXYUDPTUNNEL
UDP Tunnel through the HTTP proxy. CURLOPT_HTTPPROXYUDPTUNNEL(3)
## CURLOPT_HTTP_CONTENT_DECODING
Disable Content decoding. See CURLOPT_HTTP_CONTENT_DECODING(3)

View file

@ -298,6 +298,13 @@ supports HTTP NTLM
libcurl was built with support for NTLM delegation to a winbind helper. This
feature was removed from curl in 8.8.0.
## `PROXY-HTTP3`
*features* mask bit: non-existent
libcurl was built with EXPERIMENTAL support for HTTP/3 proxy tunneling
(Added in 8.20.0)
## `PSL`
*features* mask bit: CURL_VERSION_PSL

View file

@ -0,0 +1,90 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Title: CURLOPT_HTTPPROXYUDPTUNNEL
Section: 3
Source: libcurl
Protocol:
- All
See-also:
- CURLOPT_PROXY (3)
- CURLOPT_PROXYPORT (3)
- CURLOPT_PROXYTYPE (3)
- CURLOPT_HTTPPROXYTUNNEL (3)
- CURLOPT_HTTP_VERSION (3)
Added-in: 8.20.0
---
# NAME
CURLOPT_HTTPPROXYUDPTUNNEL - tunnel through HTTP proxy using CONNECT-UDP
# SYNOPSIS
~~~c
#include <curl/curl.h>
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HTTPPROXYUDPTUNNEL, long udptunnel);
~~~
# DESCRIPTION
This feature is experimental and requires a build with HTTP/3 proxy support
enabled.
Set the **udptunnel** parameter to 1L to make libcurl tunnel operations
through an HTTP proxy (set with CURLOPT_PROXY(3)) using CONNECT-UDP.
UDP tunneling means that a CONNECT-UDP request is sent to the proxy,
asking it to establish a UDP relay to a remote host on a specific port
number. Once the tunnel is established, UDP datagrams are encapsulated
and forwarded through the proxy, allowing end-to-end communication with
the target server. Proxies may restrict which destinations or ports are
allowed for CONNECT-UDP.
Unlike traditional HTTP CONNECT tunneling, which is stream-oriented and
used for TCP, CONNECT-UDP supports connectionless protocols such as
QUIC, HTTP/3, or other UDP-based traffic.
When not using UDP tunneling, libcurl cannot use UDP-based protocols
through an HTTP proxy, as HTTP proxies do not support forwarding UDP
traffic. Enabling CONNECT-UDP makes this possible by relaying UDP
datagrams through the proxy.
CONNECT-UDP typically requires an HTTP/3-capable proxy and appropriate
support on both the client and proxy side.
This option is intentionally explicit. libcurl does not automatically
infer CONNECT-UDP from HTTP/3 settings because origin HTTP version
and proxy tunnel type are configured independently.
# DEFAULT
0
# %PROTOCOLS%
# EXAMPLE
~~~c
int main(void)
{
CURL *curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/");
curl_easy_setopt(curl, CURLOPT_PROXY, "https://proxy.example.com");
curl_easy_setopt(curl, CURLOPT_HTTPPROXYUDPTUNNEL, 1L);
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3ONLY);
curl_easy_perform(curl);
}
}
~~~
# %AVAILABILITY%
# RETURN VALUE
curl_easy_setopt(3) returns a CURLcode indicating success or error.
CURLE_OK (0) means everything was OK, non-zero means an error occurred, see
libcurl-errors(3).

View file

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

View file

@ -41,6 +41,12 @@ HTTPS Proxy using HTTP/1. (Added in 7.52.0 for OpenSSL and GnuTLS. Since
HTTPS Proxy and attempt to speak HTTP/2 over it. (Added in 8.1.0)
## CURLPROXY_HTTPS3
HTTPS Proxy and attempt to speak HTTP/3 over it. (Added in 8.20.0)
This feature is experimental and requires a build with HTTP/3 proxy support
enabled.
## CURLPROXY_HTTP_1_0
HTTP 1.0 Proxy. This is similar to CURLPROXY_HTTP except it uses HTTP/1.0 for

View file

@ -227,6 +227,7 @@ man_MANS = \
CURLOPT_HTTPHEADER.3 \
CURLOPT_HTTPPOST.3 \
CURLOPT_HTTPPROXYTUNNEL.3 \
CURLOPT_HTTPPROXYUDPTUNNEL.3 \
CURLOPT_IGNORE_CONTENT_LENGTH.3 \
CURLOPT_INFILESIZE.3 \
CURLOPT_INFILESIZE_LARGE.3 \

View file

@ -696,6 +696,7 @@ CURLOPT_HTTPGET 7.8.1
CURLOPT_HTTPHEADER 7.1
CURLOPT_HTTPPOST 7.1 7.56.0
CURLOPT_HTTPPROXYTUNNEL 7.3
CURLOPT_HTTPPROXYUDPTUNNEL 8.20.0
CURLOPT_HTTPREQUEST 7.1 - 7.15.5
CURLOPT_IGNORE_CONTENT_LENGTH 7.14.1
CURLOPT_INFILE 7.1 7.9.7
@ -993,6 +994,7 @@ CURLPROXY_HTTP 7.10
CURLPROXY_HTTP_1_0 7.19.4
CURLPROXY_HTTPS 7.52.0
CURLPROXY_HTTPS2 8.1.0
CURLPROXY_HTTPS3 8.20.0
CURLPROXY_SOCKS4 7.10
CURLPROXY_SOCKS4A 7.18.0
CURLPROXY_SOCKS5 7.10

View file

@ -177,6 +177,7 @@
--proxy-digest 7.12.0
--proxy-header 7.37.0
--proxy-http2 8.1.0
--proxy-http3 8.20.0
--proxy-insecure 7.52.0
--proxy-key 7.52.0
--proxy-key-type 7.52.0
@ -195,6 +196,7 @@
--proxy-user (-U) 4.0
--proxy1.0 7.19.4
--proxytunnel (-p) 7.3
--proxyudptunnel 8.20.0
--pubkey 7.16.2
--quote (-Q) 5.3
--random-file 7.7

View file

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

View file

@ -802,9 +802,11 @@ typedef CURLcode (*curl_ssl_ctx_callback)(CURL *curl, /* easy handle */
#define CURLPROXY_SOCKS5_HOSTNAME 7L /* Use the SOCKS5 protocol but pass along
the hostname rather than the IP
address. added in 7.18.0 */
#define CURLPROXY_HTTPS3 8L /* HTTPS and attempt HTTP/3
added in 8.20.0 */
typedef enum {
CURLPROXY_LAST = 8 /* never use */
CURLPROXY_LAST = 9 /* never use */
} curl_proxytype; /* this enum was added in 7.10 */
/*
@ -1494,8 +1496,8 @@ typedef enum {
CURLOPT(CURLOPT_SHARE, CURLOPTTYPE_OBJECTPOINT, 100),
/* indicates type of proxy. accepted values are CURLPROXY_HTTP (default),
CURLPROXY_HTTPS, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A and
CURLPROXY_SOCKS5. */
CURLPROXY_HTTPS, CURLPROXY_HTTPS2, CURLPROXY_HTTPS3, CURLPROXY_SOCKS4,
CURLPROXY_SOCKS4A and CURLPROXY_SOCKS5. */
CURLOPT(CURLOPT_PROXYTYPE, CURLOPTTYPE_VALUES, 101),
/* Set the Accept-Encoding string. Use this to tell a server you would like
@ -2258,6 +2260,9 @@ typedef enum {
/* set TLS supported signature algorithms */
CURLOPT(CURLOPT_SSL_SIGNATURE_ALGORITHMS, CURLOPTTYPE_STRINGPOINT, 328),
/* tunnel non-http operations through an HTTP proxy using UDP tunnel */
CURLOPT(CURLOPT_HTTPPROXYUDPTUNNEL, CURLOPTTYPE_LONG, 329),
CURLOPT_LASTENTRY /* the last unused */
} CURLoption;

View file

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

View file

@ -280,6 +280,27 @@ bool Curl_bufq_is_full(const struct bufq *q)
return chunk_is_full(q->tail);
}
size_t Curl_bufq_space(const struct bufq *q)
{
size_t space = 0;
struct buf_chunk *chunk;
size_t spare_count = 0;
if(q->opts & BUFQ_OPT_SOFT_LIMIT)
return SIZE_MAX;
if(q->tail)
space += (q->tail->dlen - q->tail->w_offset);
for(chunk = q->spare; chunk; chunk = chunk->next)
++spare_count;
space += spare_count * q->chunk_size;
if(q->chunk_count < q->max_chunks)
space += (q->max_chunks - q->chunk_count) * q->chunk_size;
return space;
}
static struct buf_chunk *get_spare(struct bufq *q)
{
struct buf_chunk *chunk = NULL;

View file

@ -157,6 +157,13 @@ bool Curl_bufq_is_empty(const struct bufq *q);
*/
bool Curl_bufq_is_full(const struct bufq *q);
/**
* Return the number of bytes that can still be written before the
* queue is full. For queues with BUFQ_OPT_SOFT_LIMIT this returns
* SIZE_MAX since writing is always allowed.
*/
size_t Curl_bufq_space(const struct bufq *q);
/**
* Write buf to the end of the buffer queue. The buf is copied
* and the amount of copied bytes is returned.

306
lib/capsule.c Normal file
View file

@ -0,0 +1,306 @@
/***************************************************************************
* _ _ ____ _
* 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
}
static size_t capsule_varint_len(uint64_t value)
{
if(value <= 0x3F)
return 1;
else if(value <= 0x3FFF)
return 2;
else if(value <= 0x3FFFFFFF)
return 4;
return 8;
}
/**
* 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;
}
}
#ifdef USE_NGTCP2
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;
}
#endif /* USE_NGTCP2 */
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_udp_payload_written(size_t payload_len,
size_t capsule_bytes)
{
uint64_t capsule_len;
size_t hdr_len = 2; /* capsule type + context ID */
#if SIZEOF_SIZE_T > 4
if(payload_len >= (size_t)UINT64_C(0x3FFFFFFFFFFFFFFF))
capsule_len = UINT64_C(0x3FFFFFFFFFFFFFFF);
else
#endif
capsule_len = (uint64_t)payload_len + 1;
hdr_len += capsule_varint_len(capsule_len);
if(capsule_bytes <= hdr_len)
return 0;
capsule_bytes -= hdr_len;
if(capsule_bytes > payload_len)
capsule_bytes = payload_len;
return capsule_bytes;
}
#ifdef USE_NGTCP2
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]);
*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);
*err = CURLE_RECV_ERROR;
return 0;
}
offset += 1;
if(!capsule_length) {
infof(data, "Error! Invalid capsule length: 0");
*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);
*err = CURLE_AGAIN;
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;
}
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
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */

87
lib/capsule.h Normal file
View file

@ -0,0 +1,87 @@
#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);
#ifdef USE_NGTCP2
/**
* 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
/**
* Map written capsule bytes back to written UDP payload bytes.
* `capsule_bytes` is the amount written from a buffer produced by
* `Curl_capsule_encap_udp_datagram()` with the same `payload_len`.
*/
size_t Curl_capsule_udp_payload_written(size_t payload_len,
size_t capsule_bytes);
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#endif /* HEADER_CURL_CAPSULE_H */

View file

@ -585,6 +585,32 @@ const struct Curl_addrinfo *Curl_conn_dns_get_ai(struct Curl_easy *data,
return Curl_cf_dns_get_ai(conn->cfilter[sockindex], data, ai_family, index);
}
const struct Curl_addrinfo *
Curl_conn_dns_get_ip_addr(struct Curl_easy *data,
int sockindex,
uint8_t ip_version)
{
const struct Curl_addrinfo *addr = NULL;
if(ip_version == CURL_IPRESOLVE_V4) {
addr = Curl_conn_dns_get_ai(data, sockindex, AF_INET, 0);
}
else if(ip_version == CURL_IPRESOLVE_V6) {
#ifdef USE_IPV6
addr = Curl_conn_dns_get_ai(data, sockindex, AF_INET6, 0);
#endif
}
else {
#ifdef USE_IPV6
addr = Curl_conn_dns_get_ai(data, sockindex, AF_INET6, 0);
if(!addr)
#endif
addr = Curl_conn_dns_get_ai(data, sockindex, AF_INET, 0);
}
return addr;
}
#ifdef USE_HTTPSRR
/* Return the HTTPS-RR info from the first "resolve" filter at the
* connection. If the DNS resolving is not done yet or if there

View file

@ -56,10 +56,17 @@ const struct Curl_addrinfo *Curl_conn_dns_get_ai(struct Curl_easy *data,
int ai_family,
unsigned int index);
const struct Curl_addrinfo *Curl_cf_dns_get_ai(struct Curl_cfilter *cf,
struct Curl_easy *data,
int ai_family,
unsigned int index);
/* Get the preferred IP address for sockindex, honoring ip_version */
const struct Curl_addrinfo *
Curl_conn_dns_get_ip_addr(struct Curl_easy *data,
int sockindex,
uint8_t ip_version);
const struct Curl_addrinfo *
Curl_cf_dns_get_ai(struct Curl_cfilter *cf,
struct Curl_easy *data,
int ai_family,
unsigned int index);
#ifdef USE_HTTPSRR
const struct Curl_https_rrinfo *Curl_conn_dns_get_https(struct Curl_easy *data,

View file

@ -25,6 +25,8 @@
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include <curl/curl.h>
#include "urldata.h"
#include "curlx/dynbuf.h"
#include "sendf.h"
@ -33,13 +35,19 @@
#include "http_proxy.h"
#include "select.h"
#include "progress.h"
#include "multiif.h"
#include "cfilters.h"
#include "cf-h1-proxy.h"
#include "connect.h"
#include "curl_trc.h"
#include "strcase.h"
#include "curlx/strparse.h"
#include "capsule.h"
#define PROXY_H1_CHUNK_SIZE (16*1024)
#define H1_TUNNEL_WINDOW_SIZE (10 * 1024 * 1024)
#define PROXY_H1_NW_RECV_CHUNKS (H1_TUNNEL_WINDOW_SIZE / PROXY_H1_CHUNK_SIZE)
#define PROXY_H1_NW_SEND_CHUNKS (H1_TUNNEL_WINDOW_SIZE / PROXY_H1_CHUNK_SIZE)
typedef enum {
H1_TUNNEL_INIT, /* init/default/no tunnel state */
@ -54,6 +62,8 @@ typedef enum {
struct h1_tunnel_state {
struct dynbuf rcvbuf;
struct dynbuf request_data;
struct bufq sendbuf; /* UDP capsule send buffer */
struct bufq recvbuf; /* UDP capsule receive buffer */
size_t nsent;
size_t headerlines;
struct Curl_chunker ch;
@ -89,6 +99,8 @@ static CURLcode tunnel_reinit(struct Curl_cfilter *cf,
DEBUGASSERT(ts);
curlx_dyn_reset(&ts->rcvbuf);
curlx_dyn_reset(&ts->request_data);
Curl_bufq_reset(&ts->sendbuf);
Curl_bufq_reset(&ts->recvbuf);
ts->tunnel_state = H1_TUNNEL_INIT;
ts->keepon = KEEPON_CONNECT;
ts->cl = 0;
@ -117,6 +129,10 @@ static CURLcode tunnel_init(struct Curl_cfilter *cf,
curlx_dyn_init(&ts->rcvbuf, DYN_PROXY_CONNECT_HEADERS);
curlx_dyn_init(&ts->request_data, DYN_HTTP_REQUEST);
Curl_bufq_init2(&ts->sendbuf, PROXY_H1_CHUNK_SIZE, PROXY_H1_NW_SEND_CHUNKS,
BUFQ_OPT_SOFT_LIMIT);
Curl_bufq_init2(&ts->recvbuf, PROXY_H1_CHUNK_SIZE, PROXY_H1_NW_RECV_CHUNKS,
BUFQ_OPT_SOFT_LIMIT);
Curl_httpchunk_init(data, &ts->ch, TRUE);
*pts = ts;
@ -156,7 +172,9 @@ static void h1_tunnel_go_state(struct Curl_cfilter *cf,
case H1_TUNNEL_ESTABLISHED:
CURL_TRC_CF(data, cf, "new tunnel state 'established'");
infof(data, "CONNECT phase completed");
infof(data, "CONNECT%s phase completed for HTTP proxy",
cf->conn->bits.udp_tunnel_proxy ? "-UDP" : "");
data->state.authproxy.done = TRUE;
data->state.authproxy.multipass = FALSE;
FALLTHROUGH();
@ -186,6 +204,8 @@ static void tunnel_free(struct Curl_cfilter *cf,
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
curlx_dyn_free(&ts->rcvbuf);
curlx_dyn_free(&ts->request_data);
Curl_bufq_free(&ts->sendbuf);
Curl_bufq_free(&ts->recvbuf);
Curl_httpchunk_free(data, &ts->ch);
curlx_free(ts);
cf->ctx = NULL;
@ -210,11 +230,20 @@ static CURLcode start_CONNECT(struct Curl_cfilter *cf,
and we do not really use the newly cloned URL here then. Free it. */
curlx_safefree(data->req.newurl);
result = Curl_http_proxy_create_CONNECT(&req, cf, data, 1);
if(cf->conn->bits.udp_tunnel_proxy) {
result = Curl_http_proxy_create_CONNECTUDP(&req, cf, data, 1);
}
else {
result = Curl_http_proxy_create_CONNECT(&req, cf, data, 1);
}
if(result)
goto out;
infof(data, "Establish HTTP proxy tunnel to %s", req->authority);
if(cf->conn->bits.udp_tunnel_proxy)
infof(data, "Establishing HTTP proxy UDP tunnel to %s:%s",
data->state.up.hostname, data->state.up.port);
else
infof(data, "Establishing HTTP proxy tunnel to %s", req->authority);
curlx_dyn_reset(&ts->request_data);
ts->nsent = 0;
@ -268,6 +297,52 @@ out:
return result;
}
static CURLcode on_resp_header_udp(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h1_tunnel_state *ts,
const char *header)
{
CURLcode result = CURLE_OK;
struct SingleRequest *k = &data->req;
if(checkprefix("Transfer-Encoding:", header)) {
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(!strncmp(header, "HTTP/1.", 7) &&
((header[7] == '0') || (header[7] == '1')) &&
(header[8] == ' ') &&
ISDIGIT(header[9]) && ISDIGIT(header[10]) && ISDIGIT(header[11]) &&
!ISDIGIT(header[12])) {
/* store the HTTP code from the proxy */
data->info.httpproxycode = k->httpcode =
((header[9] - '0') * 100) +
((header[10] - '0') * 10) +
(header[11] - '0');
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> %d", k->httpcode);
}
return result;
}
static CURLcode on_resp_header(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h1_tunnel_state *ts,
@ -406,7 +481,13 @@ static CURLcode single_header(struct Curl_cfilter *cf,
return result;
}
result = on_resp_header(cf, data, ts, linep);
if(cf->conn->bits.udp_tunnel_proxy) {
result = on_resp_header_udp(cf, data, ts, linep);
}
else {
result = on_resp_header(cf, data, ts, linep);
}
if(result)
return result;
@ -448,6 +529,13 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
}
if(!nread) {
if(ts->maybe_folded) {
/* EOF right after LF: finalize the pending header line. */
result = single_header(cf, data, ts);
if(result)
return result;
ts->maybe_folded = FALSE;
}
if(data->set.proxyauth && data->state.authproxy.avail &&
data->req.proxyuserpwd) {
/* proxy auth was requested and there was proxy auth available,
@ -539,6 +627,8 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
ts->maybe_folded = TRUE;
}
if(result)
return result;
} /* while there is buffer left and loop is requested */
if(error)
@ -641,17 +731,36 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf,
} while(data->req.newurl);
DEBUGASSERT(ts->tunnel_state == H1_TUNNEL_RESPONSE);
if(data->info.httpproxycode / 100 != 2) {
/* a non-2xx response and we have no next URL to try. */
curlx_safefree(data->req.newurl);
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode);
return CURLE_COULDNT_CONNECT;
if(cf->conn->bits.udp_tunnel_proxy) {
/* MASQUE FIX: envoy and h2o has different behaviour */
/* envoy returns 200 OK, h2o returns 101 Switching Protocols */
if(data->info.httpproxycode != 200 && data->info.httpproxycode != 101) {
/* 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-UDP tunnel failed, response %d",
data->req.httpcode);
return CURLE_COULDNT_CONNECT;
}
}
else {
if(data->info.httpproxycode / 100 != 2) {
/* a non-2xx response and we have no next URL to try. */
curlx_safefree(data->req.newurl);
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode);
return CURLE_COULDNT_CONNECT;
}
}
/* 2xx response, SUCCESS! */
/* 101 Switching Protocol for CONNECT-UDP */
h1_tunnel_go_state(cf, ts, H1_TUNNEL_ESTABLISHED, data);
infof(data, "CONNECT tunnel established, response %d",
data->info.httpproxycode);
if(cf->conn->bits.udp_tunnel_proxy)
infof(data, "CONNECT-UDP tunnel established, response %d",
data->info.httpproxycode);
else
infof(data, "CONNECT tunnel established, response %d",
data->info.httpproxycode);
result = CURLE_OK;
out:
@ -701,7 +810,10 @@ out:
Curl_client_reset(data);
Curl_pgrsReset(data);
tunnel_free(cf, data);
/* For UDP tunnel proxy, keep the tunnel state for ongoing operations */
if(!cf->conn->bits.udp_tunnel_proxy) {
tunnel_free(cf, data);
}
}
return result;
}
@ -730,9 +842,38 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf,
else
result = Curl_pollset_set_out_only(data, ps, sock);
}
else {
if(cf->next)
result = cf->next->cft->adjust_pollset(cf->next, data, ps);
/* Keep write interest while encapsulated datagrams remain buffered. */
if(!result && ts && cf->conn->bits.udp_tunnel_proxy &&
!Curl_bufq_is_empty(&ts->sendbuf)) {
curl_socket_t sock = Curl_conn_cf_get_socket(cf, data);
bool want_recv, want_send;
if(sock != CURL_SOCKET_BAD) {
Curl_pollset_check(data, ps, sock, &want_recv, &want_send);
if(!want_send)
result = Curl_pollset_set(data, ps, sock, want_recv, TRUE);
}
}
}
return result;
}
static bool cf_h1_proxy_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
struct h1_tunnel_state *ts = cf->ctx;
if(ts && cf->conn->bits.udp_tunnel_proxy &&
!Curl_bufq_is_empty(&ts->recvbuf))
return TRUE;
return cf->next ? cf->next->cft->has_data_pending(cf->next, data) : FALSE;
}
static void cf_h1_proxy_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
@ -748,12 +889,168 @@ static void cf_h1_proxy_close(struct Curl_cfilter *cf,
cf->connected = FALSE;
if(cf->ctx) {
h1_tunnel_go_state(cf, cf->ctx, H1_TUNNEL_INIT, data);
/* For UDP tunnel proxy, free the tunnel state on close */
if(cf->conn->bits.udp_tunnel_proxy) {
tunnel_free(cf, data);
}
}
if(cf->next)
cf->next->cft->do_close(cf->next, data);
}
}
static CURLcode
cf_h1_proxy_flush_sendbuf(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h1_tunnel_state *ts)
{
const unsigned char *buf;
size_t len, nwritten;
CURLcode result = CURLE_OK;
while(Curl_bufq_peek(&ts->sendbuf, &buf, &len)) {
nwritten = 0;
result = cf->next->cft->do_send(cf->next, data, (const uint8_t *)buf, len,
FALSE, &nwritten);
if(nwritten)
Curl_bufq_skip(&ts->sendbuf, nwritten);
if(result)
return result;
if(nwritten < len)
return CURLE_AGAIN;
}
return CURLE_OK;
}
static CURLcode
cf_h1_proxy_send(struct Curl_cfilter *cf, struct Curl_easy *data,
const uint8_t *buf, size_t len, bool eos, size_t *pnwritten)
{
CURLcode result = CURLE_SEND_ERROR;
*pnwritten = 0;
if(!cf->next)
return result;
if(data->conn->bits.udp_tunnel_proxy) {
struct h1_tunnel_state *ts = cf->ctx;
struct dynbuf dyn;
size_t nwritten = 0;
size_t capsule_len;
(void)eos;
if(!ts)
return result;
if(!Curl_bufq_is_empty(&ts->sendbuf)) {
result = cf_h1_proxy_flush_sendbuf(cf, data, ts);
if(result)
goto out;
}
result = Curl_capsule_encap_udp_datagram(&dyn, buf, len);
if(result)
goto out;
capsule_len = curlx_dyn_len(&dyn);
if(Curl_bufq_space(&ts->sendbuf) < capsule_len) {
curlx_dyn_free(&dyn);
result = CURLE_AGAIN;
goto out;
}
result = Curl_bufq_write(&ts->sendbuf,
(const unsigned char *)curlx_dyn_ptr(&dyn),
capsule_len, &nwritten);
curlx_dyn_free(&dyn);
if(result)
goto out;
if(nwritten < capsule_len) {
result = CURLE_AGAIN;
goto out;
}
/* A payload is only accepted when its whole capsule is queued. */
*pnwritten = len;
result = cf_h1_proxy_flush_sendbuf(cf, data, ts);
goto out;
}
else {
return cf->next->cft->do_send(cf->next, data, buf, len, eos, pnwritten);
}
out:
if(data->conn->bits.udp_tunnel_proxy) {
struct h1_tunnel_state *ts = cf->ctx;
if(ts && !Curl_bufq_is_empty(&ts->sendbuf) &&
(!result || (result == CURLE_AGAIN)))
Curl_multi_mark_dirty(data);
}
return result;
}
struct cf_h1_proxy_reader_ctx {
struct Curl_cfilter *cf;
struct Curl_easy *data;
};
static CURLcode proxy_nw_in_reader(void *reader_ctx,
unsigned char *buf, size_t buflen,
size_t *pnread)
{
struct cf_h1_proxy_reader_ctx *rctx = reader_ctx;
return rctx->cf->next->cft->do_recv(rctx->cf->next, rctx->data,
(char *)buf, buflen, pnread);
}
static CURLcode
cf_h1_proxy_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
char *buf, size_t len, size_t *pnread)
{
struct h1_tunnel_state *ts = cf->ctx;
CURLcode result = CURLE_RECV_ERROR;
*pnread = 0;
if(!cf->next)
return result;
if(data->conn->bits.udp_tunnel_proxy) {
/* For UDP tunnel proxy, we need to handle capsule processing */
if(!ts)
return CURLE_RECV_ERROR;
/* First, try to read more data from the network into our recvbuf */
if(!Curl_bufq_is_full(&ts->recvbuf)) {
struct cf_h1_proxy_reader_ctx rctx;
rctx.cf = cf;
rctx.data = data;
result = Curl_bufq_slurp(&ts->recvbuf, proxy_nw_in_reader,
&rctx, pnread);
if(result && result != CURLE_AGAIN) {
return result;
}
}
/* Decapsulate UDP payload from HTTP DATAGRAM capsules. */
#ifdef USE_NGTCP2
*pnread = Curl_capsule_process_udp_raw(cf, data, &ts->recvbuf,
(unsigned char *)buf, len, &result);
if(!result && !*pnread)
result = CURLE_AGAIN;
return result;
#else
infof(data, "UDP tunnel proxy not supported for HTTP/1.1");
return CURLE_UNSUPPORTED_PROTOCOL;
#endif
}
else {
return cf->next->cft->do_recv(cf->next, data, buf, len, pnread);
}
}
struct Curl_cftype Curl_cft_h1_proxy = {
"H1-PROXY",
CF_TYPE_IP_CONNECT | CF_TYPE_PROXY,
@ -763,9 +1060,9 @@ struct Curl_cftype Curl_cft_h1_proxy = {
cf_h1_proxy_close,
Curl_cf_def_shutdown,
cf_h1_proxy_adjust_pollset,
Curl_cf_def_data_pending,
Curl_cf_def_send,
Curl_cf_def_recv,
cf_h1_proxy_data_pending,
cf_h1_proxy_send,
cf_h1_proxy_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,

View file

@ -26,6 +26,7 @@
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY) && \
defined(USE_NGHTTP2)
#include <nghttp2/nghttp2.h>
#include "urldata.h"
@ -42,6 +43,7 @@
#include "sendf.h"
#include "select.h"
#include "cf-h2-proxy.h"
#include "capsule.h"
#define PROXY_H2_CHUNK_SIZE (16 * 1024)
@ -147,7 +149,8 @@ static void h2_tunnel_go_state(struct Curl_cfilter *cf,
case H2_TUNNEL_ESTABLISHED:
CURL_TRC_CF(data, cf, "[%d] new tunnel state 'established'",
ts->stream_id);
infof(data, "CONNECT phase completed");
infof(data, "CONNECT%s phase completed for HTTP/2 proxy",
cf->conn->bits.udp_tunnel_proxy ? "-UDP" : "");
data->state.authproxy.done = TRUE;
data->state.authproxy.multipass = FALSE;
FALLTHROUGH();
@ -213,7 +216,8 @@ static void drain_tunnel(struct Curl_cfilter *cf,
struct cf_h2_proxy_ctx *ctx = cf->ctx;
(void)cf;
if(!tunnel->closed && !tunnel->reset &&
!Curl_bufq_is_empty(&ctx->tunnel.sendbuf))
(!Curl_bufq_is_empty(&ctx->tunnel.sendbuf) ||
!Curl_bufq_is_empty(&ctx->tunnel.recvbuf)))
Curl_multi_mark_dirty(data);
}
@ -750,14 +754,23 @@ static CURLcode submit_CONNECT(struct Curl_cfilter *cf,
CURLcode result;
struct httpreq *req = NULL;
result = Curl_http_proxy_create_CONNECT(&req, cf, data, 2);
if(cf->conn->bits.udp_tunnel_proxy) {
result = Curl_http_proxy_create_CONNECTUDP(&req, cf, data, 2);
}
else {
result = Curl_http_proxy_create_CONNECT(&req, cf, data, 2);
}
if(result)
goto out;
result = Curl_creader_set_null(data);
if(result)
goto out;
infof(data, "Establish HTTP/2 proxy tunnel to %s", req->authority);
if(cf->conn->bits.udp_tunnel_proxy)
infof(data, "Establishing HTTP/2 proxy UDP tunnel to %s:%s",
data->state.up.hostname, data->state.up.port);
else
infof(data, "Establishing HTTP/2 proxy tunnel to %s", req->authority);
result = proxy_h2_submit(&ts->stream_id, cf, data, ctx->h2, req,
NULL, ts, tunnel_send_callback, cf);
@ -779,14 +792,60 @@ static CURLcode inspect_response(struct Curl_cfilter *cf,
struct tunnel_stream *ts)
{
CURLcode result = CURLE_OK;
struct dynhds_entry *capsule_protocol = NULL;
struct dynhds_entry *auth_reply = NULL;
size_t i, header_count;
(void)cf;
DEBUGASSERT(ts->resp);
if(ts->resp->status / 100 == 2) {
infof(data, "CONNECT tunnel established, response %d", ts->resp->status);
h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data);
return CURLE_OK;
/* Log all response headers */
header_count = Curl_dynhds_count(&ts->resp->headers);
if(cf->conn->bits.udp_tunnel_proxy)
infof(data, "CONNECT-UDP Response Status %d", ts->resp->status);
else
infof(data, "CONNECT Response Status %d", ts->resp->status);
infof(data, "Response Headers (%zu total):", header_count);
for(i = 0; i < header_count; i++) {
struct dynhds_entry *entry = Curl_dynhds_getn(&ts->resp->headers, i);
if(entry)
infof(data, " %s: %s", entry->name, entry->value);
}
if(cf->conn->bits.udp_tunnel_proxy) {
if(ts->resp->status == 200) {
capsule_protocol = Curl_dynhds_cget(&ts->resp->headers,
"capsule-protocol");
if(capsule_protocol) {
if(strncmp(capsule_protocol->value, "?1", 2) == 0) {
infof(data, "CONNECT-UDP tunnel established, response %d",
ts->resp->status);
h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data);
return CURLE_OK;
}
}
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", ts->resp->status);
h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data);
return CURLE_OK;
}
}
else {
failf(data, "Failed to establish CONNECT-UDP tunnel, "
"response %d", ts->resp->status);
h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data);
return CURLE_COULDNT_CONNECT;
}
}
else {
if(ts->resp->status / 100 == 2) {
infof(data, "CONNECT tunnel established, response %d",
ts->resp->status);
h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data);
return CURLE_OK;
}
}
if(ts->resp->status == 401) {
@ -1151,6 +1210,42 @@ static CURLcode cf_h2_proxy_adjust_pollset(struct Curl_cfilter *cf,
return result;
}
static ssize_t process_udp_capsule(struct Curl_cfilter *cf,
struct Curl_easy *data,
char *buf, size_t len, CURLcode *err)
{
struct cf_h2_proxy_ctx *ctx = cf->ctx;
#ifdef USE_NGTCP2
ssize_t nread;
size_t consumed_before, consumed_after, total_consumed;
/* Track buffer consumption to calculate bytes consumed */
consumed_before = Curl_bufq_len(&ctx->tunnel.recvbuf);
nread = (ssize_t)Curl_capsule_process_udp_raw(cf, data,
&ctx->tunnel.recvbuf,
(unsigned char *)buf,
len, err);
consumed_after = Curl_bufq_len(&ctx->tunnel.recvbuf);
total_consumed = consumed_before - consumed_after;
if(total_consumed > 0) {
/* Return consumed bytes as stream window credit. This also covers
* zero-payload capsules where nread is 0 but bytes were consumed. */
nghttp2_session_consume(ctx->h2, ctx->tunnel.stream_id, total_consumed);
}
return nread;
#else
(void)ctx;
(void)buf;
(void)len;
infof(data, "UDP tunnel proxy not supported for HTTP/2");
*err = CURLE_UNSUPPORTED_PROTOCOL;
return -1;
#endif
}
static CURLcode h2_handle_tunnel_close(struct Curl_cfilter *cf,
struct Curl_easy *data,
size_t *pnread)
@ -1205,6 +1300,7 @@ static CURLcode cf_h2_proxy_recv(struct Curl_cfilter *cf,
{
struct cf_h2_proxy_ctx *ctx = cf->ctx;
struct cf_call_data save;
ssize_t capsules_processed;
CURLcode result;
*pnread = 0;
@ -1221,18 +1317,43 @@ static CURLcode cf_h2_proxy_recv(struct Curl_cfilter *cf,
goto out;
}
result = tunnel_recv(cf, data, buf, len, pnread);
if(data->conn->bits.udp_tunnel_proxy) {
if(Curl_bufq_is_empty(&ctx->tunnel.recvbuf)) {
result = CURLE_AGAIN;
goto out;
}
if(!result) {
CURL_TRC_CF(data, cf, "[%d] increase window by %zu",
ctx->tunnel.stream_id, *pnread);
nghttp2_session_consume(ctx->h2, ctx->tunnel.stream_id, *pnread);
capsules_processed = process_udp_capsule(cf, data, buf, len, &result);
if(!result || result == CURLE_AGAIN) {
*pnread = (size_t)capsules_processed;
}
else {
if(ctx->tunnel.closed) {
result = h2_handle_tunnel_close(cf, data, pnread);
}
else if(ctx->tunnel.reset ||
(ctx->conn_closed && Curl_bufq_is_empty(&ctx->inbufq)) ||
(ctx->rcvd_goaway &&
ctx->last_stream_id < ctx->tunnel.stream_id)) {
result = CURLE_RECV_ERROR;
}
}
}
else {
result = tunnel_recv(cf, data, buf, len, pnread);
if(!result) {
CURL_TRC_CF(data, cf, "[%d] increase window by %zu",
ctx->tunnel.stream_id, *pnread);
nghttp2_session_consume(ctx->h2, ctx->tunnel.stream_id, *pnread);
}
}
result = Curl_1st_fatal(result, proxy_h2_progress_egress(cf, data));
out:
if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) &&
if((!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) ||
!Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) &&
(!result || (result == CURLE_AGAIN))) {
/* data pending and no fatal error to report. Need to trigger
* draining to avoid stalling when no socket events happen. */
@ -1268,7 +1389,32 @@ static CURLcode cf_h2_proxy_send(struct Curl_cfilter *cf,
goto out;
}
result = Curl_bufq_write(&ctx->tunnel.sendbuf, buf, len, pnwritten);
if(data->conn->bits.udp_tunnel_proxy) {
struct dynbuf dyn;
size_t capsule_len;
size_t capsule_written = 0;
result = Curl_capsule_encap_udp_datagram(&dyn, buf, len);
if(result)
goto out;
capsule_len = curlx_dyn_len(&dyn);
if(Curl_bufq_space(&ctx->tunnel.sendbuf) < capsule_len) {
curlx_dyn_free(&dyn);
result = CURLE_AGAIN;
goto out;
}
result = Curl_bufq_write(&ctx->tunnel.sendbuf,
(const unsigned char *)curlx_dyn_ptr(&dyn),
capsule_len, &capsule_written);
*pnwritten = Curl_capsule_udp_payload_written(len, capsule_written);
curlx_dyn_free(&dyn);
}
else {
result = Curl_bufq_write(&ctx->tunnel.sendbuf, buf, len, pnwritten);
}
CURL_TRC_CF(data, cf, "cf_send(), bufq_write %d, %zd", result, *pnwritten);
if(result && (result != CURLE_AGAIN))
goto out;
@ -1298,7 +1444,8 @@ static CURLcode cf_h2_proxy_send(struct Curl_cfilter *cf,
}
out:
if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) &&
if((!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) ||
!Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) &&
(!result || (result == CURLE_AGAIN))) {
/* data pending and no fatal error to report. Need to trigger
* draining to avoid stalling when no socket events happen. */

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

File diff suppressed because it is too large Load diff

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

@ -0,0 +1,40 @@
#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(USE_NGHTTP3) && !defined(CURL_DISABLE_PROXY) && \
defined(USE_NGTCP2) && defined(USE_OPENSSL)
CURLcode Curl_cf_h3_proxy_insert_after(struct Curl_cfilter **pcf,
struct Curl_easy *data);
extern struct Curl_cftype Curl_cft_h3_proxy;
#endif /* USE_NGHTTP3 && !CURL_DISABLE_PROXY && \
USE_NGTCP2 && USE_OPENSSL */
#endif /* HEADER_CURL_H3_PROXY_H */

View file

@ -63,6 +63,7 @@
#include "curlx/inet_ntop.h"
#include "curlx/strparse.h"
#include "vtls/vtls.h" /* for vtls cfilters */
#include "vquic/vquic.h" /* for QUIC cfilters */
#include "progress.h"
#include "conncache.h"
#include "multihandle.h"
@ -359,8 +360,28 @@ connect_sub_chain:
return result;
}
#ifndef CURL_DISABLE_PROXY
if(ctx->state == CF_SETUP_INIT &&
IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) {
/* When CURLPROXY_HTTPS3 is used, we want to skip the happy eyeballing
So, skipping to CF_SETUP_CNNCT_EYEBALLS state. The filter chain will
be like this: Curl_cft_http_connect --> Curl_cft_setup --> <HTTP/1/2/3>
--> Curl_cft_http_proxy --> Curl_cft_h3_proxy --> Curl_cft_udp */
ctx->state = CF_SETUP_CNNCT_EYEBALLS;
}
#endif
if(ctx->state < CF_SETUP_CNNCT_EYEBALLS) {
result = cf_ip_happy_insert_after(cf, data, ctx->transport);
#ifndef CURL_DISABLE_PROXY
/* Forcing TCP here in case of "--proxyudptunnel" because the underlying
conn is TCP (HTTP/1.1 or HTTP/2). Here, we are tunneling UDP
traffic over TCP, so ctx->transport = TRNSPRT_QUIC by default */
if(cf->conn->bits.udp_tunnel_proxy)
result = cf_ip_happy_insert_after(cf, data, TRNSPRT_TCP);
else
#endif
result = cf_ip_happy_insert_after(cf, data, ctx->transport);
if(result)
return result;
ctx->state = CF_SETUP_CNNCT_EYEBALLS;
@ -381,16 +402,31 @@ connect_sub_chain:
if(ctx->state < CF_SETUP_CNNCT_HTTP_PROXY && cf->conn->bits.httpproxy) {
#ifdef USE_SSL
if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype) &&
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) {
result = Curl_cf_ssl_proxy_insert_after(cf, data);
if(result)
return result;
/* 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(cf->conn->bits.udp_tunnel_proxy) {
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) {
if(cf->conn->bits.tunnel_proxy || cf->conn->bits.udp_tunnel_proxy) {
result = Curl_cf_http_proxy_insert_after(cf, data);
if(result)
return result;
@ -420,21 +456,45 @@ connect_sub_chain:
goto connect_sub_chain;
}
if(ctx->state < CF_SETUP_CNNCT_SSL) {
#ifdef USE_SSL
if((ctx->ssl_mode == CURL_CF_SSL_ENABLE ||
(ctx->ssl_mode != CURL_CF_SSL_DISABLE &&
cf->conn->scheme->flags & PROTOPT_SSL)) && /* we want SSL */
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */
result = Curl_cf_ssl_insert_after(cf, data);
/* Adding Curl_cf_quic_insert_after() because now we
need the next filter to be QUIC/HTTP/3 (which has SSL) */
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3)
if(cf->conn->bits.udp_tunnel_proxy) {
if(ctx->state < CF_SETUP_CNNCT_SSL) {
const struct Curl_addrinfo *addr;
addr = Curl_conn_dns_get_ip_addr(data, cf->sockindex,
cf->conn->ip_version);
if(!addr) {
failf(data, "Failed to get QUIC remote address");
return CURLE_COULDNT_RESOLVE_HOST;
}
result = Curl_cf_quic_insert_after(cf, data, addr);
if(result)
return result;
ctx->state = CF_SETUP_CNNCT_SSL;
}
#endif /* USE_SSL */
ctx->state = CF_SETUP_CNNCT_SSL;
if(!cf->next || !cf->next->connected)
goto connect_sub_chain;
}
else
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 */
{
if(ctx->state < CF_SETUP_CNNCT_SSL) {
#ifdef USE_SSL
if((ctx->ssl_mode == CURL_CF_SSL_ENABLE ||
(ctx->ssl_mode != CURL_CF_SSL_DISABLE &&
cf->conn->scheme->flags & PROTOPT_SSL)) /* we want SSL */
&& !Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */
result = Curl_cf_ssl_insert_after(cf, data);
if(result)
return result;
}
#endif /* USE_SSL */
ctx->state = CF_SETUP_CNNCT_SSL;
if(!cf->next || !cf->next->connected)
goto connect_sub_chain;
}
}
ctx->state = CF_SETUP_DONE;
cf->connected = TRUE;

View file

@ -706,6 +706,9 @@ ${SIZEOF_TIME_T_CODE}
/* if libuv is in use */
#cmakedefine USE_LIBUV 1
/* if HTTP/3 proxy support is available */
#cmakedefine USE_PROXY_HTTP3 1
/* Define to 1 if you have the <uv.h> header file. */
#cmakedefine HAVE_UV_H 1

View file

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

View file

@ -140,6 +140,7 @@ const struct curl_easyoption Curl_easyopts[] = {
{ "HTTPHEADER", CURLOPT_HTTPHEADER, CURLOT_SLIST, 0 },
{ "HTTPPOST", CURLOPT_HTTPPOST, CURLOT_OBJECT, 0 },
{ "HTTPPROXYTUNNEL", CURLOPT_HTTPPROXYTUNNEL, CURLOT_LONG, 0 },
{ "HTTPPROXYUDPTUNNEL", CURLOPT_HTTPPROXYUDPTUNNEL, CURLOT_LONG, 0 },
{ "HTTP_CONTENT_DECODING", CURLOPT_HTTP_CONTENT_DECODING, CURLOT_LONG, 0 },
{ "HTTP_TRANSFER_DECODING", CURLOPT_HTTP_TRANSFER_DECODING,
CURLOT_LONG, 0 },
@ -385,6 +386,6 @@ const struct curl_easyoption Curl_easyopts[] = {
*/
int Curl_easyopts_check(void)
{
return (CURLOPT_LASTENTRY % 10000) != (328 + 1);
return (CURLOPT_LASTENTRY % 10000) != (329 + 1);
}
#endif

View file

@ -1985,7 +1985,8 @@ static CURLcode ftp_epsv_disable(struct Curl_easy *data,
if(conn->bits.ipv6
#ifndef CURL_DISABLE_PROXY
&& !(conn->bits.tunnel_proxy || conn->bits.socksproxy)
&& !(conn->bits.tunnel_proxy || conn->bits.udp_tunnel_proxy ||
conn->bits.socksproxy)
#endif
) {
/* We cannot disable EPSV when doing IPv6, so this is instead a fail */
@ -2019,7 +2020,8 @@ static CURLcode ftp_control_addr_dup(struct Curl_easy *data, char **newhostp)
the effective control connection address is the proxy address,
not the ftp host. */
#ifndef CURL_DISABLE_PROXY
if(conn->bits.tunnel_proxy || conn->bits.socksproxy)
if(conn->bits.tunnel_proxy || conn->bits.udp_tunnel_proxy ||
conn->bits.socksproxy)
*newhostp = curlx_strdup(conn->host.name);
else
#endif

View file

@ -1762,8 +1762,8 @@ CURLcode Curl_add_custom_headers(struct Curl_easy *data,
if(is_connect)
proxy = HEADER_CONNECT;
else
proxy = data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy ?
HEADER_PROXY : HEADER_SERVER;
proxy = data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy &&
!data->conn->bits.udp_tunnel_proxy ? HEADER_PROXY : HEADER_SERVER;
switch(proxy) {
case HEADER_SERVER:
@ -1782,6 +1782,12 @@ CURLcode Curl_add_custom_headers(struct Curl_easy *data,
else
h[0] = data->set.headers;
break;
case HEADER_CONNECT_UDP:
if(data->set.sep_headers)
h[0] = data->set.proxyheaders;
else
h[0] = data->set.headers;
break;
}
#else
(void)is_connect;
@ -2113,7 +2119,8 @@ static CURLcode http_target(struct Curl_easy *data,
}
#ifndef CURL_DISABLE_PROXY
if(conn->bits.httpproxy && !conn->bits.tunnel_proxy) {
if(conn->bits.httpproxy && !conn->bits.tunnel_proxy &&
!conn->bits.udp_tunnel_proxy) {
/* Using a proxy but does not tunnel through it */
/* The path sent to the proxy is in fact the entire URL, but if the remote
@ -2744,13 +2751,17 @@ static CURLcode http_check_new_conn(struct Curl_easy *data)
alpn = Curl_conn_get_alpn_negotiated(data, conn);
if(alpn && !strcmp("h3", alpn)) {
DEBUGASSERT(Curl_conn_http_version(data, conn) == 30);
#ifndef CURL_DISABLE_PROXY
if((Curl_conn_http_version(data, conn) == 30) || !conn->bits.proxy ||
conn->bits.tunnel_proxy || conn->bits.udp_tunnel_proxy)
#endif
DEBUGASSERT(Curl_conn_http_version(data, conn) == 30);
info_version = "HTTP/3";
}
else if(alpn && !strcmp("h2", alpn)) {
#ifndef CURL_DISABLE_PROXY
if((Curl_conn_http_version(data, conn) != 20) &&
conn->bits.proxy && !conn->bits.tunnel_proxy) {
if((Curl_conn_http_version(data, conn) != 20) && conn->bits.proxy &&
!conn->bits.tunnel_proxy && !conn->bits.udp_tunnel_proxy) {
result = Curl_http2_switch(data);
if(result)
return result;
@ -4883,10 +4894,10 @@ struct name_const {
size_t namelen;
};
/* keep them sorted by length! */
static const struct name_const H2_NON_FIELD[] = {
{ STRCONST("Host") },
{ STRCONST("Upgrade") },
{ STRCONST("Protocol") },
{ STRCONST("Connection") },
{ STRCONST("Keep-Alive") },
{ STRCONST("Proxy-Connection") },
@ -4897,10 +4908,8 @@ static bool h2_permissible_field(struct dynhds_entry *e)
{
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(H2_NON_FIELD); ++i) {
if(e->namelen < H2_NON_FIELD[i].namelen)
return TRUE;
if(e->namelen == H2_NON_FIELD[i].namelen &&
curl_strequal(H2_NON_FIELD[i].name, e->name))
curl_strnequal(H2_NON_FIELD[i].name, e->name, e->namelen))
return FALSE;
}
return TRUE;

View file

@ -83,8 +83,6 @@ char *Curl_checkProxyheaders(struct Curl_easy *data,
CURLcode Curl_add_timecondition(struct Curl_easy *data, struct dynbuf *req);
CURLcode Curl_add_custom_headers(struct Curl_easy *data, bool is_connect,
int httpversion, struct dynbuf *req);
CURLcode Curl_dynhds_add_custom(struct Curl_easy *data, bool is_connect,
struct dynhds *hds);
void Curl_http_to_fold(struct dynbuf *bf);

View file

@ -2870,7 +2870,8 @@ bool Curl_http2_may_switch(struct Curl_easy *data)
(data->state.http_neg.wanted & CURL_HTTP_V2x) &&
data->state.http_neg.h2_prior_knowledge) {
#ifndef CURL_DISABLE_PROXY
if(data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy) {
if(data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy &&
!data->conn->bits.udp_tunnel_proxy) {
/* We do not support HTTP/2 proxies yet. Also it is debatable
whether or not this setting should apply to HTTP/2 proxies. */
infof(data, "Ignoring HTTP/2 prior knowledge due to proxy");

View file

@ -33,13 +33,14 @@
#include "cfilters.h"
#include "cf-h1-proxy.h"
#include "cf-h2-proxy.h"
#include "cf-h3-proxy.h"
#include "connect.h"
#include "vauth/vauth.h"
#include "curlx/strparse.h"
static CURLcode dynhds_add_custom(struct Curl_easy *data,
bool is_connect, int httpversion,
struct dynhds *hds)
bool is_udp, struct dynhds *hds)
{
struct connectdata *conn = data->conn;
struct curl_slist *h[2];
@ -49,10 +50,13 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data,
enum Curl_proxy_use proxy;
if(is_connect)
if(is_connect && !is_udp)
proxy = HEADER_CONNECT;
else if(is_connect && is_udp)
proxy = HEADER_CONNECT_UDP;
else
proxy = conn->bits.httpproxy && !conn->bits.tunnel_proxy ?
proxy = (conn->bits.httpproxy && !conn->bits.tunnel_proxy &&
!conn->bits.udp_tunnel_proxy) ?
HEADER_PROXY : HEADER_SERVER;
switch(proxy) {
@ -72,6 +76,12 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data,
else
h[0] = data->set.headers;
break;
case HEADER_CONNECT_UDP:
if(data->set.sep_headers)
h[0] = data->set.proxyheaders;
else
h[0] = data->set.headers;
break;
}
/* loop through one or two lists */
@ -255,7 +265,8 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
goto out;
}
result = dynhds_add_custom(data, TRUE, ctx->httpversion, &req->headers);
result = dynhds_add_custom(data, TRUE, ctx->httpversion,
FALSE, &req->headers);
out:
if(result && req) {
@ -267,12 +278,147 @@ out:
return result;
}
CURLcode Curl_http_proxy_create_CONNECTUDP(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
int http_version_major)
{
const char *hostname = NULL;
const char *proxy_scheme = "http";
const char *proxy_host = cf->conn->http_proxy.host.name;
char *authority = NULL;
char *path = NULL;
uint16_t port;
bool ipv6_ip;
bool proxy_ipv6_ip;
struct cf_proxy_ctx *ctx = cf->ctx;
CURLcode result;
struct httpreq *req = NULL;
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";
Curl_http_proxy_get_destination(cf, &hostname, &port, &ipv6_ip);
proxy_ipv6_ip = (strchr(proxy_host, ':') != NULL);
authority = curl_maprintf("%s%s%s:%d", proxy_ipv6_ip ? "[" : "",
proxy_host, proxy_ipv6_ip ? "]" : "",
cf->conn->http_proxy.port);
if(!authority) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
/* MASQUE FIX: envoy and h2o has different behaviour */
/* envoy expects path --> "/.well-known/masque/udp/<host>/<port/" */
/* path = aprintf("/.well-known/masque/udp/%s/%d/", hostname, port); */
/* h2o expects path --> "/<host>/<port/" */
path = curl_maprintf("/%s/%u/", hostname, (unsigned int)port);
if(!path) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
if(http_version_major == 1) {
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(http_version_major == 2 || http_version_major == 3) {
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;
}
/* If user is not overriding Host: header, we add for HTTP/1.x */
if(http_version_major == 1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) {
result = Curl_dynhds_cadd(&req->headers, "Host", authority);
if(result)
goto out;
}
if(data->req.proxyuserpwd) {
result = Curl_dynhds_h1_cadd_line(&req->headers,
data->req.proxyuserpwd);
if(result)
goto out;
}
if(http_version_major == 1 &&
!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(http_version_major == 1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) {
result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive");
if(result)
goto out;
}
if(http_version_major == 1) {
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(http_version_major >= 2) {
result = Curl_dynhds_cadd(&req->headers, "Capsule-Protocol", "?1");
if(result)
goto out;
}
}
result = dynhds_add_custom(data, TRUE, ctx->httpversion,
TRUE, &req->headers);
out:
if(result && req) {
Curl_http_req_free(req);
req = NULL;
}
curlx_free(authority);
curlx_free(path);
*preq = req;
return result;
}
static CURLcode http_proxy_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
struct cf_proxy_ctx *ctx = cf->ctx;
CURLcode result;
const char *tunnel_type; /* Determine tunnel type once and reuse */
tunnel_type = cf->conn->bits.udp_tunnel_proxy ?
"CONNECT-UDP" : "CONNECT";
if(cf->connected) {
*done = TRUE;
@ -281,19 +427,30 @@ static CURLcode http_proxy_cf_connect(struct Curl_cfilter *cf,
CURL_TRC_CF(data, cf, "connect");
connect_sub:
result = cf->next->cft->do_connect(cf->next, data, done);
if(result || !*done)
return result;
/* in case of h3_proxy, cf->next will be NULL initially */
if(cf->next) {
result = cf->next->cft->do_connect(cf->next, data, done);
if(result || !*done)
return result;
}
*done = FALSE;
if(!ctx->sub_filter_installed) {
int httpversion = 0;
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);
}
else {
alpn = "h3";
}
if(alpn)
infof(data, "CONNECT: '%s' negotiated", alpn);
infof(data, "%s: '%s' negotiated", tunnel_type, alpn);
else
infof(data, "CONNECT: no ALPN negotiated");
infof(data, "%s: no ALPN negotiated", tunnel_type);
if(alpn && !strcmp(alpn, "http/1.0")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.0");
@ -318,9 +475,18 @@ connect_sub:
goto out;
httpversion = 20;
}
#endif
#if 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);
if(result)
goto out;
httpversion = 31;
}
#endif
else {
failf(data, "CONNECT: negotiated ALPN '%s' not supported", alpn);
failf(data, "%s: negotiated ALPN '%s' not supported", tunnel_type, alpn);
result = CURLE_COULDNT_CONNECT;
goto out;
}

View file

@ -32,7 +32,8 @@
enum Curl_proxy_use {
HEADER_SERVER, /* direct to server */
HEADER_PROXY, /* regular request to proxy */
HEADER_CONNECT /* sending CONNECT to a proxy */
HEADER_CONNECT, /* sending CONNECT to a proxy */
HEADER_CONNECT_UDP /* sending CONNECT-UDP to a proxy */
};
void Curl_http_proxy_get_destination(struct Curl_cfilter *cf,
@ -43,6 +44,10 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
int http_version_major);
CURLcode Curl_http_proxy_create_CONNECTUDP(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
int http_version_major);
/* Default proxy timeout in milliseconds */
#define PROXY_TIMEOUT (3600 * 1000)
@ -59,6 +64,9 @@ extern struct Curl_cftype Curl_cft_http_proxy;
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#define IS_HTTPS_PROXY(t) (((t) == CURLPROXY_HTTPS) || \
((t) == CURLPROXY_HTTPS2))
((t) == CURLPROXY_HTTPS2) || \
((t) == CURLPROXY_HTTPS3))
#define IS_QUIC_PROXY(t) ((t) == CURLPROXY_HTTPS3)
#endif /* HEADER_CURL_HTTP_PROXY_H */

View file

@ -578,6 +578,22 @@ static CURLcode setopt_long_bool(struct Curl_easy *data, CURLoption option,
* Tunnel operations through the proxy instead of normal proxy use
*/
s->tunnel_thru_httpproxy = enabled;
if(enabled)
s->tunnel_thru_httpproxy_udp = FALSE;
break;
case CURLOPT_HTTPPROXYUDPTUNNEL:
/*
* Tunnel operations through the UDP proxy instead of normal proxy use
*/
#ifdef USE_PROXY_HTTP3
s->tunnel_thru_httpproxy_udp = enabled;
if(enabled)
s->tunnel_thru_httpproxy = FALSE;
#else
if(enabled)
return CURLE_NOT_BUILT_IN;
s->tunnel_thru_httpproxy_udp = FALSE;
#endif
break;
case CURLOPT_HAPROXYPROTOCOL:
/*
@ -1041,8 +1057,12 @@ static CURLcode setopt_long_proxy(struct Curl_easy *data, CURLoption option,
case CURLOPT_PROXYAUTH:
return httpauth(data, TRUE, (unsigned long)arg);
case CURLOPT_PROXYTYPE:
if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_SOCKS5_HOSTNAME))
if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_HTTPS3))
return CURLE_BAD_FUNCTION_ARGUMENT;
#ifndef USE_PROXY_HTTP3
if(arg == CURLPROXY_HTTPS3)
return CURLE_NOT_BUILT_IN;
#endif
s->proxytype = (unsigned char)arg;
break;
case CURLOPT_SOCKS5_AUTH:

View file

@ -885,7 +885,8 @@ static bool url_match_proxy_use(struct connectdata *conn,
return FALSE;
if(m->needle->bits.httpproxy) {
if(m->needle->bits.tunnel_proxy != conn->bits.tunnel_proxy)
if(m->needle->bits.tunnel_proxy != conn->bits.tunnel_proxy ||
m->needle->bits.udp_tunnel_proxy != conn->bits.udp_tunnel_proxy)
return FALSE;
if(!proxy_info_matches(&m->needle->http_proxy, &conn->http_proxy))
@ -1022,7 +1023,8 @@ static bool url_match_destination(struct connectdata *conn,
* not talking to an HTTP proxy OR using a tunnel through a proxy */
if((m->needle->scheme->flags & PROTOPT_SSL)
#ifndef CURL_DISABLE_PROXY
|| !m->needle->bits.httpproxy || m->needle->bits.tunnel_proxy
|| !m->needle->bits.httpproxy || m->needle->bits.tunnel_proxy ||
m->needle->bits.udp_tunnel_proxy
#endif
) {
if(!curl_strequal(m->needle->scheme->name, conn->scheme->name)) {
@ -1391,6 +1393,7 @@ static struct connectdata *allocate_conn(struct Curl_easy *data)
conn->bits.proxy_user_passwd = !!data->state.aptr.proxyuser;
conn->bits.tunnel_proxy = data->set.tunnel_thru_httpproxy;
conn->bits.udp_tunnel_proxy = data->set.tunnel_thru_httpproxy_udp;
#endif /* CURL_DISABLE_PROXY */
#ifndef CURL_DISABLE_FTP
@ -1399,7 +1402,12 @@ static struct connectdata *allocate_conn(struct Curl_easy *data)
#endif
conn->ip_version = data->set.ipver;
conn->bits.connect_only = (bool)data->set.connect_only;
conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */
#ifndef CURL_DISABLE_PROXY
if(conn->http_proxy.proxytype == CURLPROXY_HTTPS3)
conn->transport_wanted = TRNSPRT_QUIC;
else
#endif
conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */
/* Store the local bind parameters that will be used for this connection */
if(data->set.str[STRING_DEVICE]) {
@ -1798,7 +1806,8 @@ static CURLcode setup_connection_internals(struct Curl_easy *data,
/* Now create the destination name */
#ifndef CURL_DISABLE_PROXY
if(conn->bits.httpproxy && !conn->bits.tunnel_proxy) {
if(conn->bits.httpproxy && !conn->bits.tunnel_proxy &&
!conn->bits.udp_tunnel_proxy) {
hostname = conn->http_proxy.host.name;
port = conn->http_proxy.port;
}
@ -1959,10 +1968,12 @@ static CURLcode parse_proxy(struct Curl_easy *data,
}
if(curl_strequal("https", scheme)) {
if(proxytype != CURLPROXY_HTTPS2)
if(proxytype != CURLPROXY_HTTPS2 && proxytype != CURLPROXY_HTTPS3)
proxytype = CURLPROXY_HTTPS;
else
else if(proxytype != CURLPROXY_HTTPS3)
proxytype = CURLPROXY_HTTPS2;
else
proxytype = CURLPROXY_HTTPS3;
}
else if(curl_strequal("socks5h", scheme))
proxytype = CURLPROXY_SOCKS5_HOSTNAME;
@ -2269,9 +2280,9 @@ static CURLcode create_conn_helper_init_proxy(struct Curl_easy *data,
/* force this connection's protocol to become HTTP if compatible */
if(!(conn->scheme->protocol & PROTO_FAMILY_HTTP)) {
if((conn->scheme->flags & PROTOPT_PROXY_AS_HTTP) &&
!conn->bits.tunnel_proxy)
!conn->bits.tunnel_proxy && !conn->bits.udp_tunnel_proxy)
conn->scheme = &Curl_scheme_http;
else
else if(!conn->bits.udp_tunnel_proxy)
/* if not converting to HTTP over the proxy, enforce tunneling */
conn->bits.tunnel_proxy = TRUE;
}
@ -2281,6 +2292,7 @@ static CURLcode create_conn_helper_init_proxy(struct Curl_easy *data,
else {
conn->bits.httpproxy = FALSE; /* not an HTTP proxy */
conn->bits.tunnel_proxy = FALSE; /* no tunneling if not HTTP */
conn->bits.udp_tunnel_proxy = FALSE; /* no tunneling if not HTTP */
}
if(conn->socks_proxy.host.rawalloc) {
@ -2304,6 +2316,10 @@ static CURLcode create_conn_helper_init_proxy(struct Curl_easy *data,
conn->bits.httpproxy = FALSE;
}
conn->bits.proxy = conn->bits.httpproxy || conn->bits.socksproxy;
if(conn->bits.httpproxy)
conn->bits.udp_tunnel_proxy = data->set.tunnel_thru_httpproxy_udp;
else
conn->bits.udp_tunnel_proxy = FALSE;
if(!conn->bits.proxy) {
/* we are not using the proxy after all... */
@ -2312,6 +2328,7 @@ static CURLcode create_conn_helper_init_proxy(struct Curl_easy *data,
conn->bits.socksproxy = FALSE;
conn->bits.proxy_user_passwd = FALSE;
conn->bits.tunnel_proxy = FALSE;
conn->bits.udp_tunnel_proxy = FALSE;
/* CURLPROXY_HTTPS does not have its own flag in conn->bits, yet we need
to signal that CURLPROXY_HTTPS is not used for this connection */
conn->http_proxy.proxytype = CURLPROXY_HTTP;
@ -3092,7 +3109,8 @@ static CURLcode url_create_needle(struct Curl_easy *data,
* If the protocol is using SSL and HTTP proxy is used, we set
* the tunnel_proxy bit.
*************************************************************/
if((needle->given->flags & PROTOPT_SSL) && needle->bits.httpproxy)
if((needle->given->flags & PROTOPT_SSL) && needle->bits.httpproxy &&
!needle->bits.udp_tunnel_proxy)
needle->bits.tunnel_proxy = TRUE;
#endif
@ -3166,7 +3184,7 @@ static CURLcode url_create_needle(struct Curl_easy *data,
* we set the tunnel_proxy bit.
*************************************************************/
if((needle->bits.conn_to_host || needle->bits.conn_to_port) &&
needle->bits.httpproxy)
needle->bits.httpproxy && !needle->bits.udp_tunnel_proxy)
needle->bits.tunnel_proxy = TRUE;
#endif

View file

@ -259,6 +259,8 @@ struct ConnectBits {
This is implicit when SSL-protocols are used through
proxies, but can also be enabled explicitly by
apps */
BIT(udp_tunnel_proxy); /* if CONNECT-UDP is used to "tunnel" through
the proxy */
BIT(proxy); /* if set, this transfer is done through a proxy - any type */
#endif
/* always modify bits.close with the connclose() and connkeep() macros! */
@ -1264,6 +1266,7 @@ struct UserDefined {
BIT(get_filetime); /* get the time and get of the remote file */
#ifndef CURL_DISABLE_PROXY
BIT(tunnel_thru_httpproxy); /* use CONNECT through an HTTP proxy */
BIT(tunnel_thru_httpproxy_udp); /* use CONNECT-UDP through an HTTP proxy */
#endif
BIT(prefer_ascii); /* ASCII rather than binary */
BIT(remote_append); /* append, not overwrite, on upload */

View file

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

View file

@ -72,6 +72,7 @@
#define QUIC_MAX_STREAMS (256 * 1024)
#define QUIC_HANDSHAKE_TIMEOUT (10 * NGTCP2_SECONDS)
#define QUIC_TUNNEL_INBUF_SIZE (64 * 1024)
/* We announce a small window size in transport param to the server,
* and grow that immediately to max when no rate limit is in place.
@ -139,6 +140,9 @@ struct cf_ngtcp2_ctx {
is accepted by peer */
CURLcode tls_vrfy_result; /* result of TLS peer verification */
int qlogfd;
const struct Curl_addrinfo *addr; /* remote addr */
unsigned char *tunnel_inbuf; /* ingress buffer for tunneled packets */
size_t tunnel_inbuf_len;
BIT(initialized);
BIT(tls_handshake_complete); /* TLS handshake is done */
BIT(use_earlydata); /* Using 0RTT data */
@ -156,6 +160,8 @@ static void cf_ngtcp2_ctx_init(struct cf_ngtcp2_ctx *ctx)
{
DEBUGASSERT(!ctx->initialized);
ctx->qlogfd = -1;
ctx->tunnel_inbuf = NULL;
ctx->tunnel_inbuf_len = 0;
ctx->version = NGTCP2_PROTO_VER_MAX;
Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
H3_STREAM_POOL_SPARES);
@ -173,6 +179,8 @@ static void cf_ngtcp2_ctx_free(struct cf_ngtcp2_ctx *ctx)
curlx_dyn_free(&ctx->scratch);
Curl_uint32_hash_destroy(&ctx->streams);
Curl_ssl_peer_cleanup(&ctx->peer);
curlx_safefree(ctx->tunnel_inbuf);
ctx->tunnel_inbuf_len = 0;
}
curlx_free(ctx);
}
@ -493,7 +501,7 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx,
static CURLcode init_ngh3_conn(struct Curl_cfilter *cf,
struct Curl_easy *data);
static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data)
static int cb_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data)
{
struct Curl_cfilter *cf = user_data;
struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL;
@ -860,7 +868,7 @@ static ngtcp2_callbacks ng_callbacks = {
ngtcp2_crypto_client_initial_cb,
NULL, /* recv_client_initial */
ngtcp2_crypto_recv_crypto_data_cb,
cf_ngtcp2_handshake_completed,
cb_ngtcp2_handshake_completed,
NULL, /* recv_version_negotiation */
ngtcp2_crypto_encrypt_cb,
ngtcp2_crypto_decrypt_cb,
@ -979,6 +987,13 @@ static CURLcode cf_ngtcp2_adjust_pollset(struct Curl_cfilter *cf,
if(!ctx->qconn)
return CURLE_OK;
if(!cf->next)
return CURLE_OK;
if(cf->next->cft != &Curl_cft_udp) {
return cf->next->cft->adjust_pollset(cf->next, data, ps);
}
Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send);
if(!want_send && !Curl_bufq_is_empty(&ctx->q.sendbuf))
want_send = TRUE;
@ -1897,8 +1912,71 @@ static CURLcode cf_progress_ingress(struct Curl_cfilter *cf,
rctx.pktx = pktx;
rctx.pkt_count = 0;
return vquic_recv_packets(cf, data, &ctx->q, 1000,
if(!cf->next)
return CURLE_OK;
if(cf->next->cft == &Curl_cft_udp) {
return vquic_recv_packets(cf, data, &ctx->q, 1000,
cf_ngtcp2_recv_pkts, &rctx);
}
else {
unsigned char *buf;
size_t max_udp_payload = QUIC_TUNNEL_INBUF_SIZE;
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(TRUE) {
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;
}
}
}
/**
@ -2213,6 +2291,12 @@ static CURLcode cf_ngtcp2_shutdown(struct Curl_cfilter *cf,
return CURLE_OK;
}
if(!cf->next) {
Curl_bufq_reset(&ctx->q.sendbuf);
*done = TRUE;
return CURLE_OK;
}
CF_DATA_SAVE(save, cf, data);
*done = FALSE;
pktx_init(&pktx, cf, data);
@ -2641,30 +2725,72 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
if(result)
return result;
if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL))
return CURLE_QUIC_CONNECT_ERROR;
ctx->q.local_addrlen = sizeof(ctx->q.local_addr);
rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr,
&ctx->q.local_addrlen);
if(rv == -1)
if(!cf->next)
return CURLE_QUIC_CONNECT_ERROR;
ngtcp2_addr_init(&ctx->connected_path.local,
(struct sockaddr *)&ctx->q.local_addr,
ctx->q.local_addrlen);
ngtcp2_addr_init(&ctx->connected_path.remote,
&sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen);
if(cf->next->cft == &Curl_cft_udp) {
if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL))
return CURLE_QUIC_CONNECT_ERROR;
ctx->q.local_addrlen = sizeof(ctx->q.local_addr);
rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr,
&ctx->q.local_addrlen);
if(rv == -1)
return CURLE_QUIC_CONNECT_ERROR;
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
&ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ngtcp2_addr_init(&ctx->connected_path.local,
(struct sockaddr *)&ctx->q.local_addr,
ctx->q.local_addrlen);
ngtcp2_addr_init(&ctx->connected_path.remote,
&sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen);
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
&ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
}
else {
if(!ctx->addr || !ctx->addr->ai_addr || !ctx->addr->ai_addrlen)
return CURLE_QUIC_CONNECT_ERROR;
memset(&ctx->q.local_addr, 0, sizeof(ctx->q.local_addr));
switch(ctx->addr->ai_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,
ctx->addr->ai_addr, (socklen_t)ctx->addr->ai_addrlen);
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
&ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
}
result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, &ALPN_SPEC_H3,
cf_ngtcp2_tls_ctx_setup, &ctx->tls,
@ -2713,11 +2839,16 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf,
return CURLE_OK;
}
/* Connect the UDP filter first */
if(!cf->next->connected) {
result = Curl_conn_cf_connect(cf->next, data, done);
if(result || !*done)
return result;
if(!cf->next)
return CURLE_QUIC_CONNECT_ERROR;
if(cf->next->cft == &Curl_cft_udp) {
/* Connect the UDP filter first */
if(!cf->next->connected) {
result = Curl_conn_cf_connect(cf->next, data, done);
if(result || !*done)
return result;
}
}
*done = FALSE;
@ -2794,11 +2925,13 @@ out:
#ifdef CURLVERBOSE
if(result) {
struct ip_quadruple ip;
if(cf->next && cf->next->cft == &Curl_cft_udp) {
struct ip_quadruple ip;
if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip))
infof(data, "QUIC connect to %s port %u failed: %s",
ip.remote_ip, ip.remote_port, curl_easy_strerror(result));
if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip))
infof(data, "QUIC connect to %s port %u failed: %s",
ip.remote_ip, ip.remote_port, curl_easy_strerror(result));
}
}
#endif
if(!result && ctx->qconn) {
@ -2994,4 +3127,38 @@ out:
return result;
}
CURLcode Curl_cf_ngtcp2_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
const struct Curl_addrinfo *remoteaddr)
{
struct cf_ngtcp2_ctx *ctx = NULL;
struct Curl_cfilter *cf = NULL;
CURLcode result;
if(!remoteaddr) {
failf(data, "No address available for HTTP/3 connection");
return CURLE_COULDNT_RESOLVE_HOST;
}
ctx = curlx_calloc(1, sizeof(*ctx));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
cf_ngtcp2_ctx_init(ctx);
ctx->addr = remoteaddr;
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

View file

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

View file

@ -261,6 +261,44 @@ out:
return result;
}
/* Split QUIC payload by datagram (gso) boundaries when sending over a
* non-UDP lower filter (for example CONNECT-UDP proxy tunnel). */
static CURLcode send_packet_no_gso_cf(struct Curl_cfilter *cf,
struct Curl_easy *data,
const uint8_t *pkt, size_t pktlen,
size_t gsolen, size_t *psent)
{
const uint8_t *p, *end = pkt + pktlen;
size_t sent, len;
CURLcode result = CURLE_OK;
VERBOSE(size_t calls = 0);
*psent = 0;
/* Send one datagram-sized chunk per call into the lower filter. */
for(p = pkt; p < end; p += len) {
len = CURLMIN(gsolen, (size_t)(end - p));
result = Curl_conn_cf_send(cf->next, data, p, len, FALSE, &sent);
/* Report forward progress even if we return CURLE_AGAIN later. */
*psent += sent;
VERBOSE(++calls);
/* Preserve lower-filter errors (including CURLE_AGAIN). */
if(result)
goto out;
if(sent < len) {
/* We need whole datagrams here. Partial accept means blocked. */
result = CURLE_AGAIN;
goto out;
}
}
out:
CURL_TRC_CF(data, cf, "vquic_cf_send(len=%zu, gso=%zu, calls=%zu)"
" -> %d, sent=%zu",
pktlen, gsolen, calls, result, *psent);
return result;
}
static CURLcode vquic_send_packets(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct cf_quic_ctx *qctx,
@ -302,6 +340,11 @@ CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data,
CURLcode result;
size_t gsolen;
if(!cf->next) {
CURL_TRC_CF(data, cf, "vquic_flush called without lower filter");
return CURLE_SEND_ERROR;
}
while(Curl_bufq_peek(&qctx->sendbuf, &buf, &blen)) {
gsolen = qctx->gsolen;
if(qctx->split_len) {
@ -310,7 +353,21 @@ CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data,
blen = qctx->split_len;
}
result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent);
if(cf->next->cft == &Curl_cft_udp) {
result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent);
}
else {
if(cf->conn && cf->conn->bits.udp_tunnel_proxy &&
gsolen && (blen > gsolen)) {
/* Send one datagram at a time to preserve packet boundaries. */
result = send_packet_no_gso_cf(cf, data, buf, blen, gsolen, &sent);
}
else {
/* No GSO aggregate to split, regular lower-filter send is enough. */
result = Curl_conn_cf_send(cf->next, data, buf, blen, FALSE, &sent);
}
}
if(result) {
if(result == CURLE_AGAIN) {
Curl_bufq_skip(&qctx->sendbuf, sent);
@ -699,6 +756,20 @@ CURLcode Curl_qlogdir(struct Curl_easy *data,
return CURLE_OK;
}
CURLcode Curl_cf_quic_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
const struct Curl_addrinfo *remoteaddr)
{
#if defined(USE_NGTCP2) && defined(USE_NGHTTP3)
return Curl_cf_ngtcp2_insert_after(cf_at, data, remoteaddr);
#else
(void)cf_at;
(void)data;
(void)remoteaddr;
return CURLE_NOT_BUILT_IN;
#endif
}
CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
@ -737,7 +808,8 @@ CURLcode Curl_conn_may_http3(struct Curl_easy *data,
failf(data, "HTTP/3 is not supported over a SOCKS proxy");
return CURLE_URL_MALFORMAT;
}
if(conn->bits.httpproxy && conn->bits.tunnel_proxy) {
if(conn->bits.httpproxy && conn->bits.tunnel_proxy
&& !conn->bits.udp_tunnel_proxy) {
failf(data, "HTTP/3 is not supported over an HTTP proxy");
return CURLE_URL_MALFORMAT;
}

View file

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

View file

@ -3711,8 +3711,11 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
return result;
}
if(data->set.fdebug && data->set.verbose) {
/* the SSL trace callback is only used for verbose logging */
if(data->set.fdebug && data->set.verbose &&
(peer->transport != TRNSPRT_QUIC)) {
/* the SSL trace callback is only used for verbose logging;
* QUIC connections use a different TLS record format that
* ossl_trace cannot handle */
SSL_CTX_set_msg_callback(octx->ssl_ctx, ossl_trace);
SSL_CTX_set_msg_callback_arg(octx->ssl_ctx, cf);
}

View file

@ -734,6 +734,11 @@ static CURLcode proxy_setopts(struct OperationConfig *config, CURL *curl)
MY_SETOPT_STR(curl, CURLOPT_PROXYUSERPWD, config->proxyuserpwd);
my_setopt_long(curl, CURLOPT_HTTPPROXYTUNNEL, config->proxytunnel);
/* new in libcurl 8.20.0 */
my_setopt_long(curl, CURLOPT_HTTPPROXYUDPTUNNEL, config->proxyudptunnel);
/* new in libcurl 7.52.0 */
if(config->preproxy)
MY_SETOPT_STR(curl, CURLOPT_PRE_PROXY, config->preproxy);

View file

@ -232,6 +232,7 @@ struct OperationConfig {
BIT(mail_rcpt_allowfails); /* --mail-rcpt-allowfails */
BIT(sasl_ir); /* Enable/disable SASL initial response */
BIT(proxytunnel);
BIT(proxyudptunnel);
BIT(ftp_append); /* APPE on ftp */
BIT(use_ascii); /* select ASCII or text transfer */
BIT(autoreferer); /* automatically set referer */

View file

@ -254,6 +254,7 @@ static const struct LongShort aliases[]= {
{"proxy-digest", ARG_BOOL, ' ', C_PROXY_DIGEST},
{"proxy-header", ARG_STRG, ' ', C_PROXY_HEADER},
{"proxy-http2", ARG_BOOL, ' ', C_PROXY_HTTP2},
{"proxy-http3", ARG_BOOL, ' ', C_PROXY_HTTP3},
{"proxy-insecure", ARG_BOOL, ' ', C_PROXY_INSECURE},
{"proxy-key", ARG_FILE|ARG_TLS, ' ', C_PROXY_KEY},
{"proxy-key-type", ARG_STRG|ARG_TLS, ' ', C_PROXY_KEY_TYPE},
@ -274,6 +275,7 @@ static const struct LongShort aliases[]= {
{"proxy-user", ARG_STRG|ARG_CLEAR, 'U', C_PROXY_USER},
{"proxy1.0", ARG_STRG, ' ', C_PROXY1_0},
{"proxytunnel", ARG_BOOL, 'p', C_PROXYTUNNEL},
{"proxyudptunnel", ARG_BOOL, ' ', C_PROXYUDPTUNNEL},
{"pubkey", ARG_STRG, ' ', C_PUBKEY},
{"quote", ARG_STRG, 'Q', C_QUOTE},
{"random-file", ARG_FILE|ARG_DEPR, ' ', C_RANDOM_FILE},
@ -2028,6 +2030,18 @@ static ParameterError opt_bool(struct OperationConfig *config,
config->proxyver = toggle ? CURLPROXY_HTTPS2 : CURLPROXY_HTTPS;
break;
case C_PROXY_HTTP3: /* --proxy-http3 */
#ifndef USE_PROXY_HTTP3
if(toggle)
return PARAM_LIBCURL_DOESNT_SUPPORT;
config->proxyver = CURLPROXY_HTTPS;
#else
if(!feature_httpsproxy || !feature_http3)
return PARAM_LIBCURL_DOESNT_SUPPORT;
config->proxyver = toggle ? CURLPROXY_HTTPS3 : CURLPROXY_HTTPS;
#endif
break;
case C_APPEND: /* --append */
config->ftp_append = toggle;
break;
@ -2163,8 +2177,28 @@ static ParameterError opt_bool(struct OperationConfig *config,
case C_REMOTE_NAME: /* --remote-name */
return parse_remote_name(config, toggle);
case C_PROXYTUNNEL: /* --proxytunnel */
if(toggle && config->proxyudptunnel) {
errorf("--proxytunnel is mutually exclusive with --proxyudptunnel");
return PARAM_BAD_USE;
}
config->proxytunnel = toggle;
break;
case C_PROXYUDPTUNNEL: /* --proxyudptunnel */
/* UDP proxy tunnel for non-http protocols */
#ifndef USE_PROXY_HTTP3
if(toggle)
return PARAM_LIBCURL_DOESNT_SUPPORT;
config->proxyudptunnel = FALSE;
#else
if(toggle && !feature_http3)
return PARAM_LIBCURL_DOESNT_SUPPORT;
if(toggle && config->proxytunnel) {
errorf("--proxyudptunnel is mutually exclusive with --proxytunnel");
return PARAM_BAD_USE;
}
config->proxyudptunnel = toggle;
#endif
break;
case C_DISABLE: /* --disable */
/* if used first, already taken care of, we do it like this so we do not
cause an error! */
@ -2902,8 +2936,13 @@ static ParameterError opt_string(struct OperationConfig *config,
case C_PROXY: /* --proxy */
/* --proxy */
err = getstr(&config->proxy, nextarg, ALLOW_BLANK);
if(config->proxyver != CURLPROXY_HTTPS2)
if(config->proxyver != CURLPROXY_HTTPS2 &&
config->proxyver != CURLPROXY_HTTPS3)
config->proxyver = CURLPROXY_HTTP;
else if(config->proxyver != CURLPROXY_HTTPS3)
config->proxyver = CURLPROXY_HTTPS2;
else
config->proxyver = CURLPROXY_HTTPS3;
break;
case C_REQUEST: /* --request */
/* set custom request */

View file

@ -201,6 +201,7 @@ typedef enum {
C_PROXY_DIGEST,
C_PROXY_HEADER,
C_PROXY_HTTP2,
C_PROXY_HTTP3,
C_PROXY_INSECURE,
C_PROXY_KEY,
C_PROXY_KEY_TYPE,
@ -219,6 +220,7 @@ typedef enum {
C_PROXY_USER,
C_PROXY1_0,
C_PROXYTUNNEL,
C_PROXYUDPTUNNEL,
C_PUBKEY,
C_QUOTE,
C_RANDOM_FILE,

View file

@ -542,6 +542,9 @@ const struct helptxt helptext[] = {
{ " --proxy-http2",
"Use HTTP/2 with HTTPS proxy",
CURLHELP_HTTP | CURLHELP_PROXY },
{ " --proxy-http3",
"Use HTTP/3 with HTTPS proxy",
CURLHELP_HTTP | CURLHELP_PROXY },
{ " --proxy-insecure",
"Skip HTTPS proxy cert verification",
CURLHELP_PROXY | CURLHELP_TLS },
@ -596,6 +599,9 @@ const struct helptxt helptext[] = {
{ "-p, --proxytunnel",
"HTTP proxy tunnel (using CONNECT)",
CURLHELP_PROXY },
{ " --proxyudptunnel",
"HTTP proxy tunnel (using CONNECT-UDP)",
CURLHELP_PROXY },
{ " --pubkey <key>",
"SSH Public key filename",
CURLHELP_SFTP | CURLHELP_SCP | CURLHELP_SSH | CURLHELP_AUTH },

View file

@ -283,7 +283,7 @@ test3100 test3101 test3102 test3103 test3104 test3105 \
\
test3200 test3201 test3202 test3203 test3204 test3205 test3206 test3207 \
test3208 test3209 test3210 test3211 test3212 test3213 test3214 test3215 \
test3216 test3217 test3218 test3219 \
test3216 test3217 test3218 test3219 test3220 \
\
test3300 test3301 \
\

19
tests/data/test3220 Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
unittest
capsule
</keywords>
</info>
<client>
<features>
unittest
</features>
<name>
capsule UDP decoding edge cases
</name>
</client>
</testcase>

View file

@ -28,6 +28,12 @@ if(NOT CADDY)
endif()
mark_as_advanced(CADDY)
find_program(H2O "h2o") # /usr/local/bin/h2o
if(NOT H2O)
set(H2O "")
endif()
mark_as_advanced(H2O)
find_program(VSFTPD "vsftpd") # /usr/sbin/vsftpd
if(NOT VSFTPD)
set(VSFTPD "")

View file

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

View file

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

View file

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

View file

@ -0,0 +1,657 @@
#!/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,
tunneludp: 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")
elif tunneludp:
xargs.append("--proxyudptunnel")
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,
tunneludp: 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")
elif tunneludp:
xargs.append("--proxyudptunnel")
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", "tunnel", "tunneludp"],
[
pytest.param("http/1.1", "h3", True, False, id="h1_over_h3_proxytunnel"),
pytest.param(
"h2",
"h3",
True,
False,
marks=MARK_NEEDS_NGHTTP2,
id="h2_over_h3_proxytunnel",
),
pytest.param("h3", "h3", False, True, id="h3_over_h3_proxyudptunnel"),
pytest.param(
"h3",
"h2",
False,
True,
marks=MARK_NEEDS_NGHTTP2,
id="h3_over_h2_proxyudptunnel",
),
pytest.param("h3", "http/1.1", False, True, id="h3_over_h1_proxyudptunnel"),
],
)
def test_success_matrix(
self,
env: Env,
h2o_server,
h2o_proxy,
alpn_proto,
proxy_proto,
tunnel,
tunneludp,
):
_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, tunneludp, 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", "tunnel", "tunneludp", "exp_err"],
[
pytest.param(
"http/1.1",
"h3",
True,
False,
"failed: could not connect to server",
id="fail_h1_over_h3_proxytunnel",
),
pytest.param(
"h2",
"h3",
True,
False,
"failed: could not connect to server",
marks=MARK_NEEDS_NGHTTP2,
id="fail_h2_over_h3_proxytunnel",
),
pytest.param(
"h3",
"h3",
False,
True,
"failed: could not connect to server",
id="fail_h3_over_h3_proxyudptunnel",
),
pytest.param(
"h3",
"h2",
False,
True,
"connect-udp response status 400",
marks=MARK_NEEDS_NGHTTP2,
id="fail_h3_over_h2_proxyudptunnel",
),
pytest.param(
"h3",
"http/1.1",
False,
True,
"connect-udp tunnel failed, response 404",
id="fail_h3_over_h1_proxyudptunnel",
),
],
)
def test_failure_matrix(
self,
env: Env,
httpd,
nghttpx,
alpn_proto,
proxy_proto,
tunnel,
tunneludp,
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, tunneludp)
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 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_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}"
)
@pytest.mark.skipif(
condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support"
)
def test_guard_proxyudptunnel_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/",
"--proxyudptunnel",
"--proxy-insecure",
"--cacert",
env.ca.cert_file,
]
r = curl.http_download(
urls=[url], alpn_proto="h3", 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_graceful_shutdown_sends_connection_close(
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", tunneludp=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_proxy_goes_away_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_large_download_10mb_via_connect(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", True, False, 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_large_upload_2mb_via_connect(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", True, False, 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_parallel_downloads_via_connect(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", True, False, 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_proxy_basic_auth_header(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", True, False, 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_proxy_connection_reuse_sequential(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", True, False, 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_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", True, False, 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_udp_tunnel_varying_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", False, 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_udp_tunnel_capsule_protocol_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", False, 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_connect_timeout_unreachable_proxy(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_h2_over_udp_tunnel_rejected(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", False, True, insecure=True)
# h2 requires TCP, but CONNECT-UDP provides a UDP tunnel.
# httpd only supports h1/h2 over TCP, so this should fail.
r = curl.http_download(
urls=[url], alpn_proto="h2", with_stats=True, extra_args=proxy_args
)
assert r.exit_code != 0, "expected failure: h2 cannot work over UDP tunnel"

View file

@ -683,12 +683,18 @@ class CurlClient:
os.makedirs(path)
def get_proxy_args(self, proto: str = 'http/1.1',
proxys: bool = True, tunnel: bool = False,
proxys: bool = True,
tunnel: bool = False, tunneludp: bool = False,
use_ip: bool = False, use_ipv6: bool = False):
proxy_name = '[::1]' if use_ipv6 else \
self._server_addr if use_ip else self.env.proxy_domain
if proxys:
pport = self.env.pts_port(proto) if tunnel else self.env.proxys_port
if tunnel or tunneludp:
pport = self.env.pts_port(proto)
elif proto == 'h3':
pport = self.env.h3proxys_port
else:
pport = self.env.proxys_port
xargs = [
'--proxy', f'https://{proxy_name}:{pport}/',
'--proxy-cacert', self.env.ca.cert_file,
@ -697,6 +703,8 @@ class CurlClient:
xargs.extend(['--resolve', f'{proxy_name}:{pport}:{self._server_addr}'])
if proto == 'h2':
xargs.append('--proxy-http2')
elif proto == 'h3':
xargs.append('--proxy-http3')
else:
xargs = [
'--proxy', f'http://{proxy_name}:{self.env.proxy_port}/',
@ -705,6 +713,8 @@ class CurlClient:
xargs.extend(['--resolve', f'{proxy_name}:{self.env.proxy_port}:{self._server_addr}'])
if tunnel:
xargs.append('--proxytunnel')
elif tunneludp:
xargs.append('--proxyudptunnel')
return xargs
def http_get(self, url: str, extra_args: Optional[List[str]] = None,

View file

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

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

@ -0,0 +1,418 @@
#!/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)
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.connect-udp: [+*]
proxy.ssl.verify-peer: OFF
http2-reprioritize-blocking-assets: ON
access-log: {self._run_dir}/access.log
error-log: {self._error_log}
""")
def wait_for_state(
self,
live: bool,
timeout: timedelta,
url: Optional[str] = None,
log_prefix: str = "h2o",
):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout
if url is None:
url = f"https://{self.env.proxy_domain}:{self._port}/"
while datetime.now() < try_until:
if live:
r = curl.http_get(
url=url, extra_args=["--trace", "curl.trace", "--trace-time"]
)
if r.exit_code == 0:
return True
else:
r = curl.http_get(url=url)
if r.exit_code != 0:
return True
time.sleep(0.1)
if live:
self._log("error", f"Proxy still not responding after {timeout}")
else:
self._log("debug", f"Proxy still responding after {timeout}")
return False

View file

@ -70,6 +70,7 @@ my @unsupported_protocol_num = (
# numerical input they do not recognize as valid
my @not_built_in_num = (
'CURLOPT_HTTPAUTH',
'CURLOPT_HTTPPROXYUDPTUNNEL',
'CURLOPT_PROXYAUTH',
'CURLOPT_SOCKS5_AUTH',
);

View file

@ -47,4 +47,5 @@ TESTS_C = \
unit2600.c unit2601.c unit2602.c unit2603.c unit2604.c unit2605.c \
unit3200.c unit3205.c \
unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c unit3219.c \
unit3220.c \
unit3300.c unit3301.c

247
tests/unit/unit3220.c Normal file
View file

@ -0,0 +1,247 @@
/***************************************************************************
* _ _ ____ _
* 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));
}
static void check_payload_written_accounting(size_t payload_len)
{
unsigned char hdr[HTTP_CAPSULE_HEADER_MAX_SIZE];
size_t hdr_len, capsule_bytes, expected;
hdr_len = Curl_capsule_encap_udp_hdr(hdr, sizeof(hdr), payload_len);
fail_unless(hdr_len, "failed to encode capsule header");
for(capsule_bytes = 0; capsule_bytes <= (hdr_len + payload_len + 2);
++capsule_bytes) {
expected = 0;
if(capsule_bytes > hdr_len) {
expected = capsule_bytes - hdr_len;
if(expected > payload_len)
expected = payload_len;
}
fail_unless(Curl_capsule_udp_payload_written(payload_len, capsule_bytes) ==
expected, "capsule payload accounting mismatch");
}
}
static void test_capsule_udp_payload_written(void)
{
check_payload_written_accounting(0);
check_payload_written_accounting(3);
check_payload_written_accounting(63);
check_payload_written_accounting(64);
check_payload_written_accounting(16383);
check_payload_written_accounting(16384);
}
#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_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);
/* payload does not fit output buffer -> AGAIN and no consumption */
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_AGAIN, "expected AGAIN for short output buffer");
fail_unless(nread == 0, "expected zero read on short output buffer");
fail_unless(Curl_bufq_len(&q) == sizeof(payload_3b),
"capsule must remain buffered on short output");
/* 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_unit3220(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();
test_capsule_udp_payload_written();
#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();
#endif
UNITTEST_END_SIMPLE
}