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

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

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

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

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

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

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

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

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

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

View file

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

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()
@ -2046,6 +2062,7 @@ curl_add_if("NTLM" CURL_ENABLE_NTLM AND
curl_add_if("TLS-SRP" USE_TLS_SRP)
curl_add_if("HTTP2" USE_NGHTTP2)
curl_add_if("HTTP3" USE_NGTCP2 OR USE_QUICHE)
curl_add_if("PROXY-HTTP3" USE_PROXY_HTTP3)
curl_add_if("MultiSSL" CURL_WITH_MULTI_SSL)
curl_add_if("HTTPS-proxy" NOT CURL_DISABLE_PROXY AND _ssl_enabled AND (USE_OPENSSL OR USE_GNUTLS
OR USE_SCHANNEL OR USE_RUSTLS OR USE_MBEDTLS OR

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
@ -5028,6 +5068,28 @@ if test "$want_ssls_export" != "no"; then
fi
fi
dnl *************************************************************
dnl check whether experimental HTTP/3 proxy support is enabled
dnl
if test "$want_proxy_http3" = "yes"; then
AC_MSG_CHECKING([whether HTTP/3 proxy support is available])
if test "$CURL_DISABLE_PROXY" = "1"; then
AC_MSG_ERROR([--enable-proxy-http3 requires proxy support])
elif test "$CURL_DISABLE_HTTP" = "1"; then
AC_MSG_ERROR([--enable-proxy-http3 requires HTTP support])
elif test "$USE_NGTCP2_H3" != "1"; then
AC_MSG_ERROR([--enable-proxy-http3 requires ngtcp2 + nghttp3])
elif test "x$OPENSSL_ENABLED" != "x1"; then
AC_MSG_ERROR([--enable-proxy-http3 currently requires OpenSSL])
else
AC_DEFINE(USE_PROXY_HTTP3, 1, [if HTTP/3 proxy support is available])
USE_PROXY_HTTP3=1
AC_MSG_RESULT([yes])
experimental="$experimental PROXY-HTTP3"
fi
fi
dnl ************************************************************
dnl hiding of library internal symbols
dnl
@ -5141,6 +5203,10 @@ if test "$curl_psl_msg" = "enabled"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES PSL"
fi
if test "$USE_PROXY_HTTP3" = "1"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES PROXY-HTTP3"
fi
if test "$curl_gsasl_msg" = "enabled"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES gsasl"
fi
@ -5485,6 +5551,7 @@ AC_MSG_NOTICE([Configured to build curl/libcurl:
HTTP1: ${curl_h1_msg}
HTTP2: ${curl_h2_msg}
HTTP3: ${curl_h3_msg}
Proxy-HTTP3: ${curl_proxy_http3_msg}
ECH: ${curl_ech_msg}
HTTPS RR: ${curl_httpsrr_msg}
SSLS-EXPORT: ${curl_ssls_export_msg}

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

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

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 \

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.21.0
Mutexed: proxy-http2
Requires: HTTP/3
Help: Use HTTP/3 with HTTPS proxy
Category: http proxy
Multi: boolean
See-also:
- proxy
- proxy-http2
Example:
- --proxy-http3 -x proxy $URL
---
# `--proxy-http3`
Negotiate HTTP/3 with an HTTPS proxy.
Fails to perform the transfer if the given proxy does not support HTTP/3.
This has no effect for any other kinds of proxies.
This option is mutually exclusive with `--proxy-http2`.
This feature is experimental and requires a build with HTTP/3 proxy support
enabled. For autotools builds, use `--enable-proxy-http3`. For CMake builds,
use `-DUSE_PROXY_HTTP3=ON`.

View file

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

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.21.0)
## `PSL`
*features* mask bit: CURL_VERSION_PSL

View file

@ -58,7 +58,11 @@ HTTPS Proxy. (with OpenSSL, GnuTLS, mbedTLS, Rustls, Schannel or wolfSSL.)
This uses HTTP/1 by default. Setting CURLOPT_PROXYTYPE(3) to
**CURLPROXY_HTTPS2** allows libcurl to negotiate using HTTP/2 with proxy.
## `socks4://`
Setting CURLOPT_PROXYTYPE(3) to **CURLPROXY_HTTPS3** allows libcurl to
negotiate using HTTP/3 with proxy. This feature is experimental and requires
a build with HTTP/3 proxy support enabled.
## socks4://
SOCKS4 Proxy.

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.21.0)
This feature is experimental and requires a build with HTTP/3 proxy support
enabled.
## CURLPROXY_HTTP_1_0
HTTP 1.0 Proxy. This is similar to CURLPROXY_HTTP except it uses HTTP/1.0 for

View file

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

View file

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

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.21.0 */
typedef enum {
CURLPROXY_LAST = 8 /* never use */
CURLPROXY_LAST = 9 /* never use */
} curl_proxytype; /* this enum was added in 7.10 */
/*
@ -1494,8 +1496,8 @@ typedef enum {
CURLOPT(CURLOPT_SHARE, CURLOPTTYPE_OBJECTPOINT, 100),
/* indicates type of proxy. accepted values are CURLPROXY_HTTP (default),
CURLPROXY_HTTPS, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A and
CURLPROXY_SOCKS5. */
CURLPROXY_HTTPS, CURLPROXY_HTTPS2, CURLPROXY_HTTPS3, CURLPROXY_SOCKS4,
CURLPROXY_SOCKS4A and CURLPROXY_SOCKS5. */
CURLOPT(CURLOPT_PROXYTYPE, CURLOPTTYPE_VALUES, 101),
/* Set the Accept-Encoding string. Use this to tell a server you would like

View file

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

281
lib/capsule.c Normal file
View file

@ -0,0 +1,281 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include <curl/curl.h>
#include "urldata.h"
#include "curlx/dynbuf.h"
#include "cfilters.h"
#include "curl_trc.h"
#include "bufq.h"
#include "capsule.h"
/**
* Convert 64-bit value from network byte order to host byte order
*/
static uint64_t capsule_ntohll(uint64_t value)
{
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
return value;
#elif (defined(__GNUC__) || defined(__clang__)) && \
defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
return __builtin_bswap64(value);
#else
union {
uint64_t u64;
uint32_t u32[2];
} src, dst;
src.u64 = value;
dst.u32[0] = ntohl(src.u32[1]);
dst.u32[1] = ntohl(src.u32[0]);
return dst.u64;
#endif
}
/**
* Encode a variable-length integer into a plain buffer.
* @param buf Output buffer (must have at least 8 bytes)
* @param value Value to encode (must be <= 0x3FFFFFFFFFFFFFFF)
* @return Number of bytes written
*/
static size_t capsule_encode_varint_buf(uint8_t *buf, uint64_t value)
{
DEBUGASSERT(value <= 0x3FFFFFFFFFFFFFFF);
if(value <= 0x3F) {
buf[0] = (uint8_t)value;
return 1;
}
else if(value <= 0x3FFF) {
uint16_t encoded = (uint16_t)value & 0x3FFF;
encoded = ntohs(encoded | 0x4000);
memcpy(buf, &encoded, 2);
return 2;
}
else if(value <= 0x3FFFFFFF) {
uint32_t encoded = (uint32_t)value & 0x3FFFFFFF;
encoded = ntohl(encoded | 0x80000000);
memcpy(buf, &encoded, 4);
return 4;
}
else {
uint64_t encoded = (uint64_t)value & 0x3FFFFFFFFFFFFFFF;
encoded = capsule_ntohll(encoded | 0xC000000000000000);
memcpy(buf, &encoded, 8);
return 8;
}
}
static CURLcode capsule_peek_u8(struct bufq *recvbufq,
size_t offset,
uint8_t *pbyte)
{
const unsigned char *peek = NULL;
size_t peeklen = 0;
if(!Curl_bufq_peek_at(recvbufq, offset, &peek, &peeklen) || !peeklen)
return CURLE_AGAIN;
*pbyte = peek[0];
return CURLE_OK;
}
static CURLcode capsule_decode_varint_at(struct bufq *recvbufq,
size_t offset,
uint64_t *pvalue,
size_t *pconsumed)
{
uint8_t first_byte, byte;
uint64_t value;
size_t nbytes;
size_t i;
CURLcode result;
result = capsule_peek_u8(recvbufq, offset, &first_byte);
if(result)
return result;
nbytes = (size_t)1 << (first_byte >> 6); /* 1, 2, 4 or 8 bytes */
value = first_byte & 0x3F;
for(i = 1; i < nbytes; ++i) {
result = capsule_peek_u8(recvbufq, offset + i, &byte);
if(result)
return result;
value = (value << 8) | byte;
}
*pvalue = value;
*pconsumed = nbytes;
return CURLE_OK;
}
size_t Curl_capsule_encap_udp_hdr(uint8_t *hdr, size_t hdrlen,
size_t payload_len)
{
size_t off = 0;
DEBUGASSERT(hdrlen >= HTTP_CAPSULE_HEADER_MAX_SIZE);
if(hdrlen < HTTP_CAPSULE_HEADER_MAX_SIZE)
return 0;
hdr[off++] = 0; /* capsule type: HTTP Datagram */
off += capsule_encode_varint_buf(hdr + off, (uint64_t)payload_len + 1);
hdr[off++] = 0; /* context ID */
return off;
}
CURLcode Curl_capsule_encap_udp_datagram(struct dynbuf *dyn,
const void *buf, size_t blen)
{
CURLcode result;
uint8_t hdr[HTTP_CAPSULE_HEADER_MAX_SIZE];
size_t hdr_len;
curlx_dyn_init(dyn, HTTP_CAPSULE_HEADER_MAX_SIZE + blen);
hdr_len = Curl_capsule_encap_udp_hdr(hdr, sizeof(hdr), blen);
DEBUGASSERT(hdr_len);
if(!hdr_len)
return CURLE_FAILED_INIT;
result = curlx_dyn_addn(dyn, hdr, hdr_len);
if(result)
return result;
return curlx_dyn_addn(dyn, buf, blen);
}
size_t Curl_capsule_process_udp_raw(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct bufq *recvbufq,
unsigned char *buf, size_t len,
CURLcode *err)
{
const unsigned char *context_id, *capsule_type;
size_t read_size, varint_len;
uint64_t capsule_length;
size_t offset, payload_len;
size_t bytes_read = 0;
CURLcode result = CURLE_OK;
if(!len) {
*err = CURLE_BAD_FUNCTION_ARGUMENT;
return 0;
}
if(Curl_bufq_is_empty(recvbufq)) {
*err = CURLE_AGAIN;
return 0;
}
if(!Curl_bufq_peek(recvbufq, &capsule_type, &read_size) || !read_size) {
*err = CURLE_AGAIN;
return 0;
}
if(capsule_type[0]) {
infof(data, "Error! Invalid capsule type: %d", capsule_type[0]);
Curl_bufq_skip(recvbufq, 1);
*err = CURLE_RECV_ERROR;
return 0;
}
offset = 1;
result = capsule_decode_varint_at(recvbufq, offset, &capsule_length,
&varint_len);
if(result == CURLE_AGAIN) {
*err = CURLE_AGAIN;
return 0;
}
else if(result) {
*err = CURLE_RECV_ERROR;
return 0;
}
offset += varint_len;
if(!Curl_bufq_peek_at(recvbufq, offset, &context_id, &read_size) ||
!read_size) {
*err = CURLE_AGAIN;
return 0;
}
if(*context_id) {
infof(data, "Error! Invalid context ID: %02x", *context_id);
Curl_bufq_skip(recvbufq, offset + 1);
*err = CURLE_RECV_ERROR;
return 0;
}
offset += 1;
if(!capsule_length) {
infof(data, "Error! Invalid capsule length: 0");
Curl_bufq_skip(recvbufq, offset);
*err = CURLE_RECV_ERROR;
return 0;
}
if(capsule_length - 1 >= (uint64_t)SIZE_MAX) {
infof(data, "Error! Capsule length too large: %" CURL_FORMAT_CURL_OFF_T,
(curl_off_t)capsule_length);
*err = CURLE_RECV_ERROR;
return 0;
}
payload_len = (size_t)(capsule_length - 1);
if(Curl_bufq_len(recvbufq) < offset + payload_len) {
*err = CURLE_AGAIN;
return 0;
}
if(payload_len > len) {
infof(data, "UDP payload does not fit destination buffer: %zu > %zu",
payload_len, len);
Curl_bufq_skip(recvbufq, offset + payload_len);
*err = CURLE_RECV_ERROR;
return 0;
}
Curl_bufq_skip(recvbufq, offset);
if(!payload_len) {
*err = CURLE_OK;
return 0;
}
result = Curl_bufq_read(recvbufq, buf, payload_len, &bytes_read);
if(result || (bytes_read != payload_len)) {
infof(data, "Error! Read less than expected %zu %zu",
payload_len, bytes_read);
*err = CURLE_RECV_ERROR;
return 0;
}
if(cf && data) {
CURL_TRC_CF(data, cf, "Processed UDP capsule raw: size=%zu "
"length_left %zu", payload_len, Curl_bufq_len(recvbufq));
}
*err = CURLE_OK;
return bytes_read;
}
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */

77
lib/capsule.h Normal file
View file

@ -0,0 +1,77 @@
#ifndef HEADER_CURL_CAPSULE_H
#define HEADER_CURL_CAPSULE_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include "curlx/dynbuf.h"
#include "bufq.h"
/* HTTP Capsule constants */
#define HTTP_CAPSULE_HEADER_MAX_SIZE 10
/* HTTP Capsule function prototypes */
/**
* Write the capsule header (type + varint length + context ID) into `hdr`.
* @param hdr Output buffer (must be >= HTTP_CAPSULE_HEADER_MAX_SIZE)
* @param hdrlen Size of `hdr` in bytes
* @param payload_len Length of the UDP payload that follows
* @return Number of header bytes written, or 0 on error
*/
size_t Curl_capsule_encap_udp_hdr(uint8_t *hdr, size_t hdrlen,
size_t payload_len);
/**
* Encapsulate UDP payload into HTTP Datagram capsule format
* @param dyn Dynamic buffer to write capsule to
* @param buf Payload buffer
* @param blen Payload buffer length
* @return CURLE_OK on success, error code on failure
*/
CURLcode Curl_capsule_encap_udp_datagram(struct dynbuf *dyn,
const void *buf, size_t blen);
/**
* Process one UDP capsule from buffer into raw datagram payload bytes.
* @param cf Connection filter
* @param data Easy handle
* @param recvbufq Buffer queue containing capsule data
* @param buf Output buffer for one datagram payload
* @param len Size of output buffer in bytes
* @param err Error code output
* @return Number of payload bytes written. Check `err` for status.
*/
size_t Curl_capsule_process_udp_raw(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct bufq *recvbufq,
unsigned char *buf, size_t len,
CURLcode *err);
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#endif /* HEADER_CURL_CAPSULE_H */

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

@ -0,0 +1,253 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include <curl/curl.h>
#include "urldata.h"
#include "cfilters.h"
#include "curl_trc.h"
#include "curlx/dynbuf.h"
#include "bufq.h"
#include "capsule.h"
#include "cf-capsule.h"
/* recv buffer: 4 chunks of 16KB = 64KB, enough for large datagrams */
#define CAPSULE_RECV_CHUNKS 4
#define CAPSULE_CHUNK_SIZE (16 * 1024)
struct cf_capsule_ctx {
struct bufq recvbuf;
struct cf_call_data call_data;
unsigned char *pending; /* unsent capsule bytes from partial write */
size_t pending_len; /* total length of pending buffer */
size_t pending_offset; /* bytes already sent from pending */
size_t pending_payload; /* original payload len for pending capsule */
};
static void capsule_cf_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_capsule_ctx *ctx = cf->ctx;
(void)data;
if(ctx) {
Curl_bufq_free(&ctx->recvbuf);
curlx_free(ctx->pending);
curlx_safefree(ctx);
}
}
static void capsule_cf_close(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_capsule_ctx *ctx = cf->ctx;
CURL_TRC_CF(data, cf, "close");
cf->connected = FALSE;
if(ctx) {
Curl_bufq_reset(&ctx->recvbuf);
curlx_safefree(ctx->pending);
ctx->pending_len = 0;
ctx->pending_offset = 0;
ctx->pending_payload = 0;
}
if(cf->next)
cf->next->cft->do_close(cf->next, data);
}
static CURLcode capsule_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
if(cf->next) {
CURLcode result = cf->next->cft->do_connect(cf->next, data, done);
if(!result && *done)
cf->connected = TRUE;
return result;
}
*done = FALSE;
return CURLE_OK;
}
static CURLcode capsule_cf_send(struct Curl_cfilter *cf,
struct Curl_easy *data,
const uint8_t *buf, size_t len,
bool eos, size_t *pnwritten)
{
struct cf_capsule_ctx *ctx = cf->ctx;
struct dynbuf dyn;
size_t nwritten = 0;
size_t capsule_len;
size_t remaining;
CURLcode result;
(void)eos;
*pnwritten = 0;
if(ctx->pending) {
/* flush remaining bytes from a partially sent capsule */
remaining = ctx->pending_len - ctx->pending_offset;
result = Curl_conn_cf_send(cf->next, data,
ctx->pending + ctx->pending_offset,
remaining, FALSE, &nwritten);
if(result && result != CURLE_AGAIN) {
curlx_safefree(ctx->pending);
return result;
}
ctx->pending_offset += nwritten;
if(ctx->pending_offset < ctx->pending_len)
return CURLE_AGAIN;
/* pending capsule has been fully flusehd */
*pnwritten = ctx->pending_payload;
curlx_safefree(ctx->pending);
return CURLE_OK;
}
/* encapsulate new payload into a capsule */
result = Curl_capsule_encap_udp_datagram(&dyn, buf, len);
if(result) {
curlx_dyn_free(&dyn);
return result;
}
capsule_len = curlx_dyn_len(&dyn);
result = Curl_conn_cf_send(cf->next, data,
(const uint8_t *)curlx_dyn_ptr(&dyn),
capsule_len, FALSE, &nwritten);
if(result && result != CURLE_AGAIN) {
curlx_dyn_free(&dyn);
return result;
}
if(nwritten < capsule_len) {
/* partial or zero write - save unsent capsule bytes as pending */
remaining = capsule_len - nwritten;
ctx->pending = curlx_malloc(remaining);
if(!ctx->pending) {
curlx_dyn_free(&dyn);
return CURLE_OUT_OF_MEMORY;
}
memcpy(ctx->pending,
curlx_dyn_ptr(&dyn) + nwritten, remaining);
ctx->pending_len = remaining;
ctx->pending_offset = 0;
ctx->pending_payload = len;
curlx_dyn_free(&dyn);
return CURLE_AGAIN;
}
/* entire capsule sent */
curlx_dyn_free(&dyn);
*pnwritten = len;
return CURLE_OK;
}
static CURLcode capsule_cf_recv(struct Curl_cfilter *cf,
struct Curl_easy *data,
char *buf, size_t len,
size_t *pnread)
{
struct cf_capsule_ctx *ctx = cf->ctx;
CURLcode result;
size_t nread;
*pnread = 0;
/* fill our receive buffer from the filter below */
while(!Curl_bufq_is_full(&ctx->recvbuf)) {
result = Curl_cf_recv_bufq(cf->next, data, &ctx->recvbuf, 0, &nread);
if(result == CURLE_AGAIN)
break;
if(result)
return result;
if(!nread)
break;
}
/* try to extract a complete capsule datagram */
*pnread = Curl_capsule_process_udp_raw(cf, data, &ctx->recvbuf,
(unsigned char *)buf, len,
&result);
return result;
}
static bool capsule_cf_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
struct cf_capsule_ctx *ctx = cf->ctx;
if(ctx && !Curl_bufq_is_empty(&ctx->recvbuf))
return TRUE;
return cf->next ? cf->next->cft->has_data_pending(cf->next, data) : FALSE;
}
struct Curl_cftype Curl_cft_capsule = {
"CAPSULE",
0,
0,
capsule_cf_destroy,
capsule_cf_connect,
capsule_cf_close,
Curl_cf_def_shutdown,
Curl_cf_def_adjust_pollset,
capsule_cf_data_pending,
capsule_cf_send,
capsule_cf_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
Curl_cf_def_query,
};
CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data)
{
struct Curl_cfilter *cf;
struct cf_capsule_ctx *ctx;
CURLcode result;
(void)data;
ctx = curlx_calloc(1, sizeof(*ctx));
if(!ctx)
return CURLE_OUT_OF_MEMORY;
Curl_bufq_init2(&ctx->recvbuf, CAPSULE_CHUNK_SIZE, CAPSULE_RECV_CHUNKS,
BUFQ_OPT_SOFT_LIMIT);
result = Curl_cf_create(&cf, &Curl_cft_capsule, ctx);
if(result) {
Curl_bufq_free(&ctx->recvbuf);
curlx_free(ctx);
return result;
}
Curl_conn_cf_insert_after(cf_at, cf);
return CURLE_OK;
}
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */

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

@ -0,0 +1,40 @@
#ifndef HEADER_CURL_CF_CAPSULE_H
#define HEADER_CURL_CF_CAPSULE_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
/* Insert a capsule protocol filter after `cf_at` in the filter chain.
* The capsule filter encapsulates/decapsulates UDP datagrams using
* the HTTP Datagram capsule format (RFC 9297). */
CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data);
extern struct Curl_cftype Curl_cft_capsule;
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#endif /* HEADER_CURL_CF_CAPSULE_H */

View file

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

View file

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

View file

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

View file

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

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

File diff suppressed because it is too large Load diff

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

@ -0,0 +1,42 @@
#ifndef HEADER_CURL_H3_PROXY_H
#define HEADER_CURL_H3_PROXY_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY) && \
defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) && \
defined(USE_NGTCP2) && defined(USE_OPENSSL)
CURLcode Curl_cf_h3_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
struct Curl_peer *dest,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_h3_proxy;
#endif
#endif /* HEADER_CURL_H3_PROXY_H */

View file

@ -1023,3 +1023,28 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
Curl_conn_cf_insert_after(cf_at, cf);
return CURLE_OK;
}
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
defined(USE_PROXY_HTTP3)
CURLcode cf_ip_happy_quic_udp_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data)
{
/* For H3 proxy: create happy eyeballs that races IPv4/IPv6 using raw
UDP sockets with TRNSPRT_QUIC transport. Using TRNSPRT_QUIC causes
cf_udp_connect() to call cf_udp_setup_quic() which connects the
socket to the peer address, making send() work without an explicit
destination. We use Curl_cf_udp_create (not Curl_cf_quic_create)
because H3-PROXY manages its own ngtcp2 QUIC stack on top. */
struct Curl_cfilter *cf;
CURLcode result;
DEBUGASSERT(cf_at);
result = cf_ip_happy_create(&cf, data, cf_at->conn,
Curl_cf_udp_create, TRNSPRT_QUIC);
if(result)
return result;
Curl_conn_cf_insert_after(cf_at, cf);
return CURLE_OK;
}
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */

View file

@ -52,6 +52,15 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
uint8_t transport);
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
defined(USE_PROXY_HTTP3)
/* For H3 proxy: create happy eyeballs that races IPv4/IPv6 using raw UDP
sockets with TRNSPRT_QUIC transport so the socket is connected to the
proxy peer. H3-PROXY manages its own ngtcp2 QUIC stack on top. */
CURLcode cf_ip_happy_quic_udp_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data);
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */
extern struct Curl_cftype Curl_cft_ip_happy;
#endif /* HEADER_CURL_IP_HAPPY_H */

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"
@ -341,6 +342,66 @@ struct cf_setup_ctx {
uint8_t transport;
};
#ifndef CURL_DISABLE_PROXY
static CURLcode cf_setup_add_http_proxy(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct cf_setup_ctx *ctx)
{
CURLcode result = CURLE_OK;
#ifndef USE_SSL
(void)cf;
(void)data;
(void)ctx;
#else
/* Skipping the Curl_conn_is_ssl check because SSL is a part of QUIC
For CURLPROXY_HTTPS and CURLPROXY_HTTPS2:
Curl_cft_setup --> Curl_cft_ssl --> Curl_cft_http_proxy --> ...
For CURLPROXY_HTTPS3:
Curl_cft_setup --> Curl_cft_http3 --> Curl_cft_http_proxy --> ... */
if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy) {
if(!IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) {
result = Curl_cf_ssl_proxy_insert_after(cf, data);
if(result)
return result;
}
}
else {
if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype)
&& !Curl_conn_is_ssl(cf->conn, cf->sockindex)
&& !IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) {
result = Curl_cf_ssl_proxy_insert_after(cf, data);
if(result)
return result;
}
}
#endif /* USE_SSL */
#ifndef CURL_DISABLE_HTTP
if(cf->conn->bits.tunnel_proxy) {
struct Curl_peer *dest; /* where HTTP should tunnel to */
bool udp_tun = false;
dest = Curl_conn_get_destination(cf->conn, cf->sockindex);
/* Use CONNECT-UDP only for explicit HTTP/3-only target tunnels.
Do not derive this from proxy transport (for example HTTPS3 proxy). */
if(data->state.http_neg.wanted == CURL_HTTP_V3x) {
#ifdef USE_PROXY_HTTP3
udp_tun = TRUE;
#else
failf(data, "HTTP/3 proxy tunnel support not built-in");
return CURLE_NOT_BUILT_IN;
#endif /* USE_PROXY_HTTP3 */
}
result = Curl_cf_http_proxy_insert_after(cf, data, dest,
cf->conn->http_proxy.proxytype,
udp_tun);
if(result)
return result;
}
#endif /* !CURL_DISABLE_HTTP */
return result;
}
#endif /* !CURL_DISABLE_PROXY */
static CURLcode cf_setup_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
@ -364,7 +425,35 @@ connect_sub_chain:
}
if(ctx->state < CF_SETUP_CNNCT_EYEBALLS) {
result = cf_ip_happy_insert_after(cf, data, ctx->transport);
#ifndef CURL_DISABLE_PROXY
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
defined(USE_PROXY_HTTP3)
if(IS_QUIC_PROXY(cf->conn->http_proxy.proxytype) &&
cf->conn->bits.tunnel_proxy) {
/* For HTTPS3 proxy tunnels, H3-PROXY manages the QUIC connection
on top of the UDP socket. Let happy eyeballs race IPv4/IPv6 using
QUIC-transport UDP sockets so the socket is connected to the
proxy peer and H3-PROXY can send directly via send().
Filter chains:
H1/H2 target (CONNECT over QUIC):
SETUP --> HTTP/1.1 or HTTP/2 --> SSL --> HTTP-PROXY -->
H3-PROXY --> HAPPY-EYEBALLS --> UDP
H3 target (MASQUE CONNECT-UDP over QUIC):
SETUP --> HTTP/3 --> CAPSULE --> HTTP-PROXY -->
H3-PROXY --> HAPPY-EYEBALLS --> UDP */
result = cf_ip_happy_quic_udp_insert_after(cf, data);
}
/* When tunneling QUIC through an HTTP proxy (CONNECT-UDP),
the underlying conn to the proxy is TCP. */
else
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */
if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy
&& !IS_QUIC_PROXY(cf->conn->http_proxy.proxytype))
result = cf_ip_happy_insert_after(cf, data, TRNSPRT_TCP);
else
#endif /* !CURL_DISABLE_PROXY */
result = cf_ip_happy_insert_after(cf, data, ctx->transport);
if(result)
return result;
ctx->state = CF_SETUP_CNNCT_EYEBALLS;
@ -402,25 +491,9 @@ connect_sub_chain:
}
if(ctx->state < CF_SETUP_CNNCT_HTTP_PROXY && cf->conn->bits.httpproxy) {
#ifdef USE_SSL
if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype) &&
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) {
result = Curl_cf_ssl_proxy_insert_after(cf, data);
if(result)
return result;
}
#endif /* USE_SSL */
#ifndef CURL_DISABLE_HTTP
if(cf->conn->bits.tunnel_proxy) {
struct Curl_peer *dest; /* where HTTP should tunnel to */
dest = Curl_conn_get_destination(cf->conn, cf->sockindex);
result = Curl_cf_http_proxy_insert_after(
cf, data, dest, cf->conn->http_proxy.proxytype);
if(result)
return result;
}
#endif /* !CURL_DISABLE_HTTP */
result = cf_setup_add_http_proxy(cf, data, ctx);
if(result)
return result;
ctx->state = CF_SETUP_CNNCT_HTTP_PROXY;
if(!cf->next || !cf->next->connected)
goto connect_sub_chain;
@ -445,21 +518,41 @@ connect_sub_chain:
goto connect_sub_chain;
}
if(ctx->state < CF_SETUP_CNNCT_SSL) {
#ifdef USE_SSL
if((ctx->ssl_mode == CURL_CF_SSL_ENABLE ||
(ctx->ssl_mode != CURL_CF_SSL_DISABLE &&
cf->conn->scheme->flags & PROTOPT_SSL)) && /* we want SSL */
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */
result = Curl_cf_ssl_insert_after(cf, data);
/* Adding Curl_cf_quic_insert_after() because now we
need the next filter to be QUIC/HTTP/3 (which has SSL) */
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
defined(USE_PROXY_HTTP3)
if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy &&
cf->conn->bits.tunnel_proxy &&
(data->state.http_neg.wanted == CURL_HTTP_V3x)) {
if(ctx->state < CF_SETUP_CNNCT_SSL) {
result = Curl_cf_quic_insert_after(cf);
if(result)
return result;
ctx->state = CF_SETUP_CNNCT_SSL;
}
#endif /* USE_SSL */
ctx->state = CF_SETUP_CNNCT_SSL;
if(!cf->next || !cf->next->connected)
goto connect_sub_chain;
}
else
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */
{
if(ctx->state < CF_SETUP_CNNCT_SSL) {
#ifdef USE_SSL
if((ctx->ssl_mode == CURL_CF_SSL_ENABLE ||
(ctx->ssl_mode != CURL_CF_SSL_DISABLE &&
cf->conn->scheme->flags & PROTOPT_SSL)) /* we want SSL */
&& !Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */
result = Curl_cf_ssl_insert_after(cf, data);
if(result)
return result;
}
#endif /* USE_SSL */
ctx->state = CF_SETUP_CNNCT_SSL;
if(!cf->next || !cf->next->connected)
goto connect_sub_chain;
}
}
ctx->state = CF_SETUP_DONE;
cf->connected = TRUE;

View file

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

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_PROXY_HTTP3) && defined(USE_NGHTTP3)
{ &Curl_cft_h3_proxy, TRC_CT_PROXY },
#endif
{ &Curl_cft_http_proxy, TRC_CT_PROXY },
#endif /* !CURL_DISABLE_HTTP */

View file

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

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

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

View file

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

View file

@ -32,14 +32,47 @@
enum Curl_proxy_use {
HEADER_SERVER, /* direct to server */
HEADER_PROXY, /* regular request to proxy */
HEADER_CONNECT /* sending CONNECT to a proxy */
HEADER_CONNECT, /* sending CONNECT to a proxy */
HEADER_CONNECT_UDP /* sending CONNECT-UDP to a proxy */
};
/* HTTP version for proxy tunnel request creation */
typedef enum {
PROXY_HTTP_V1 = 1,
PROXY_HTTP_V2 = 2,
PROXY_HTTP_V3 = 3
} proxy_http_ver;
/* Result from inspecting a proxy tunnel response */
typedef enum {
PROXY_INSPECT_OK, /* Tunnel established */
PROXY_INSPECT_FAILED, /* Tunnel failed */
PROXY_INSPECT_AUTH_RETRY /* Retry with auth */
} proxy_inspect_result;
CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
int httpversion);
proxy_http_ver ver);
CURLcode Curl_http_proxy_create_CONNECTUDP(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
proxy_http_ver ver);
/* Create CONNECT or CONNECT-UDP request */
CURLcode Curl_http_proxy_create_tunnel_request(
struct httpreq **preq, struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_peer *dest,
proxy_http_ver ver, bool udp_tunnel);
/* Inspect tunnel response for H2/H3 proxy (capsule-protocol, auth) */
struct http_resp;
CURLcode Curl_http_proxy_inspect_tunnel_response(
struct Curl_cfilter *cf, struct Curl_easy *data,
struct http_resp *resp, bool udp_tunnel,
proxy_inspect_result *presult);
/* Default proxy timeout in milliseconds */
#define PROXY_TIMEOUT (3600 * 1000)
@ -47,13 +80,17 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
struct Curl_peer *dest,
uint8_t proxytype);
uint8_t proxytype,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_http_proxy;
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#define IS_HTTPS_PROXY(t) (((t) == CURLPROXY_HTTPS) || \
((t) == CURLPROXY_HTTPS2))
((t) == CURLPROXY_HTTPS2) || \
((t) == CURLPROXY_HTTPS3))
#define IS_QUIC_PROXY(t) ((t) == CURLPROXY_HTTPS3)
#endif /* HEADER_CURL_HTTP_PROXY_H */

View file

@ -536,6 +536,37 @@ out:
#define UNIX_SOCKET_PREFIX "localhost"
#endif
CURLcode Curl_scheme_to_proxytype(struct Curl_easy *data,
const char *scheme,
uint8_t *proxytype, const char *url)
{
if(!scheme)
return CURLE_OK;
if(curl_strequal("https", scheme)) {
if(*proxytype != CURLPROXY_HTTPS2 && *proxytype != CURLPROXY_HTTPS3)
*proxytype = CURLPROXY_HTTPS;
}
else if(curl_strequal("socks5h", scheme))
*proxytype = CURLPROXY_SOCKS5_HOSTNAME;
else if(curl_strequal("socks5", scheme))
*proxytype = CURLPROXY_SOCKS5;
else if(curl_strequal("socks4a", scheme))
*proxytype = CURLPROXY_SOCKS4A;
else if(curl_strequal("socks4", scheme) || curl_strequal("socks", scheme))
*proxytype = CURLPROXY_SOCKS4;
else if(curl_strequal("http", scheme)) {
if(*proxytype != CURLPROXY_HTTP_1_0)
*proxytype = CURLPROXY_HTTP;
}
else {
/* Any other xxx:// reject! */
failf(data, "Unsupported proxy scheme for \'%s\'", url);
return CURLE_COULDNT_CONNECT;
}
return CURLE_OK;
}
CURLcode Curl_peer_from_proxy_url(CURLU *uh,
struct Curl_easy *data,
const char *url,
@ -570,6 +601,7 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh,
break;
case CURLPROXY_HTTPS:
case CURLPROXY_HTTPS2:
case CURLPROXY_HTTPS3:
pp.scheme = &Curl_scheme_https;
break;
case CURLPROXY_SOCKS4:
@ -592,29 +624,9 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh,
}
else {
pp.scheme = Curl_get_scheme(scheme);
if(pp.scheme == &Curl_scheme_https) {
proxytype = (proxytype != CURLPROXY_HTTPS2) ?
CURLPROXY_HTTPS : CURLPROXY_HTTPS2;
}
else if(pp.scheme == &Curl_scheme_socks5h)
proxytype = CURLPROXY_SOCKS5_HOSTNAME;
else if(pp.scheme == &Curl_scheme_socks5)
proxytype = CURLPROXY_SOCKS5;
else if(pp.scheme == &Curl_scheme_socks4a)
proxytype = CURLPROXY_SOCKS4A;
else if((pp.scheme == &Curl_scheme_socks4) ||
(pp.scheme == &Curl_scheme_socks))
proxytype = CURLPROXY_SOCKS4;
else if(pp.scheme == &Curl_scheme_http) {
proxytype = (uint8_t)((proxytype != CURLPROXY_HTTP_1_0) ?
CURLPROXY_HTTP : CURLPROXY_HTTP_1_0);
}
else {
/* Any other xxx:// reject! */
failf(data, "Unsupported proxy scheme for \'%s\'", url);
result = CURLE_COULDNT_CONNECT;
result = Curl_scheme_to_proxytype(data, scheme, &proxytype, url);
if(result)
goto out;
}
}
DEBUGASSERT(pp.scheme);

View file

@ -94,6 +94,11 @@ CURLcode Curl_peer_from_connect_to(struct Curl_easy *data,
#ifndef CURL_DISABLE_PROXY
CURLcode Curl_scheme_to_proxytype(struct Curl_easy *data,
const char *scheme,
uint8_t *proxytype,
const char *url);
CURLcode Curl_peer_from_proxy_url(CURLU *uh,
struct Curl_easy *data,
const char *url,

View file

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

View file

@ -99,6 +99,7 @@
#include "headers.h"
#include "curlx/strerr.h"
#include "curlx/strparse.h"
#include "peer.h"
/* Now for the protocols */
#include "ftp.h"
@ -1316,7 +1317,12 @@ static struct connectdata *allocate_conn(struct Curl_easy *data)
#endif
conn->ip_version = data->set.ipver;
conn->bits.connect_only = (bool)data->set.connect_only;
conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */
#ifndef CURL_DISABLE_PROXY
if(conn->http_proxy.proxytype == CURLPROXY_HTTPS3)
conn->transport_wanted = TRNSPRT_QUIC;
else
#endif
conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */
/* Store the local bind parameters that will be used for this connection */
if(data->set.str[STRING_DEVICE]) {
@ -1793,6 +1799,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
{
char *proxyuser = NULL;
char *proxypasswd = NULL;
char *scheme = NULL;
CURLcode result = CURLE_OK;
/* Set the start proxy type for url scheme guessing */
uint8_t proxytype = for_pre_proxy ? CURLPROXY_SOCKS4 : data->set.proxytype;
@ -1807,7 +1814,21 @@ static CURLcode parse_proxy(struct Curl_easy *data,
these made up ones for proxies. Guess scheme for URLs without it. */
uc = curl_url_set(uhp, CURLUPART_URL, proxy,
CURLU_NON_SUPPORT_SCHEME | CURLU_GUESS_SCHEME);
if(uc) {
if(!uc) {
/* parsed okay as a URL - only update proxytype when scheme was explicit */
uc = curl_url_get(uhp, CURLUPART_SCHEME, &scheme, CURLU_NO_GUESS_SCHEME);
if(!uc) {
result = Curl_scheme_to_proxytype(data, scheme, &proxytype, proxy);
if(result)
goto error;
}
else if(uc != CURLUE_NO_SCHEME) {
result = CURLE_OUT_OF_MEMORY;
goto error;
}
/* else: no explicit scheme, keep the configured proxytype */
}
else {
failf(data, "Unsupported proxy syntax in \'%s\': %s", proxy,
curl_url_strerror(uc));
result = CURLE_COULDNT_RESOLVE_PROXY;
@ -1824,6 +1845,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
case CURLPROXY_HTTP_1_0:
case CURLPROXY_HTTPS:
case CURLPROXY_HTTPS2:
case CURLPROXY_HTTPS3:
if(for_pre_proxy) {
failf(data, "Unsupported pre-proxy type for \'%s\'", proxy);
result = CURLE_COULDNT_RESOLVE_PROXY;
@ -1878,6 +1900,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
proxyinfo->proxytype = proxytype;
error:
curlx_free(scheme);
curlx_free(proxyuser);
curlx_free(proxypasswd);
curl_url_cleanup(uhp);

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

View file

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

View file

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

View file

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

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,
@ -310,7 +348,22 @@ CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data,
blen = qctx->split_len;
}
result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent);
if(qctx->sockfd != CURL_SOCKET_BAD) {
/* Direct UDP socket (via happy eyeballs) */
result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent);
}
else {
/* Tunneled QUIC (CONNECT-UDP through proxy) */
if(gsolen && (blen > gsolen)) {
/* Send one datagram at a time to preserve packet boundaries. */
result = send_packet_no_gso_cf(cf, data, buf, blen, gsolen, &sent);
}
else {
/* No GSO aggregate to split, regular lower-filter send is enough. */
result = Curl_conn_cf_send(cf->next, data, buf, blen, FALSE, &sent);
}
}
if(result) {
if(result == CURLE_AGAIN) {
Curl_bufq_skip(&qctx->sendbuf, sent);
@ -699,6 +752,16 @@ CURLcode Curl_qlogdir(struct Curl_easy *data,
return CURLE_OK;
}
CURLcode Curl_cf_quic_insert_after(struct Curl_cfilter *cf_at)
{
#if defined(USE_NGTCP2) && defined(USE_NGHTTP3)
return Curl_cf_ngtcp2_insert_after(cf_at);
#else
(void)cf_at;
return CURLE_NOT_BUILT_IN;
#endif
}
CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
@ -737,10 +800,6 @@ CURLcode Curl_conn_may_http3(struct Curl_easy *data,
failf(data, "HTTP/3 is not supported over a SOCKS proxy");
return CURLE_URL_MALFORMAT;
}
if(conn->bits.httpproxy && conn->bits.tunnel_proxy) {
failf(data, "HTTP/3 is not supported over an HTTP proxy");
return CURLE_URL_MALFORMAT;
}
#endif
return CURLE_OK;

View file

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

View file

@ -3713,8 +3713,11 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
return result;
}
if(data->set.fdebug && data->set.verbose) {
/* the SSL trace callback is only used for verbose logging */
if(data->set.fdebug && data->set.verbose &&
(peer->transport != TRNSPRT_QUIC)) {
/* the SSL trace callback is only used for verbose logging;
* QUIC connections use a different TLS record format that
* ossl_trace cannot handle */
SSL_CTX_set_msg_callback(octx->ssl_ctx, ossl_trace);
SSL_CTX_set_msg_callback_arg(octx->ssl_ctx, cf);
}
@ -4007,12 +4010,20 @@ static CURLcode ossl_connect_step1(struct Curl_cfilter *cf,
{
struct ssl_connect_data *connssl = cf->ctx;
struct ossl_ctx *octx = (struct ossl_ctx *)connssl->backend;
char tls_id[80];
BIO *bio;
CURLcode result;
DEBUGASSERT(ssl_connect_1 == connssl->connecting_state);
DEBUGASSERT(octx);
if(!connssl->peer.dest) {
Curl_ossl_version(tls_id, sizeof(tls_id));
result = Curl_ssl_peer_init(&connssl->peer, cf, tls_id, TRNSPRT_TCP);
if(result)
return result;
}
result = Curl_ossl_ctx_init(octx, cf, data, &connssl->peer,
connssl->alpn, NULL, NULL,
ossl_new_session_cb, cf,

View file

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

View file

@ -133,9 +133,6 @@ struct ssl_connect_data {
BIT(input_pending); /* data for SSL_read() may be available */
};
#undef CF_CTX_CALL_DATA
#define CF_CTX_CALL_DATA(cf) ((struct ssl_connect_data *)(cf)->ctx)->call_data
/* Definitions for SSL Implementations */
struct Curl_ssl {
@ -209,3 +206,9 @@ CURLcode Curl_on_session_reuse(struct Curl_cfilter *cf,
#endif /* USE_SSL */
#endif /* HEADER_CURL_VTLS_INT_H */
#ifdef USE_SSL
/* Restore the default SSL filter call_data accessor for unity builds. */
#undef CF_CTX_CALL_DATA
#define CF_CTX_CALL_DATA(cf) ((struct ssl_connect_data *)(cf)->ctx)->call_data
#endif

View file

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

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,

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 },

View file

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

19
tests/data/test3400 Normal file
View file

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

View file

@ -28,6 +28,12 @@ if(NOT CADDY)
endif()
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 \
@ -72,6 +73,7 @@ EXTRA_DIST = \
test_40_socks.py \
test_50_scp.py \
test_51_sftp.py \
test_60_h3_proxy.py \
$(TESTENV)
clean-local:

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,689 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
# | (__| |_| | _ <| |___
# \___|\___/|_| \_\_____|
#
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
###########################################################################
#
import os
import subprocess
import time
import pytest
from testenv import CurlClient, Env
MARK_NEEDS_HTTPS_PROXY = pytest.mark.skipif(
condition=not Env.curl_has_feature("HTTPS-proxy"),
reason="curl lacks HTTPS-proxy support"
)
MARK_NEEDS_HTTP3 = pytest.mark.skipif(
condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support"
)
MARK_NEEDS_PROXY_HTTP3 = pytest.mark.skipif(
condition=not Env.curl_has_feature("PROXY-HTTP3"),
reason="curl lacks experimental HTTP/3 proxy support"
)
MARK_NEEDS_NGHTTP3 = pytest.mark.skipif(
condition=not Env.curl_uses_lib("nghttp3"), reason="only supported with nghttp3"
)
MARK_NEEDS_NGHTTP2 = pytest.mark.skipif(
condition=not Env.curl_uses_lib("nghttp2"), reason="only supported with nghttp2"
)
MARK_NEEDS_H2O = pytest.mark.skipif(
condition=not Env.have_h2o(), reason="no h2o available"
)
MARK_NEEDS_NGHTTPX = pytest.mark.skipif(
condition=not Env.have_nghttpx(), reason="no nghttpx available"
)
H3_PROXY_COMMON_MARKS = [
MARK_NEEDS_HTTPS_PROXY,
MARK_NEEDS_HTTP3,
MARK_NEEDS_PROXY_HTTP3,
MARK_NEEDS_NGHTTP3,
]
NGTCP2_ONLY_MSG = "only supported with the ngtcp2 quic stack"
UNSUPPORTED_OPT_MSG = "does not support this"
H2O_HELLO_MSG = '"message": "Hello from h2o HTTP/3 server"'
def _require_available(**items):
missing = [name for name, value in items.items() if not value]
if missing:
pytest.skip(f"{' or '.join(missing)} not available")
def _download_path(curl: CurlClient) -> str:
return os.path.join(curl.run_dir, "download_#1.data")
def _check_download_message(curl: CurlClient, expected: str):
dpath = _download_path(curl)
assert os.path.exists(dpath), f"Download file not found: {dpath}"
with open(dpath, "r") as fd:
content = fd.read()
assert expected in content, f"Unexpected response content: {content}"
def _check_download_size(curl: CurlClient, expected_size: int):
dpath = _download_path(curl)
assert os.path.exists(dpath), f"Download file not found: {dpath}"
actual = os.path.getsize(dpath)
assert actual == expected_size, f"expected {expected_size}B download, got {actual}B"
def _nghttpx_proxy_args(
env: Env,
nghttpx,
proxy_proto: str,
tunnel: bool,
insecure: bool = False,
):
xargs = [
"--proxy",
f"https://{env.proxy_domain}:{nghttpx._port}/",
"--resolve",
f"{env.proxy_domain}:{nghttpx._port}:127.0.0.1",
"--proxy-cacert",
env.ca.cert_file,
]
if proxy_proto == "h3":
xargs.append("--proxy-http3")
elif proxy_proto == "h2":
xargs.append("--proxy-http2")
if tunnel:
xargs.append("--proxytunnel")
xargs.extend(["--cacert", env.ca.cert_file, "--proxy-insecure"])
if insecure:
xargs.append("--insecure")
return xargs
def _h2o_proxy_args(
env: Env,
h2o_proxy,
proxy_proto: str,
tunnel: bool,
insecure: bool = False,
):
if proxy_proto == "h3":
pport = h2o_proxy.port
elif proxy_proto == "h2":
pport = h2o_proxy.h2_port
else:
pport = h2o_proxy.h1_port
xargs = [
"--proxy",
f"https://{env.proxy_domain}:{pport}/",
"--resolve",
f"{env.proxy_domain}:{pport}:127.0.0.1",
"--proxy-cacert",
env.ca.cert_file,
]
if proxy_proto == "h2":
xargs.append("--proxy-http2")
elif proxy_proto == "h3":
xargs.append("--proxy-http3")
if tunnel:
xargs.append("--proxytunnel")
xargs.extend(["--cacert", env.ca.cert_file, "--proxy-insecure"])
if insecure:
xargs.append("--insecure")
return xargs
class TestH3ProxySuccess:
"""Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP."""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
@pytest.mark.parametrize(
["alpn_proto", "proxy_proto"],
[
pytest.param("http/1.1", "h3", id="h1_over_h3_proxytunnel"),
pytest.param(
"h2",
"h3",
marks=MARK_NEEDS_NGHTTP2,
id="h2_over_h3_proxytunnel",
),
pytest.param("h3", "h3", id="h3_over_h3_proxytunnel"),
pytest.param(
"h3",
"h2",
marks=MARK_NEEDS_NGHTTP2,
id="h3_over_h2_proxytunnel",
),
pytest.param("h3", "http/1.1", id="h3_over_h1_proxytunnel"),
],
)
def test_60_01_connect_tunnel(
self,
env: Env,
h2o_server,
h2o_proxy,
alpn_proto,
proxy_proto,
):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
curl = CurlClient(env=env)
url = f"https://localhost:{h2o_server.port}/data.json"
proxy_args = _h2o_proxy_args(
env, h2o_proxy, proxy_proto, tunnel=True, insecure=True
)
r = curl.http_download(
urls=[url], alpn_proto=alpn_proto, with_stats=True, extra_args=proxy_args
)
r.check_response(count=1, http_status=200)
_check_download_message(curl, H2O_HELLO_MSG)
class TestH3ProxyFailure:
"""Failure matrix when proxy side does not support requested mode."""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX]
@pytest.mark.parametrize(
["alpn_proto", "proxy_proto", "exp_err"],
[
pytest.param(
"http/1.1",
"h3",
"could not connect to server",
id="fail_h1_over_h3_proxytunnel",
),
pytest.param(
"h2",
"h3",
"could not connect to server",
marks=MARK_NEEDS_NGHTTP2,
id="fail_h2_over_h3_proxytunnel",
),
pytest.param(
"h3",
"h3",
"could not connect to server",
id="fail_h3_over_h3_proxytunnel",
),
pytest.param(
"h3",
"h2",
"connect-udp response status 400",
marks=MARK_NEEDS_NGHTTP2,
id="fail_h3_over_h2_proxytunnel",
),
pytest.param(
"h3",
"http/1.1",
"connect-udp tunnel failed, response 404",
id="fail_h3_over_h1_proxytunnel",
),
],
)
def test_60_02_connect_tunnel_fail(
self,
env: Env,
httpd,
nghttpx,
alpn_proto,
proxy_proto,
exp_err,
):
_require_available(httpd=httpd, nghttpx=nghttpx)
curl = CurlClient(env=env)
url = f"https://localhost:{httpd.ports['https']}/data.json"
proxy_args = _nghttpx_proxy_args(env, nghttpx, proxy_proto, tunnel=True)
r = curl.http_download(
urls=[url], alpn_proto=alpn_proto, with_stats=True, extra_args=proxy_args
)
assert r.exit_code != 0, f"Expected failure but curl succeeded: {r}"
assert exp_err in r.stderr.lower(), (
f"Expected protocol/proxy error but got: {r.stderr}"
)
class TestH3ProxyModeSelection:
"""Behavior checks for tunnel vs non-tunnel proxy mode selection."""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX]
@pytest.mark.parametrize(
["proxy_proto"],
[
pytest.param("h3", id="proxy_h3"),
pytest.param("h2", marks=MARK_NEEDS_NGHTTP2, id="proxy_h2"),
pytest.param("http/1.1", id="proxy_h1"),
],
)
def test_60_03_h3_target_auto_connect_udp(
self, env: Env, httpd, nghttpx, proxy_proto
):
_require_available(httpd=httpd, nghttpx=nghttpx)
curl = CurlClient(env=env)
url = f"https://localhost:{httpd.ports['https']}/data.json"
proxy_args = _nghttpx_proxy_args(
env, nghttpx, proxy_proto, tunnel=False
)
r = curl.http_download(
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
)
# An HTTP/3 target auto-triggers CONNECT-UDP even without --proxytunnel,
# just as HTTPS targets auto-trigger CONNECT. nghttpx does not support
# CONNECT-UDP so this fails, which confirms auto-CONNECT-UDP is active.
assert r.exit_code != 0, (
"expected failure: h3 target auto-triggers CONNECT-UDP "
"which nghttpx does not support"
)
assert "connect-udp" in r.stderr.lower(), (
f"expected CONNECT-UDP attempt in output, got: {r.stderr}"
)
class TestH3ProxyRuntimeGuards:
"""Guard checks for unsupported HTTP/3 proxy options."""
pytestmark = [
MARK_NEEDS_HTTPS_PROXY,
MARK_NEEDS_PROXY_HTTP3,
pytest.mark.skipif(
condition=Env.curl_uses_lib("ngtcp2"),
reason="guard only applies to non-ngtcp2 builds",
),
]
@pytest.mark.skipif(
condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support"
)
def test_60_04_guard_proxy_http3_unsupported(self, env: Env, httpd):
curl = CurlClient(env=env)
url = f"https://localhost:{httpd.ports['https']}/data.json"
proxy_args = [
"--proxy",
"https://127.0.0.1:1/",
"--proxy-http3",
"--proxytunnel",
"--proxy-insecure",
"--cacert",
env.ca.cert_file,
]
r = curl.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
if not env.curl_has_feature("PROXY-HTTP3"):
r.check_exit_code(2)
assert UNSUPPORTED_OPT_MSG in r.stderr.lower(), (
f"Expected unsupported option failure but got: {r.stderr}"
)
return
r.check_exit_code(1)
assert NGTCP2_ONLY_MSG in r.stderr.lower(), (
f"Expected ngtcp2 guard failure but got: {r.stderr}"
)
class TestH3ProxyRobustness:
"""Robustness checks for shutdown and proxy loss during transfer."""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
@pytest.fixture(autouse=True, scope="class")
def _class_scope(self, env):
doc_root = os.path.join(env.gen_dir, "docs")
env.make_data_file(
indir=doc_root, fname="proxy-drop-20m", fsize=20 * 1024 * 1024
)
def test_60_05_graceful_shutdown(
self, env: Env, h2o_server, h2o_proxy
):
if not env.curl_is_debug():
pytest.skip("needs debug curl for shutdown trace lines")
if not env.curl_is_verbose():
pytest.skip("needs verbose-strings curl build")
curl = CurlClient(env=env, run_env={"CURL_DEBUG": "all"})
url = f"https://localhost:{h2o_server.port}/data.json"
proxy_args = curl.get_proxy_args(proto="h3", tunnel=True)
proxy_args.extend(["--cacert", env.ca.cert_file, "--insecure"])
r = curl.http_download(
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
)
r.check_response(count=1, http_status=200)
shutdown_lines = [
line
for line in r.trace_lines
if ("start shutdown(" in line.lower())
or ("shutdown completely sent off" in line.lower())
]
assert shutdown_lines, f"No shutdown trace lines found:\n{r.stderr}"
def test_60_06_proxy_drop_mid_transfer(self, env: Env, h2o_server, h2o_proxy):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
proxy_port = h2o_proxy.port
url = f"https://localhost:{h2o_server.port}/proxy-drop-20m"
out_path = os.path.join(env.gen_dir, "proxy-drop.out")
args = [
env.curl,
"--http1.1",
"--proxy",
f"https://{env.proxy_domain}:{proxy_port}/",
"--resolve",
f"{env.proxy_domain}:{proxy_port}:127.0.0.1",
"--proxy-cacert",
env.ca.cert_file,
"--proxy-http3",
"--proxytunnel",
"--proxy-insecure",
"--cacert",
env.ca.cert_file,
"--limit-rate",
"100k",
"--max-time",
"20",
"-o",
out_path,
url,
]
proc = None
try:
proc = subprocess.Popen(
args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
time.sleep(1.0)
assert h2o_proxy.stop(), "failed to stop h2o proxy"
_, stderr = proc.communicate(timeout=30)
assert proc.returncode != 0, (
"curl should fail when proxy is terminated mid-transfer"
)
serr = stderr.lower()
assert (
"failed" in serr
or "transfer closed" in serr
or "recv failure" in serr
or "connection" in serr
), f"Unexpected error output: {stderr}"
finally:
if proc and (proc.poll() is None):
proc.kill()
proc.wait(timeout=5)
assert h2o_proxy.start(), "failed to restart h2o proxy"
class TestH3ProxyDataTransfer:
"""Large file transfers and multiplexing through HTTP/3 proxy."""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
@pytest.fixture(autouse=True, scope="class")
def _class_scope(self, env):
doc_root = os.path.join(env.gen_dir, "docs")
env.make_data_file(indir=doc_root, fname="download-1m", fsize=1 * 1024 * 1024)
env.make_data_file(indir=doc_root, fname="download-10m", fsize=10 * 1024 * 1024)
env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024)
def test_60_07_large_download(self, env: Env, h2o_server, h2o_proxy):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
curl = CurlClient(env=env)
url = f"https://localhost:{h2o_server.port}/download-10m"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
r = curl.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r.check_response(count=1, http_status=200)
_check_download_size(curl, 10 * 1024 * 1024)
def test_60_08_large_upload(self, env: Env, httpd, h2o_server, h2o_proxy):
_require_available(h2o_proxy=h2o_proxy)
fdata = os.path.join(env.gen_dir, "upload-2m")
curl = CurlClient(env=env)
url = f"https://localhost:{httpd.ports['https']}/curltest/echo?id=[0-0]"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
r = curl.http_upload(
urls=[url],
data=f"@{fdata}",
alpn_proto="http/1.1",
with_stats=True,
extra_args=proxy_args,
)
r.check_response(count=1, http_status=200)
def test_60_09_parallel_downloads(self, env: Env, h2o_server, h2o_proxy):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
count = 5
curl = CurlClient(env=env)
urln = f"https://localhost:{h2o_server.port}/download-1m?[0-{count - 1}]"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
proxy_args.extend(["--parallel", "--parallel-max", f"{count}"])
r = curl.http_download(
urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r.check_response(count=count, http_status=200)
class TestH3ProxyConnectionManagement:
"""Proxy authentication, connection reuse, and session resumption."""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
def test_60_10_proxy_basic_auth(self, env: Env, h2o_server, h2o_proxy):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
curl = CurlClient(env=env)
url = f"https://localhost:{h2o_server.port}/data.json"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
proxy_args.extend(["--proxy-user", "testuser:testpass"])
r = curl.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r.check_response(count=1, http_status=200)
_check_download_message(curl, H2O_HELLO_MSG)
def test_60_11_connection_reuse(self, env: Env, h2o_server, h2o_proxy):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
curl = CurlClient(env=env)
urln = f"https://localhost:{h2o_server.port}/data.json?[0-2]"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
r = curl.http_download(
urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r.check_response(count=3, http_status=200)
assert r.total_connects <= 3, (
f"expected proxy connection reuse, got {r.total_connects} connects"
)
def test_60_12_quic_session_resumption(self, env: Env, h2o_server, h2o_proxy):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
# First request establishes QUIC session
curl1 = CurlClient(env=env)
url = f"https://localhost:{h2o_server.port}/data.json"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
r1 = curl1.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r1.check_response(count=1, http_status=200)
# Second request from a fresh CurlClient; session may be reused
# by the TLS session cache if supported
curl2 = CurlClient(env=env)
r2 = curl2.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r2.check_response(count=1, http_status=200)
# Third request from a fresh CurlClient; session may be reused
# by the TLS session cache if supported
curl3 = CurlClient(env=env)
r3 = curl3.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r3.check_response(count=1, http_status=200)
class TestH3ProxyUdpTunnel:
"""CONNECT-UDP tunnel payload size and capsule-protocol tests."""
pytestmark = H3_PROXY_COMMON_MARKS
@pytest.fixture(autouse=True, scope="class")
def _class_scope(self, env):
doc_root = os.path.join(env.gen_dir, "docs")
env.make_data_file(indir=doc_root, fname="download-1400", fsize=1400)
env.make_data_file(indir=doc_root, fname="download-1m", fsize=1 * 1024 * 1024)
env.make_data_file(indir=doc_root, fname="download-10m", fsize=10 * 1024 * 1024)
@MARK_NEEDS_H2O
@pytest.mark.parametrize(
"fname,fsize",
[
("download-1400", 1400),
("download-1m", 1 * 1024 * 1024),
("download-10m", 10 * 1024 * 1024),
],
)
def test_60_13_udp_tunnel_payload_sizes(
self, env: Env, h2o_server, h2o_proxy, fname, fsize
):
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
curl = CurlClient(env=env)
url = f"https://localhost:{h2o_server.port}/{fname}"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
r = curl.http_download(
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
)
r.check_response(count=1, http_status=200)
_check_download_size(curl, fsize)
@MARK_NEEDS_NGHTTPX
def test_60_14_udp_tunnel_capsule_absent(self, env: Env, httpd, nghttpx):
_require_available(httpd=httpd, nghttpx=nghttpx)
curl = CurlClient(env=env)
url = f"https://localhost:{httpd.ports['https']}/data.json"
proxy_args = _nghttpx_proxy_args(env, nghttpx, "h3", tunnel=True)
r = curl.http_download(
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
)
assert r.exit_code != 0, (
"expected failure: nghttpx does not support CONNECT-UDP / Capsule-Protocol"
)
class TestH3ProxyEdgeCases:
"""Timeout and protocol-mismatch edge cases."""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
def test_60_15_connect_timeout(self, env: Env, h2o_server):
_require_available(h2o_server=h2o_server)
curl = CurlClient(env=env, timeout=15)
url = f"https://localhost:{h2o_server.port}/data.json"
proxy_args = [
"--proxy",
"https://192.0.2.1:1/",
"--proxy-http3",
"--proxytunnel",
"--proxy-insecure",
"--connect-timeout",
"3",
"--cacert",
env.ca.cert_file,
]
r = curl.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
assert r.exit_code != 0, "expected timeout connecting to unreachable proxy"
assert r.duration.total_seconds() < 10, (
f"timeout not respected: took {r.duration.total_seconds():.1f}s"
)
@MARK_NEEDS_NGHTTP2
def test_60_16_h2_uses_connect_tcp_not_udp(self, env: Env, httpd, h2o_proxy):
_require_available(httpd=httpd, h2o_proxy=h2o_proxy)
curl = CurlClient(env=env)
url = f"https://localhost:{httpd.ports['https']}/data.json"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
# h2 inner traffic always uses CONNECT (TCP), never CONNECT-UDP,
# even through an HTTP/3 proxy with --proxytunnel. h2o supports
# CONNECT TCP tunneling, so this request succeeds.
r = curl.http_download(
urls=[url], alpn_proto="h2", with_stats=True, extra_args=proxy_args
)
r.check_response(count=1, http_status=200)
class TestH3ProxyHappyEyeballs:
"""
Verify that happy eyeballs is active for HTTP/3 proxy connections.
With the H3-PROXY filter sitting above HAPPY-EYEBALLS -> UDP, address
family selection to the proxy is done by happy eyeballs.
"""
pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
def test_60_17_h3_proxy_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy):
"""Verbose trace confirms HAPPY-EYEBALLS filter is in the H3 proxy chain."""
if not env.curl_is_debug():
pytest.skip("needs debug curl for filter trace")
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
curl = CurlClient(env=env, run_env={"CURL_DEBUG": "HAPPY-EYEBALLS,H3-PROXY"})
url = f"https://localhost:{h2o_server.port}/data.json"
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True)
r = curl.http_download(
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
)
r.check_response(count=1, http_status=200)
assert "happy-eyeballs" in r.stderr.lower(), (
f"expected HAPPY-EYEBALLS trace for H3 proxy, got: {r.stderr}"
)
@MARK_NEEDS_NGHTTP2
def test_60_18_h3_proxy_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy):
"""IPv4-forced H3 proxy works for h1/h2/h3 inner protocols."""
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
for alpn_proto in ["http/1.1", "h2", "h3"]:
curl = CurlClient(env=env)
url = f"https://localhost:{h2o_server.port}/data.json"
proxy_args = _h2o_proxy_args(
env, h2o_proxy, "h3", tunnel=True, insecure=True
)
proxy_args.append("--ipv4")
r = curl.http_download(
urls=[url],
alpn_proto=alpn_proto,
with_stats=True,
extra_args=proxy_args,
)
r.check_response(count=1, http_status=200)

View file

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

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:
@ -528,32 +596,31 @@ class Env:
@staticmethod
def curl_can_h3_early_data() -> bool:
return Env.curl_can_early_data() and \
Env.curl_uses_lib('ngtcp2')
return Env.curl_can_early_data() and Env.curl_uses_lib("ngtcp2")
@staticmethod
def http_protos() -> List[str]:
# http protocols we can test
if Env.have_h2_curl():
if Env.have_h3():
return ['http/1.1', 'h2', 'h3']
return ['http/1.1', 'h2']
return ['http/1.1']
return ["http/1.1", "h2", "h3"]
return ["http/1.1", "h2"]
return ["http/1.1"]
@staticmethod
def http_h1_h2_protos() -> List[str]:
# http 1+2 protocols we can test
if Env.have_h2_curl():
return ['http/1.1', 'h2']
return ['http/1.1']
return ["http/1.1", "h2"]
return ["http/1.1"]
@staticmethod
def http_mplx_protos() -> List[str]:
# http multiplexing protocols we can test
if Env.have_h2_curl():
if Env.have_h3():
return ['h2', 'h3']
return ['h2']
return ["h2", "h3"]
return ["h2"]
return []
@staticmethod
@ -572,6 +639,10 @@ class Env:
def caddy_version() -> str:
return Env.CONFIG.caddy_version
@staticmethod
def h2o_version() -> str:
return Env.CONFIG.h2o_version
@staticmethod
def caddy_is_at_least(minv) -> bool:
return Env.CONFIG.caddy_is_at_least(minv)
@ -611,21 +682,20 @@ class Env:
def __init__(self, pytestconfig=None, env_config=None):
if env_config:
Env.CONFIG = env_config
self._verbose = pytestconfig.option.verbose \
if pytestconfig is not None else 0
self._verbose = pytestconfig.option.verbose if pytestconfig is not None else 0
self._ca = None
self._test_timeout = 300.0 if self._verbose > 1 else 60.0 # seconds
def issue_certs(self):
if self._ca is None:
# ca_dir = os.path.join(self.CONFIG.gen_root, 'ca')
ca_dir = os.path.join(self.gen_dir, 'ca')
ca_dir = os.path.join(self.gen_dir, "ca")
os.makedirs(ca_dir, exist_ok=True)
lock_file = os.path.join(ca_dir, 'ca.lock')
lock_file = os.path.join(ca_dir, "ca.lock")
with FileLock(lock_file):
self._ca = TestCA.create_root(name=self.CONFIG.tld,
store_dir=ca_dir,
key_type="rsa2048")
self._ca = TestCA.create_root(
name=self.CONFIG.tld, store_dir=ca_dir, key_type="rsa2048"
)
self._ca.issue_certs(self.CONFIG.cert_specs)
if self.have_openssl():
self._ca.create_hashdir(self.openssl)
@ -714,19 +784,19 @@ class Env:
@property
def http_port(self) -> int:
return self.CONFIG.ports.get('http', 0)
return self.CONFIG.ports.get("http", 0)
@property
def https_port(self) -> int:
return self.CONFIG.ports['https']
return self.CONFIG.ports["https"]
@property
def https_only_tcp_port(self) -> int:
return self.CONFIG.ports['https-tcp-only']
return self.CONFIG.ports["https-tcp-only"]
@property
def nghttpx_https_port(self) -> int:
return self.CONFIG.ports['nghttpx_https']
return self.CONFIG.ports["nghttpx_https"]
@property
def h3_port(self) -> int:
@ -734,27 +804,35 @@ class Env:
@property
def proxy_port(self) -> int:
return self.CONFIG.ports['proxy']
return self.CONFIG.ports["proxy"]
@property
def proxys_port(self) -> int:
return self.CONFIG.ports['proxys']
return self.CONFIG.ports["proxys"]
@property
def ftp_port(self) -> int:
return self.CONFIG.ports['ftp']
return self.CONFIG.ports["ftp"]
@property
def ftps_port(self) -> int:
return self.CONFIG.ports['ftps']
return self.CONFIG.ports["ftps"]
@property
def h2proxys_port(self) -> int:
return self.CONFIG.ports['h2proxys']
return self.CONFIG.ports["h2proxys"]
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

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

@ -0,0 +1,428 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
# | (__| |_| | _ <| |___
# \___|\___/|_| \_\_____|
#
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
###########################################################################
#
import logging
import os
import signal
import socket
import subprocess
import time
from datetime import datetime, timedelta
from typing import Dict, Optional
from .curl import CurlClient
from .env import Env
from .ports import alloc_ports_and_do
log = logging.getLogger(__name__)
class H2o:
def __init__(self, env: Env, name: str, domain: str, cred_name: str):
self.env = env
self._name = name
self._domain = domain
self._port = 0 # defaults to h3_port
self._cred_name = cred_name
self._loaded_cred_name = None
self._process = None
self._tmp_dir = os.path.join(self.env.gen_dir, self._name)
self._run_dir = os.path.join(self._tmp_dir, "run")
self._conf_file = os.path.join(self._run_dir, "h2o.conf")
self._error_log = os.path.join(self._run_dir, "h2o.log")
self._pid_file = os.path.join(self._run_dir, "h2o.pid")
self._stderr = os.path.join(self._run_dir, "h2o.stderr")
self._cmd = env.CONFIG.h2o
# For proxy subclasses
self._h1_port = None
self._h2_port = None
@property
def port(self) -> int:
return self._port
@property
def h1_port(self) -> Optional[int]:
return getattr(self, "_h1_port", None)
@property
def h2_port(self) -> Optional[int]:
return getattr(self, "_h2_port", None)
def clear_logs(self):
self._rmf(self._error_log)
self._rmf(self._stderr)
def dump_logs(self):
lines = []
lines.append(f"stderr of {self._name}")
lines.append("-------------------------------------------")
self._dump_file(self._stderr, lines)
lines.append("")
lines.append(f"errorlog of {self._name}")
lines.append("-------------------------------------------")
self._dump_file(self._error_log, lines)
lines.append("")
return lines
def _rmf(self, path):
if os.path.isfile(path):
os.remove(path)
return
def _dump_file(self, path, lines):
if os.path.isfile(path):
with open(path) as fd:
for line in fd:
lines.append(line.rstrip())
def _mkpath(self, path):
if not os.path.exists(path):
os.makedirs(path)
return
def _log(self, level, msg):
getattr(log, level)(f"[{self._name}] {msg}")
def is_running(self):
if self._process:
self._process.poll()
return self._process.returncode is None
return False
def initial_start(self):
self._rmf(self._pid_file)
self._rmf(self._error_log)
self._mkpath(self._run_dir)
self.write_config()
def start(self, wait_live=True):
self._mkpath(self._tmp_dir)
self._mkpath(self._run_dir)
if self._process:
self.stop()
self._loaded_cred_name = self._cred_name
self.write_config()
args = [self._cmd, "-c", self._conf_file]
ngerr = open(self._stderr, "a")
self._process = subprocess.Popen(args=args, stderr=ngerr)
if self._process.returncode is not None:
return False
if wait_live:
time.sleep(1)
# fail fast if h2o rejected the config and already exited
self._process.poll()
if self._process.returncode is not None:
self._log("error",
f"h2o exited early (rc={self._process.returncode})"
f" - check {self._stderr} for details")
self._process = None
return False
return not wait_live or self.wait_for_state(
live=True, timeout=timedelta(seconds=Env.SERVER_TIMEOUT)
)
def stop(self, wait_dead=True):
self._mkpath(self._tmp_dir)
if self._process:
self._process.terminate()
try:
self._process.wait(timeout=5)
except subprocess.TimeoutExpired:
self._process.kill()
self._process.wait(timeout=2)
self._process = None
return not wait_dead or self.wait_for_state(
live=False, timeout=timedelta(seconds=5)
)
return True
def restart(self):
self.stop()
return self.start()
def reload(self, timeout: timedelta = timedelta(seconds=Env.SERVER_TIMEOUT)):
if self._process:
running = self._process
self._process = None
os.kill(running.pid, signal.SIGQUIT)
end_wait = datetime.now() + timedelta(seconds=5)
exited = False
if not self.start(wait_live=False):
self._process = running
return False
while datetime.now() < end_wait:
try:
self._log("debug", f"waiting for h2o({running.pid}) to exit.")
running.wait(1)
self._log(
"debug",
f"h2o({running.pid}) terminated -> {running.returncode}",
)
exited = True
break
except subprocess.TimeoutExpired:
self._log("warning", f"h2o({running.pid}), not shut down yet.")
os.kill(running.pid, signal.SIGQUIT)
if not exited and datetime.now() >= end_wait:
self._log("error", f"h2o({running.pid}), terminate forcefully.")
os.kill(running.pid, signal.SIGKILL)
running.terminate()
running.wait(1)
return self.wait_for_state(live=True, timeout=timeout)
return False
def wait_for_state(
self,
live: bool,
timeout: timedelta,
url: Optional[str] = None,
log_prefix: str = "h2o",
):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout
if url is None:
url = f"https://{self._domain}:{self._port}/"
while datetime.now() < try_until:
if live:
r = curl.http_get(
url=url, extra_args=["--trace", "curl.trace", "--trace-time"]
)
if r.exit_code == 0:
return True
else:
r = curl.http_get(url=url)
if r.exit_code != 0:
return True
time.sleep(0.1)
if live:
self._log("error", f"Server still not responding after {timeout}")
else:
self._log("debug", f"Server still responding after {timeout}")
return False
def write_config(self):
# To be overridden by subclasses
with open(self._conf_file, "w") as fd:
fd.write("# h2o test config\n")
class H2oServer(H2o):
"""h2o HTTP/3 server for testing."""
PORT_SPECS = {
"h2o_https": socket.SOCK_STREAM,
}
def __init__(self, env: Env):
super().__init__(
env=env, name="h2o-server", domain=env.domain1, cred_name=env.domain1
)
def initial_start(self):
super().initial_start()
def startup(ports: Dict[str, int]) -> bool:
self._port = ports["h2o_https"]
if self.start():
self.env.update_ports(ports)
return True
self.stop()
self._port = 0
return False
return alloc_ports_and_do(
H2oServer.PORT_SPECS, startup, self.env.gen_root, max_tries=3
)
def write_config(self):
creds = self.env.get_credentials(self._cred_name)
assert creds # convince pytype this is not None
doc_root = os.path.join(self.env.gen_dir, "docs")
self._mkpath(doc_root)
self._mkpath(self._run_dir)
# Create a simple test file
with open(os.path.join(doc_root, "data.json"), "w") as f:
f.write('{"message": "Hello from h2o HTTP/3 server"}\n')
with open(self._conf_file, "w") as fd:
fd.write(f"""# h2o HTTP/3 server configuration
server-name: "h2o-test-server"
num-threads: 1
listen: &ssl_listen
port: {self._port}
ssl:
certificate-file: {creds.cert_file}
key-file: {creds.pkey_file}
neverbleed: OFF
minimum-version: TLSv1.2
ocsp-update-interval: 0
listen:
<<: *ssl_listen
type: quic
hosts:
"{self._domain}":
paths:
"/":
file.dir: {doc_root}
http2-reprioritize-blocking-assets: ON
access-log: {self._run_dir}/access.log
error-log: {self._error_log}
""")
class H2oProxy(H2o):
"""h2o MASQUE proxy for testing."""
def __init__(self, env: Env):
super().__init__(
env=env,
name="h2o-proxy",
domain=env.proxy_domain,
cred_name=env.proxy_domain,
)
def initial_start(self):
super().initial_start()
def startup(ports: Dict[str, int]) -> bool:
self._port = ports["h3proxys"]
self._h2_port = ports["h2proxys"]
self._h1_port = ports["proxys"]
if self.start():
self.env.update_ports(ports)
return True
self.stop()
self._port = 0
self._h2_port = 0
self._h1_port = 0
return False
return alloc_ports_and_do(
{
"h3proxys": socket.SOCK_DGRAM,
"h2proxys": socket.SOCK_STREAM,
"proxys": socket.SOCK_STREAM,
},
startup,
self.env.gen_root,
max_tries=3,
)
def write_config(self):
creds = self.env.get_credentials(self._cred_name)
assert creds # convince pytype this is not None
self._mkpath(self._run_dir)
with open(self._conf_file, "w") as fd:
fd.write(f"""# h2o MASQUE proxy configuration
server-name: "h2o-test-proxy"
num-threads: 1
proxy.tunnel: ON
# HTTP/1.1 proxy listener
listen: &h1_listen
port: {getattr(self, "_h1_port", self._port)}
ssl:
certificate-file: {creds.cert_file}
key-file: {creds.pkey_file}
neverbleed: OFF
minimum-version: TLSv1.2
ocsp-update-interval: 0
# HTTP/2 proxy listener
listen: &h2_listen
port: {getattr(self, "_h2_port", self._port)}
ssl:
certificate-file: {creds.cert_file}
key-file: {creds.pkey_file}
neverbleed: OFF
minimum-version: TLSv1.2
ocsp-update-interval: 0
# HTTP/3 proxy listener (main port)
listen: &h3_listen
port: {self._port}
ssl:
certificate-file: {creds.cert_file}
key-file: {creds.pkey_file}
neverbleed: OFF
minimum-version: TLSv1.2
ocsp-update-interval: 0
# QUIC listener for HTTP/3
listen:
<<: *h3_listen
type: quic
hosts:
"{self._domain}":
paths:
"/":
proxy.connect: [+*]
proxy.ssl.verify-peer: OFF
"/.well-known/masque/udp":
proxy.connect-udp: [+*]
proxy.ssl.verify-peer: OFF
http2-reprioritize-blocking-assets: ON
access-log: {self._run_dir}/access.log
error-log: {self._error_log}
""")
def wait_for_state(
self,
live: bool,
timeout: timedelta,
url: Optional[str] = None,
log_prefix: str = "h2o",
):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout
if url is None:
url = f"https://{self.env.proxy_domain}:{self._port}/"
while datetime.now() < try_until:
if live:
r = curl.http_get(
url=url, extra_args=["--trace", "curl.trace", "--trace-time"]
)
if r.exit_code == 0:
return True
else:
r = curl.http_get(url=url)
if r.exit_code != 0:
return True
time.sleep(0.1)
if live:
self._log("error", f"Proxy still not responding after {timeout}")
else:
self._log("debug", f"Proxy still responding after {timeout}")
return False

View file

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

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

@ -0,0 +1,268 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "unitcheck.h"
#include "bufq.h"
#include "capsule.h"
#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \
!defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
static void queue_bytes(struct bufq *q, const unsigned char *src, size_t len)
{
size_t nwritten = 0;
CURLcode result = Curl_bufq_write(q, src, len, &nwritten);
fail_unless(result == CURLE_OK, "queue failed");
fail_unless(nwritten == len, "queue short write");
}
#endif
#if defined(USE_PROXY_HTTP3) && \
!defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
static void check_capsule_hdr(size_t payload_len,
const unsigned char *expected,
size_t expected_len)
{
unsigned char hdr[HTTP_CAPSULE_HEADER_MAX_SIZE];
size_t hdr_len;
memset(hdr, 0xA5, sizeof(hdr));
hdr_len = Curl_capsule_encap_udp_hdr(hdr, sizeof(hdr), payload_len);
fail_unless(hdr_len == expected_len, "capsule header length mismatch");
fail_unless(!memcmp(hdr, expected, expected_len),
"capsule header bytes mismatch");
}
static void test_capsule_encap_udp_hdr_boundaries(void)
{
const unsigned char p0[] = { 0x00, 0x01, 0x00 };
const unsigned char p62[] = { 0x00, 0x3F, 0x00 };
const unsigned char p63[] = { 0x00, 0x40, 0x40, 0x00 };
const unsigned char p64[] = { 0x00, 0x40, 0x41, 0x00 };
const unsigned char p16382[] = { 0x00, 0x7F, 0xFF, 0x00 };
const unsigned char p16383[] = { 0x00, 0x80, 0x00, 0x40, 0x00, 0x00 };
const unsigned char p16384[] = { 0x00, 0x80, 0x00, 0x40, 0x01, 0x00 };
check_capsule_hdr(0, p0, sizeof(p0));
check_capsule_hdr(62, p62, sizeof(p62));
check_capsule_hdr(63, p63, sizeof(p63));
check_capsule_hdr(64, p64, sizeof(p64));
check_capsule_hdr(16382, p16382, sizeof(p16382));
check_capsule_hdr(16383, p16383, sizeof(p16383));
check_capsule_hdr(16384, p16384, sizeof(p16384));
}
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \
!defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
static void check_capsule_result(struct bufq *q,
const unsigned char *capsule, size_t capslen,
size_t outlen, CURLcode expect_err,
size_t expect_nread)
{
unsigned char out[32];
CURLcode err = CURLE_OK;
size_t nread;
memset(out, 0, sizeof(out));
Curl_bufq_reset(q);
if(capsule && capslen)
queue_bytes(q, capsule, capslen);
nread = Curl_capsule_process_udp_raw(NULL, NULL, q, out, outlen, &err);
fail_unless(err == expect_err, "unexpected capsule error");
fail_unless(nread == expect_nread, "unexpected capsule read size");
}
static void test_capsule_encode_decode_roundtrip(void)
{
struct dynbuf dyn;
struct bufq q;
unsigned char payload[128];
unsigned char out[128];
CURLcode result, err;
size_t payload_len;
size_t i, nread;
for(i = 0; i < sizeof(payload); ++i)
payload[i] = (unsigned char)i;
for(i = 0; i < 2; ++i) {
payload_len = i ? 64 : 7;
memset(out, 0, sizeof(out));
result = Curl_capsule_encap_udp_datagram(&dyn, payload, payload_len);
fail_unless(result == CURLE_OK, "failed to encapsulate UDP datagram");
Curl_bufq_init2(&q, 32, 8, BUFQ_OPT_NONE);
queue_bytes(&q, (const unsigned char *)curlx_dyn_ptr(&dyn),
curlx_dyn_len(&dyn));
err = CURLE_OK;
nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out),
&err);
fail_unless(err == CURLE_OK, "failed to decode UDP datagram");
fail_unless(nread == payload_len, "decoded payload length mismatch");
fail_unless(!memcmp(out, payload, payload_len),
"decoded payload bytes mismatch");
fail_unless(Curl_bufq_is_empty(&q), "decoded capsule must be consumed");
Curl_bufq_free(&q);
curlx_dyn_free(&dyn);
}
}
static void test_capsule_sequential_decode(void)
{
/* Verify that multiple back-to-back capsules in the same bufq are
each decoded in turn and the buffer is fully consumed. */
struct bufq q;
unsigned char out[8];
CURLcode err;
size_t nread;
/* Two back-to-back 3-byte UDP capsules */
const unsigned char two_caps[] = {
0x00, 0x04, 0x00, 0x11, 0x22, 0x33, /* capsule 1: [0x11,0x22,0x33] */
0x00, 0x04, 0x00, 0xAA, 0xBB, 0xCC /* capsule 2: [0xAA,0xBB,0xCC] */
};
Curl_bufq_init2(&q, 32, 4, BUFQ_OPT_NONE);
queue_bytes(&q, two_caps, sizeof(two_caps));
/* First capsule */
memset(out, 0, sizeof(out));
err = CURLE_OK;
nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
fail_unless(err == CURLE_OK, "sequential: first capsule decode failed");
fail_unless(nread == 3, "sequential: first capsule size mismatch");
fail_unless(out[0] == 0x11 && out[1] == 0x22 && out[2] == 0x33,
"sequential: first capsule bytes mismatch");
/* Second capsule */
memset(out, 0, sizeof(out));
err = CURLE_OK;
nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
fail_unless(err == CURLE_OK, "sequential: second capsule decode failed");
fail_unless(nread == 3, "sequential: second capsule size mismatch");
fail_unless(out[0] == 0xAA && out[1] == 0xBB && out[2] == 0xCC,
"sequential: second capsule bytes mismatch");
/* Buffer must be empty after both capsules */
fail_unless(Curl_bufq_is_empty(&q),
"sequential: buffer must be empty after two capsules");
/* No more data - CURLE_AGAIN expected */
err = CURLE_OK;
nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
fail_unless(err == CURLE_AGAIN,
"sequential: empty queue should return AGAIN");
fail_unless(nread == 0, "sequential: empty queue should read zero bytes");
Curl_bufq_free(&q);
}
static void test_capsule_decode_paths(void)
{
struct bufq q;
unsigned char out[8];
CURLcode err = CURLE_OK;
size_t nread;
const unsigned char invalid_type[] = { 0x01 };
const unsigned char partial_len[] = { 0x00, 0x40 };
const unsigned char invalid_context[] = { 0x00, 0x01, 0x01 };
const unsigned char invalid_caps_len[] = { 0x00, 0x00, 0x00 };
const unsigned char partial_payload[] = { 0x00, 0x04, 0x00, 0x11, 0x22 };
const unsigned char payload_3b[] = { 0x00, 0x04, 0x00, 0x11, 0x22, 0x33 };
const unsigned char payload_empty[] = { 0x00, 0x01, 0x00 };
Curl_bufq_init2(&q, 32, 4, BUFQ_OPT_NONE);
check_capsule_result(&q, NULL, 0, 0, CURLE_BAD_FUNCTION_ARGUMENT, 0);
check_capsule_result(&q, NULL, 0, sizeof(out), CURLE_AGAIN, 0);
check_capsule_result(&q, invalid_type, sizeof(invalid_type), sizeof(out),
CURLE_RECV_ERROR, 0);
check_capsule_result(&q, partial_len, sizeof(partial_len), sizeof(out),
CURLE_AGAIN, 0);
check_capsule_result(&q, invalid_context, sizeof(invalid_context),
sizeof(out), CURLE_RECV_ERROR, 0);
check_capsule_result(&q, invalid_caps_len, sizeof(invalid_caps_len),
sizeof(out), CURLE_RECV_ERROR, 0);
check_capsule_result(&q, partial_payload, sizeof(partial_payload),
sizeof(out), CURLE_AGAIN, 0);
/* oversized payload is rejected and discarded */
Curl_bufq_reset(&q);
queue_bytes(&q, payload_3b, sizeof(payload_3b));
nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, 2, &err);
fail_unless(err == CURLE_RECV_ERROR,
"expected RECV_ERROR for short output buffer");
fail_unless(nread == 0, "expected zero read on short output buffer");
fail_unless(Curl_bufq_is_empty(&q),
"oversized capsule must be discarded");
/* zero-length UDP payload is accepted and consumed */
Curl_bufq_reset(&q);
queue_bytes(&q, payload_empty, sizeof(payload_empty));
nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
fail_unless(err == CURLE_OK, "zero-length UDP payload should succeed");
fail_unless(nread == 0, "zero-length UDP payload should read zero");
fail_unless(Curl_bufq_is_empty(&q), "zero-length capsule must be consumed");
/* normal payload decode */
Curl_bufq_reset(&q);
queue_bytes(&q, payload_3b, sizeof(payload_3b));
memset(out, 0, sizeof(out));
nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
fail_unless(err == CURLE_OK, "payload decode should succeed");
fail_unless(nread == 3, "payload decode size mismatch");
fail_unless(out[0] == 0x11 && out[1] == 0x22 && out[2] == 0x33,
"payload decode bytes mismatch");
fail_unless(Curl_bufq_is_empty(&q), "payload capsule must be consumed");
Curl_bufq_free(&q);
}
#endif /* USE_NGTCP2 && !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
static CURLcode test_unit3400(const char *arg)
{
UNITTEST_BEGIN_SIMPLE
(void)arg;
#if defined(USE_PROXY_HTTP3) && \
!defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
test_capsule_encap_udp_hdr_boundaries();
#endif
#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \
!defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
test_capsule_encode_decode_roundtrip();
test_capsule_decode_paths();
test_capsule_sequential_decode();
#endif
UNITTEST_END_SIMPLE
}