diff --git a/.github/workflows/osslq-linux.yml b/.github/workflows/osslq-linux.yml new file mode 100644 index 0000000000..cda12616e2 --- /dev/null +++ b/.github/workflows/osslq-linux.yml @@ -0,0 +1,233 @@ +# Copyright (C) Daniel Stenberg, , et al. +# +# SPDX-License-Identifier: curl + +name: osslq-linux + +on: + push: + branches: + - master + - '*/ci' + paths-ignore: + - '**/*.md' + - '**/CMakeLists.txt' + - '.azure-pipelines.yml' + - '.circleci/**' + - '.cirrus.yml' + - 'appveyor.*' + - 'CMake/**' + - 'packages/**' + - 'plan9/**' + - 'projects/**' + - 'winbuild/**' + pull_request: + branches: + - master + paths-ignore: + - '**/*.md' + - '**/CMakeLists.txt' + - '.azure-pipelines.yml' + - '.circleci/**' + - '.cirrus.yml' + - 'appveyor.*' + - 'CMake/**' + - 'packages/**' + - 'plan9/**' + - 'projects/**' + - 'winbuild/**' + +concurrency: + # Hardcoded workflow filename as workflow name above is just Linux again + group: osslq-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: {} + +env: + MAKEFLAGS: -j 3 + openssl3-version: openssl-3.2.0 + quictls-version: 3.1.4+quic + nghttp3-version: v1.1.0 + ngtcp2-version: v1.1.0 + nghttp2-version: master + mod_h2-version: v2.0.25 + +jobs: + autotools: + name: ${{ matrix.build.name }} + runs-on: 'ubuntu-latest' + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + build: + - name: openssl-quic + configure: >- + PKG_CONFIG_PATH="$HOME/openssl3/lib/pkgconfig" LDFLAGS="-Wl,-rpath,$HOME/openssl3/lib" + --enable-warnings --enable-werror --enable-debug --disable-ntlm + --with-test-nghttpx="$HOME/nghttpx/bin/nghttpx" + --with-openssl=$HOME/openssl3 --with-openssl-quic + --with-nghttp3=$HOME/nghttpx + + steps: + - run: | + sudo apt-get update + sudo apt-get install libtool autoconf automake pkg-config stunnel4 \ + libpsl-dev libbrotli-dev libzstd-dev zlib1g-dev libev-dev libc-ares-dev \ + nettle-dev libp11-kit-dev libtspi-dev libunistring-dev guile-2.2-dev libtasn1-bin \ + libtasn1-6-dev libidn2-0-dev gawk gperf libtss2-dev dns-root-data bison gtk-doc-tools \ + texinfo texlive texlive-extra-utils autopoint libev-dev \ + apache2 apache2-dev libnghttp2-dev + name: 'install prereqs and impacket, pytest, crypto, apache2' + + - name: cache openssl3 + if: contains(matrix.build.install_steps, 'openssl3') + uses: actions/cache@v3 + id: cache-openssl3 + env: + cache-name: cache-openssl3 + with: + path: /home/runner/openssl3 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.openssl3-version }} + + - name: 'install openssl3' + if: steps.cache-openssl3.outputs.cache-hit != 'true' + run: | + git clone --quiet --depth=1 -b ${{ env.openssl3-version }} https://github.com/openssl/openssl + cd openssl + ./config --prefix=$HOME/openssl3 --libdir=$HOME/openssl3/lib + make -j1 install_sw + + - name: cache quictls + if: contains(matrix.build.install_steps, 'quictls') + uses: actions/cache@v3 + id: cache-quictls + env: + cache-name: cache-quictls + with: + path: /home/runner/quictls + key: ${{ runner.os }}-build-${{ env.cache-name }}-quictls-${{ env.quictls-version }} + + - name: cache quictls + uses: actions/cache@v3 + id: cache-quictls-no-deprecated + env: + cache-name: cache-quictls-no-deprecated + with: + path: /home/runner/quictls + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.quictls-version }} + + - if: steps.cache-quictls-no-deprecated.outputs.cache-hit != 'true' + run: | + cd $HOME + git clone --quiet --depth=1 -b openssl-${{ env.quictls-version }} https://github.com/quictls/openssl quictls + cd quictls + ./config no-deprecated --prefix=$HOME/nghttpx --libdir=$HOME/nghttpx/lib + make + name: 'build quictls' + + - run: | + cd $HOME/quictls + make -j1 install_sw + name: 'install quictls' + + + - name: cache nghttp3 + uses: actions/cache@v3 + id: cache-nghttp3 + env: + cache-name: cache-nghttp3 + with: + path: /home/runner/nghttp3 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.nghttp3-version }} + + - if: steps.cache-nghttp3.outputs.cache-hit != 'true' + run: | + cd $HOME + git clone --quiet --depth=1 -b ${{ env.nghttp3-version }} https://github.com/ngtcp2/nghttp3 + cd nghttp3 + autoreconf -fi + ./configure --prefix=$HOME/nghttpx PKG_CONFIG_PATH="$HOME/nghttpx/lib/pkgconfig" --enable-lib-only + make + name: 'build nghttp3' + + - run: | + cd $HOME/nghttp3 + make install + name: 'install nghttp3' + + # depends on all other cached libs built so far + - run: | + git clone --quiet --depth=1 -b ${{ env.ngtcp2-version }} https://github.com/ngtcp2/ngtcp2 + cd ngtcp2 + autoreconf -fi + ./configure --prefix=$HOME/nghttpx PKG_CONFIG_PATH="$HOME/nghttpx/lib/pkgconfig" --enable-lib-only --with-openssl + make install + name: 'install ngtcp2' + + # depends on all other cached libs built so far + - run: | + git clone --quiet --depth=1 -b ${{ env.nghttp2-version }} https://github.com/nghttp2/nghttp2 + cd nghttp2 + autoreconf -fi + ./configure --prefix=$HOME/nghttpx PKG_CONFIG_PATH="$HOME/nghttpx/lib/pkgconfig" --enable-http3 + make install + name: 'install nghttp2' + + - name: cache mod_h2 + uses: actions/cache@v3 + id: cache-mod_h2 + env: + cache-name: cache-mod_h2 + with: + path: /home/runner/mod_h2 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ env.mod_h2-version }} + + - if: steps.cache-mod_h2.outputs.cache-hit != 'true' + run: | + cd $HOME + git clone --quiet --depth=1 -b ${{ env.mod_h2-version }} https://github.com/icing/mod_h2 + cd mod_h2 + autoreconf -fi + ./configure + make + name: 'build mod_h2' + + - run: | + cd $HOME/mod_h2 + sudo make install + name: 'install mod_h2' + + - uses: actions/checkout@v4 + + - run: | + sudo python3 -m pip install -r tests/requirements.txt -r tests/http/requirements.txt + name: 'install python test prereqs' + + - run: autoreconf -fi + name: 'autoreconf' + + - run: ./configure ${{ matrix.build.configure }} + name: 'configure' + + - run: make V=1 + name: 'make' + + - run: make V=1 examples + name: 'make examples' + + - run: make V=1 -C tests + name: 'make tests' + + - run: make V=1 test-ci + name: 'run tests' + env: + # 2500 and 25002 fail atm due to fin handling + TFLAGS: "!http/3" + + - run: pytest -v tests + name: 'run pytest' + env: + TFLAGS: "${{ matrix.build.tflags }}" + CURL_CI: github diff --git a/configure.ac b/configure.ac index 092fcb6f98..99e00514a1 100644 --- a/configure.ac +++ b/configure.ac @@ -175,7 +175,7 @@ curl_headers_msg="enabled (--disable-headers-api)" ssl_backends= curl_h1_msg="enabled (internal)" curl_h2_msg="no (--with-nghttp2)" - curl_h3_msg="no (--with-ngtcp2 --with-nghttp3, --with-quiche, --with-msh3)" + curl_h3_msg="no (--with-ngtcp2 --with-nghttp3, --with-quiche, --with-openssl-quic, --with-msh3)" enable_altsvc="yes" hsts="yes" @@ -3001,14 +3001,53 @@ if test "x$NGTCP2_ENABLED" = "x1" -a "x$WOLFSSL_ENABLED" = "x1"; then fi fi +dnl ********************************************************************** +dnl Check for OpenSSL QUIC +dnl ********************************************************************** + +OPT_OPENSSL_QUIC="no" + +if test "x$disable_http" = "xyes" -o "x$OPENSSL_ENABLED" != "x1"; then + # without HTTP or without openssl, no use + OPT_OPENSSL_QUIC="no" +fi + +AC_ARG_WITH(openssl-quic, +AS_HELP_STRING([--with-openssl-quic],[Enable OpenSSL QUIC usage]) +AS_HELP_STRING([--without-openssl-quic],[Disable OpenSSL QUIC usage]), + [OPT_OPENSSL_QUIC=$withval]) +case "$OPT_OPENSSL_QUIC" in + no) + dnl --without-openssl-quic option used + want_openssl_quic="no" + ;; + yes) + dnl --with-openssl-quic option used + want_openssl_quic="yes" + ;; +esac + +curl_openssl_quic_msg="no (--with-openssl-quic)" +if test "x$want_openssl_quic" = "xyes"; then + + if test "$NGTCP2_ENABLED" = 1; then + AC_MSG_ERROR([--with-openssl-quic and --with-ngtcp2 are mutually exclusive]) + fi + if test "$HAVE_OPENSSL_QUIC" != 1; then + AC_MSG_ERROR([--with-openssl-quic requires quic support in OpenSSL]) + fi + AC_DEFINE(USE_OPENSSL_QUIC, 1, [if openssl QUIC is in use]) + AC_SUBST(USE_OPENSSL_QUIC, [1]) +fi + dnl ********************************************************************** dnl Check for nghttp3 (HTTP/3 with ngtcp2) dnl ********************************************************************** OPT_NGHTTP3="yes" -if test "x$NGTCP2_ENABLED" = "x"; then - # without ngtcp2, nghttp3 is of no use for us +if test "x$USE_NGTCP2" = "x" -a "$USE_OPENSSL_QUIC" = "x"; then + # without ngtcp2 or openssl quic, nghttp3 is of no use for us OPT_NGHTTP3="no" fi @@ -3036,10 +3075,6 @@ esac curl_http3_msg="no (--with-nghttp3)" if test X"$want_nghttp3" != Xno; then - if test "$NGTCP2_ENABLED" != "1"; then - AC_MSG_ERROR([--with-nghttp3 also requires --with-ntcp2]) - fi - dnl backup the pre-nghttp3 variables CLEANLDFLAGS="$LDFLAGS" CLEANCPPFLAGS="$CPPFLAGS" @@ -3070,8 +3105,6 @@ if test X"$want_nghttp3" != Xno; then AC_CHECK_LIB(nghttp3, nghttp3_conn_client_new_versioned, [ AC_CHECK_HEADERS(nghttp3/nghttp3.h, - curl_h3_msg="enabled (ngtcp2 + nghttp3)" - NGHTTP3_ENABLED=1 AC_DEFINE(USE_NGHTTP3, 1, [if nghttp3 is in use]) AC_SUBST(USE_NGHTTP3, [1]) CURL_LIBRARY_PATH="$CURL_LIBRARY_PATH:$DIR_NGHTTP3" @@ -3096,6 +3129,29 @@ if test X"$want_nghttp3" != Xno; then fi +dnl ********************************************************************** +dnl Check for ngtcp2 and nghttp3 (HTTP/3 with ngtcp2 + nghttp3) +dnl ********************************************************************** + +if test "x$NGTCP2_ENABLED" = "x1" -a "x$USE_NGHTTP3" = "x1"; then + AC_DEFINE(USE_NGTCP2_H3, 1, [if ngtcp2 + nghttp3 is in use]) + AC_SUBST(USE_NGTCP2_H3, [1]) + AC_MSG_NOTICE([HTTP3 support is experimental]) + curl_h3_msg="enabled (ngtcp2 + nghttp3)" +fi + +dnl ********************************************************************** +dnl Check for OpenSSL and nghttp3 (HTTP/3 with nghttp3 using OpenSSL QUIC) +dnl ********************************************************************** + +if test "x$USE_OPENSSL_QUIC" = "x1" -a "x$USE_NGHTTP3" = "x1"; then + experimental="$experimental HTTP3" + AC_DEFINE(USE_OPENSSL_H3, 1, [if openssl quic + nghttp3 is in use]) + AC_SUBST(USE_OPENSSL_H3, [1]) + AC_MSG_NOTICE([HTTP3 support is experimental]) + curl_h3_msg="enabled (openssl + nghttp3)" +fi + dnl ********************************************************************** dnl Check for quiche (QUIC) dnl ********************************************************************** @@ -3245,6 +3301,9 @@ if test X"$want_msh3" != Xno; then if test "$NGHTTP3_ENABLED" = 1; then AC_MSG_ERROR([--with-msh3 and --with-ngtcp2 are mutually exclusive]) fi + if test "$QUICHE_ENABLED" = 1; then + AC_MSG_ERROR([--with-msh3 and --with-quiche are mutually exclusive]) + fi dnl backup the pre-msh3 variables CLEANLDFLAGS="$LDFLAGS" @@ -4575,8 +4634,8 @@ if test "x$USE_NGHTTP2" = "x1"; then SUPPORT_FEATURES="$SUPPORT_FEATURES HTTP2" fi -if test "x$USE_NGTCP2" = "x1" -o "x$USE_QUICHE" = "x1" \ - -o "x$USE_MSH3" = "x1"; then +if test "x$USE_NGTCP2_H3" = "x1" -o "x$USE_QUICHE" = "x1" \ + -o "x$USE_OPENSSL_H3" = "x1" -o "x$USE_MSH3" = "x1"; then SUPPORT_FEATURES="$SUPPORT_FEATURES HTTP3" fi diff --git a/docs/HTTP3.md b/docs/HTTP3.md index 7f2f08ada3..6d84778c13 100644 --- a/docs/HTTP3.md +++ b/docs/HTTP3.md @@ -15,6 +15,8 @@ QUIC libraries we are using: [quiche](https://github.com/cloudflare/quiche) - **EXPERIMENTAL** +[OpenSSL 3.2+ QUIC](https://github.com/openssl/openssl) - **EXPERIMENTAL** + [msh3](https://github.com/nibanks/msh3) (with [msquic](https://github.com/microsoft/msquic)) - **EXPERIMENTAL** ## Experimental @@ -193,6 +195,40 @@ Build curl: If `make install` results in `Permission denied` error, you will need to prepend it with `sudo`. +# OpenSSL version + +quiche QUIC support is **EXPERIMENTAL** + +Build OpenSSL 3.2.0 + + % cd .. + % git clone -b openssl-3.2.0 https://github.com/openssl/openssl + % cd openssl + % ./config enable-tls1_3 --prefix= --libdir=/lib + % make install + +Build nghttp3 + + % cd .. + % git clone -b v1.1.0 https://github.com/ngtcp2/nghttp3 + % cd nghttp3 + % autoreconf -fi + % ./configure --prefix= --enable-lib-only + % make + % make install + +Build curl: + + % cd .. + % git clone https://github.com/curl/curl + % cd curl + % autoreconf -fi + % ./configure --with-openssl= --with-openssl-quic --with-nghttp3= + % make + % make install + + If `make install` results in `Permission denied` error, you will need to prepend it with `sudo`. + # msh3 (msquic) version **Note**: The msquic HTTP/3 backend is immature and is not properly functional diff --git a/lib/Makefile.inc b/lib/Makefile.inc index d937e1ed0c..627148abeb 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -78,6 +78,7 @@ LIB_VTLS_HFILES = \ LIB_VQUIC_CFILES = \ vquic/curl_msh3.c \ vquic/curl_ngtcp2.c \ + vquic/curl_osslq.c \ vquic/curl_quiche.c \ vquic/vquic.c \ vquic/vquic-tls.c @@ -85,6 +86,7 @@ LIB_VQUIC_CFILES = \ LIB_VQUIC_HFILES = \ vquic/curl_msh3.h \ vquic/curl_ngtcp2.h \ + vquic/curl_osslq.h \ vquic/curl_quiche.h \ vquic/vquic.h \ vquic/vquic_int.h \ diff --git a/lib/cf-socket.c b/lib/cf-socket.c index 64af604ad3..742902f1b9 100644 --- a/lib/cf-socket.c +++ b/lib/cf-socket.c @@ -1630,11 +1630,17 @@ static CURLcode cf_udp_setup_quic(struct Curl_cfilter *cf, /* QUIC needs a connected socket, nonblocking */ DEBUGASSERT(ctx->sock != CURL_SOCKET_BAD); +#if defined(__APPLE__) && defined(USE_OPENSSL_QUIC) + (void)rc; + /* On macOS OpenSSL QUIC fails on connected sockets. + * see: */ +#else rc = connect(ctx->sock, &ctx->addr.sa_addr, ctx->addr.addrlen); if(-1 == rc) { return socket_connect_result(data, ctx->r_ip, SOCKERRNO); } ctx->sock_connected = TRUE; +#endif set_local_ip(cf, data); CURL_TRC_CF(data, cf, "%s socket %" CURL_FORMAT_SOCKET_T " connected: [%s:%d] -> [%s:%d]", diff --git a/lib/curl_setup.h b/lib/curl_setup.h index 551243e270..91e35f58b4 100644 --- a/lib/curl_setup.h +++ b/lib/curl_setup.h @@ -827,6 +827,7 @@ int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, #endif #if (defined(USE_NGTCP2) && defined(USE_NGHTTP3)) || \ + (defined(USE_OPENSSL_QUIC) && defined(USE_NGHTTP3)) || \ defined(USE_QUICHE) || defined(USE_MSH3) #define ENABLE_QUIC #define USE_HTTP3 diff --git a/lib/vquic/curl_osslq.c b/lib/vquic/curl_osslq.c new file mode 100644 index 0000000000..41f0892475 --- /dev/null +++ b/lib/vquic/curl_osslq.c @@ -0,0 +1,2239 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ + +#include "curl_setup.h" + +#if defined(USE_OPENSSL_QUIC) && defined(USE_NGHTTP3) + +#include +#include +#include +#include + +#include "urldata.h" +#include "sendf.h" +#include "strdup.h" +#include "rand.h" +#include "multiif.h" +#include "strcase.h" +#include "cfilters.h" +#include "cf-socket.h" +#include "connect.h" +#include "progress.h" +#include "strerror.h" +#include "dynbuf.h" +#include "http1.h" +#include "select.h" +#include "inet_pton.h" +#include "vquic.h" +#include "vquic_int.h" +#include "vquic-tls.h" +#include "vtls/keylog.h" +#include "vtls/vtls.h" +#include "vtls/openssl.h" +#include "curl_osslq.h" + +#include "warnless.h" + +/* The last 3 #include files should be in this order */ +#include "curl_printf.h" +#include "curl_memory.h" +#include "memdebug.h" + +/* A stream window is the maximum amount we need to buffer for + * each active transfer. We use HTTP/3 flow control and only ACK + * when we take things out of the buffer. + * Chunk size is large enough to take a full DATA frame */ +#define H3_STREAM_WINDOW_SIZE (128 * 1024) +#define H3_STREAM_CHUNK_SIZE (16 * 1024) +/* The pool keeps spares around and half of a full stream windows + * seems good. More does not seem to improve performance. + * The benefit of the pool is that stream buffer to not keep + * spares. So memory consumption goes down when streams run empty, + * have a large upload done, etc. */ +#define H3_STREAM_POOL_SPARES \ + (H3_STREAM_WINDOW_SIZE / H3_STREAM_CHUNK_SIZE ) / 2 +/* Receive and Send max number of chunks just follows from the + * chunk size and window size */ +#define H3_STREAM_RECV_CHUNKS \ + (H3_STREAM_WINDOW_SIZE / H3_STREAM_CHUNK_SIZE) +#define H3_STREAM_SEND_CHUNKS \ + (H3_STREAM_WINDOW_SIZE / H3_STREAM_CHUNK_SIZE) + +#ifndef ARRAYSIZE +#define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0])) +#endif + +#if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) +typedef uint32_t sslerr_t; +#else +typedef unsigned long sslerr_t; +#endif + + +/* How to access `call_data` from a cf_osslq filter */ +#undef CF_CTX_CALL_DATA +#define CF_CTX_CALL_DATA(cf) \ + ((struct cf_osslq_ctx *)(cf)->ctx)->call_data + +static CURLcode cf_progress_ingress(struct Curl_cfilter *cf, + struct Curl_easy *data); + +static const char *SSL_ERROR_to_str(int err) +{ + switch(err) { + case SSL_ERROR_NONE: + return "SSL_ERROR_NONE"; + case SSL_ERROR_SSL: + return "SSL_ERROR_SSL"; + case SSL_ERROR_WANT_READ: + return "SSL_ERROR_WANT_READ"; + case SSL_ERROR_WANT_WRITE: + return "SSL_ERROR_WANT_WRITE"; + case SSL_ERROR_WANT_X509_LOOKUP: + return "SSL_ERROR_WANT_X509_LOOKUP"; + case SSL_ERROR_SYSCALL: + return "SSL_ERROR_SYSCALL"; + case SSL_ERROR_ZERO_RETURN: + return "SSL_ERROR_ZERO_RETURN"; + case SSL_ERROR_WANT_CONNECT: + return "SSL_ERROR_WANT_CONNECT"; + case SSL_ERROR_WANT_ACCEPT: + return "SSL_ERROR_WANT_ACCEPT"; +#if defined(SSL_ERROR_WANT_ASYNC) + case SSL_ERROR_WANT_ASYNC: + return "SSL_ERROR_WANT_ASYNC"; +#endif +#if defined(SSL_ERROR_WANT_ASYNC_JOB) + case SSL_ERROR_WANT_ASYNC_JOB: + return "SSL_ERROR_WANT_ASYNC_JOB"; +#endif +#if defined(SSL_ERROR_WANT_EARLY) + case SSL_ERROR_WANT_EARLY: + return "SSL_ERROR_WANT_EARLY"; +#endif + default: + return "SSL_ERROR unknown"; + } +} + +/* Return error string for last OpenSSL error */ +static char *ossl_strerror(unsigned long error, char *buf, size_t size) +{ + DEBUGASSERT(size); + *buf = '\0'; + +#if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) + ERR_error_string_n((uint32_t)error, buf, size); +#else + ERR_error_string_n(error, buf, size); +#endif + + if(!*buf) { + const char *msg = error ? "Unknown error" : "No error"; + if(strlen(msg) < size) + strcpy(buf, msg); + } + + return buf; +} + +static CURLcode make_bio_addr(BIO_ADDR **pbio_addr, + const struct Curl_sockaddr_ex *addr) +{ + BIO_ADDR *ba; + CURLcode result = CURLE_FAILED_INIT; + + ba = BIO_ADDR_new(); + if(!ba) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + + switch(addr->family) { + case AF_INET: { + struct sockaddr_in * const sin = + (struct sockaddr_in * const)(void *)&addr->sa_addr; + if(!BIO_ADDR_rawmake(ba, AF_INET, &sin->sin_addr, + sizeof(sin->sin_addr), sin->sin_port)) { + goto out; + } + result = CURLE_OK; + break; + } +#ifdef ENABLE_IPV6 + case AF_INET6: { + struct sockaddr_in6 * const sin = + (struct sockaddr_in6 * const)(void *)&addr->sa_addr; + if(!BIO_ADDR_rawmake(ba, AF_INET6, &sin->sin6_addr, + sizeof(sin->sin6_addr), sin->sin6_port)) { + } + result = CURLE_OK; + break; + } +#endif /* ENABLE_IPV6 */ + default: + /* sunsupported */ + DEBUGASSERT(0); + break; + } + +out: + if(result && ba) { + BIO_ADDR_free(ba); + ba = NULL; + } + *pbio_addr = ba; + return result; +} + +/* QUIC stream (not necessarily H3) */ +struct cf_osslq_stream { + int64_t id; + SSL *ssl; + struct bufq recvbuf; /* QUIC war data recv buffer */ + BIT(recvd_eos); + BIT(closed); + BIT(reset); + BIT(send_blocked); +}; + +static CURLcode cf_osslq_stream_open(struct cf_osslq_stream *s, + SSL *conn, + uint64_t flags, + struct bufc_pool *bufcp, + void *user_data) +{ + DEBUGASSERT(!s->ssl); + Curl_bufq_initp(&s->recvbuf, bufcp, 1, BUFQ_OPT_NONE); + s->ssl = SSL_new_stream(conn, flags); + if(!s->ssl) { + return CURLE_FAILED_INIT; + } + s->id = SSL_get_stream_id(s->ssl); + SSL_set_app_data(s->ssl, user_data); + return CURLE_OK; +} + +static void cf_osslq_stream_cleanup(struct cf_osslq_stream *s) +{ + if(s->ssl) { + SSL_set_app_data(s->ssl, NULL); + SSL_free(s->ssl); + } + Curl_bufq_free(&s->recvbuf); + memset(s, 0, sizeof(*s)); +} + +static void cf_osslq_stream_close(struct cf_osslq_stream *s) +{ + if(s->ssl) { + SSL_free(s->ssl); + s->ssl = NULL; + } +} + +struct cf_osslq_h3conn { + nghttp3_conn *conn; + nghttp3_settings settings; + struct cf_osslq_stream s_ctrl; + struct cf_osslq_stream s_qpack_enc; + struct cf_osslq_stream s_qpack_dec; + struct cf_osslq_stream remote_ctrl[3]; /* uni streams opened by the peer */ + size_t remote_ctrl_n; /* number of peer streams opened */ +}; + +static void cf_osslq_h3conn_cleanup(struct cf_osslq_h3conn *h3) +{ + size_t i; + + if(h3->conn) + nghttp3_conn_del(h3->conn); + cf_osslq_stream_cleanup(&h3->s_ctrl); + cf_osslq_stream_cleanup(&h3->s_qpack_enc); + cf_osslq_stream_cleanup(&h3->s_qpack_dec); + for(i = 0; i < h3->remote_ctrl_n; ++i) { + cf_osslq_stream_cleanup(&h3->remote_ctrl[i]); + } +} + +struct cf_osslq_ctx { + struct cf_quic_ctx q; + struct ssl_peer peer; + struct quic_tls_ctx tls; + struct cf_call_data call_data; + struct cf_osslq_h3conn h3; + struct curltime started_at; /* time the current attempt started */ + struct curltime handshake_at; /* time connect handshake finished */ + struct curltime first_byte_at; /* when first byte was recvd */ + struct curltime reconnect_at; /* time the next attempt should start */ + struct bufc_pool stream_bufcp; /* chunk pool for streams */ + size_t max_stream_window; /* max flow window for one stream */ + uint64_t max_idle_ms; /* max idle time for QUIC connection */ + BIT(got_first_byte); /* if first byte was received */ +#ifdef USE_OPENSSL + BIT(x509_store_setup); /* if x509 store has been set up */ + BIT(protocol_shutdown); /* QUIC connection is shut down */ +#endif +}; + +static void cf_osslq_ctx_clear(struct cf_osslq_ctx *ctx) +{ + struct cf_call_data save = ctx->call_data; + + cf_osslq_h3conn_cleanup(&ctx->h3); + Curl_vquic_tls_cleanup(&ctx->tls); + vquic_ctx_free(&ctx->q); + Curl_bufcp_free(&ctx->stream_bufcp); + Curl_ssl_peer_cleanup(&ctx->peer); + + memset(ctx, 0, sizeof(*ctx)); + ctx->call_data = save; +} + +static void cf_osslq_close(struct Curl_cfilter *cf, struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct cf_call_data save; + + CF_DATA_SAVE(save, cf, data); + if(ctx && ctx->tls.ssl) { + /* TODO: send connection close */ + CURL_TRC_CF(data, cf, "cf_osslq_close()"); + cf_osslq_ctx_clear(ctx); + } + + cf->connected = FALSE; + CF_DATA_RESTORE(cf, save); +} + +static void cf_osslq_destroy(struct Curl_cfilter *cf, struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct cf_call_data save; + + CF_DATA_SAVE(save, cf, data); + CURL_TRC_CF(data, cf, "destroy"); + if(ctx) { + CURL_TRC_CF(data, cf, "cf_osslq_destroy()"); + cf_osslq_ctx_clear(ctx); + free(ctx); + } + cf->ctx = NULL; + /* No CF_DATA_RESTORE(cf, save) possible */ + (void)save; +} + +static CURLcode cf_osslq_h3conn_add_stream(struct cf_osslq_h3conn *h3, + SSL *stream_ssl, + struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + int64_t stream_id = SSL_get_stream_id(stream_ssl); + + if(h3->remote_ctrl_n >= ARRAYSIZE(h3->remote_ctrl)) { + /* rejected, we are full */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] rejecting additional remote stream", + stream_id); + SSL_free(stream_ssl); + return CURLE_FAILED_INIT; + } + switch(SSL_get_stream_type(stream_ssl)) { + case SSL_STREAM_TYPE_READ: { + struct cf_osslq_stream *nstream = &h3->remote_ctrl[h3->remote_ctrl_n++]; + nstream->id = stream_id; + nstream->ssl = stream_ssl; + Curl_bufq_initp(&nstream->recvbuf, &ctx->stream_bufcp, 1, BUFQ_OPT_NONE); + CURL_TRC_CF(data, cf, "[%" PRId64 "] accepted new remote uni stream", + stream_id); + break; + } + default: + CURL_TRC_CF(data, cf, "[%" PRId64 "] rejecting remote non-uni-read" + " stream", stream_id); + SSL_free(stream_ssl); + return CURLE_FAILED_INIT; + } + return CURLE_OK; + +} + +static CURLcode cf_osslq_ssl_err(struct Curl_cfilter *cf, + struct Curl_easy *data, + int detail, CURLcode def_result) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = def_result; + sslerr_t errdetail; + char ebuf[256] = "unknown"; + const char *err_descr = ebuf; + long lerr; + int lib; + int reason; + struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data); + + errdetail = ERR_get_error(); + lib = ERR_GET_LIB(errdetail); + reason = ERR_GET_REASON(errdetail); + + if((lib == ERR_LIB_SSL) && + ((reason == SSL_R_CERTIFICATE_VERIFY_FAILED) || + (reason == SSL_R_SSLV3_ALERT_CERTIFICATE_EXPIRED))) { + result = CURLE_PEER_FAILED_VERIFICATION; + + lerr = SSL_get_verify_result(ctx->tls.ssl); + if(lerr != X509_V_OK) { + ssl_config->certverifyresult = lerr; + msnprintf(ebuf, sizeof(ebuf), + "SSL certificate problem: %s", + X509_verify_cert_error_string(lerr)); + } + else + err_descr = "SSL certificate verification failed"; + } +#if defined(SSL_R_TLSV13_ALERT_CERTIFICATE_REQUIRED) + /* SSL_R_TLSV13_ALERT_CERTIFICATE_REQUIRED is only available on + OpenSSL version above v1.1.1, not LibreSSL, BoringSSL, or AWS-LC */ + else if((lib == ERR_LIB_SSL) && + (reason == SSL_R_TLSV13_ALERT_CERTIFICATE_REQUIRED)) { + /* If client certificate is required, communicate the + error to client */ + result = CURLE_SSL_CLIENTCERT; + ossl_strerror(errdetail, ebuf, sizeof(ebuf)); + } +#endif + else if((lib == ERR_LIB_SSL) && (reason == SSL_R_PROTOCOL_IS_SHUTDOWN)) { + ctx->protocol_shutdown = TRUE; + err_descr = "QUIC connectin has been shut down"; + result = def_result; + } + else { + result = def_result; + ossl_strerror(errdetail, ebuf, sizeof(ebuf)); + } + + /* detail is already set to the SSL error above */ + + /* If we e.g. use SSLv2 request-method and the server doesn't like us + * (RST connection, etc.), OpenSSL gives no explanation whatsoever and + * the SO_ERROR is also lost. + */ + if(CURLE_SSL_CONNECT_ERROR == result && errdetail == 0) { + char extramsg[80]=""; + int sockerr = SOCKERRNO; + const char *r_ip = NULL; + int r_port = 0; + + Curl_cf_socket_peek(cf->next, data, NULL, NULL, + &r_ip, &r_port, NULL, NULL); + if(sockerr && detail == SSL_ERROR_SYSCALL) + Curl_strerror(sockerr, extramsg, sizeof(extramsg)); + failf(data, "QUIC connect: %s in connection to %s:%d (%s)", + extramsg[0] ? extramsg : SSL_ERROR_to_str(detail), + ctx->peer.dispname, r_port, r_ip); + } + else { + /* Could be a CERT problem */ + failf(data, "%s", err_descr); + } + return result; +} + +static CURLcode cf_osslq_verify_peer(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + + cf->conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ + cf->conn->httpversion = 30; + cf->conn->bundle->multiuse = BUNDLE_MULTIPLEX; + + return Curl_vquic_tls_verify_peer(&ctx->tls, cf, data, &ctx->peer); +} + +/** + * All about the H3 internals of a stream + */ +struct h3_stream_ctx { + struct cf_osslq_stream s; + struct bufq sendbuf; /* h3 request body */ + struct bufq recvbuf; /* h3 response body */ + struct h1_req_parser h1; /* h1 request parsing */ + size_t sendbuf_len_in_flight; /* sendbuf amount "in flight" */ + size_t upload_blocked_len; /* the amount written last and EGAINed */ + size_t recv_buf_nonflow; /* buffered bytes, not counting for flow control */ + uint64_t error3; /* HTTP/3 stream error code */ + curl_off_t upload_left; /* number of request bytes left to upload */ + curl_off_t download_recvd; /* number of response DATA bytes received */ + int status_code; /* HTTP status code */ + bool resp_hds_complete; /* we have a complete, final response */ + bool closed; /* TRUE on stream close */ + bool reset; /* TRUE on stream reset */ + bool send_closed; /* stream is local closed */ + BIT(quic_flow_blocked); /* stream is blocked by QUIC flow control */ +}; + +#define H3_STREAM_CTX(d) ((struct h3_stream_ctx *)(((d) && (d)->req.p.http)? \ + ((struct HTTP *)(d)->req.p.http)->h3_ctx \ + : NULL)) +#define H3_STREAM_LCTX(d) ((struct HTTP *)(d)->req.p.http)->h3_ctx +#define H3_STREAM_ID(d) (H3_STREAM_CTX(d)? \ + H3_STREAM_CTX(d)->s.id : -2) + +static CURLcode h3_data_setup(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + + if(!data || !data->req.p.http) { + failf(data, "initialization failure, transfer not http initialized"); + return CURLE_FAILED_INIT; + } + + if(stream) + return CURLE_OK; + + stream = calloc(1, sizeof(*stream)); + if(!stream) + return CURLE_OUT_OF_MEMORY; + + stream->s.id = -1; + /* on send, we control how much we put into the buffer */ + Curl_bufq_initp(&stream->sendbuf, &ctx->stream_bufcp, + H3_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE); + stream->sendbuf_len_in_flight = 0; + /* on recv, we need a flexible buffer limit since we also write + * headers to it that are not counted against the nghttp3 flow limits. */ + Curl_bufq_initp(&stream->recvbuf, &ctx->stream_bufcp, + H3_STREAM_RECV_CHUNKS, BUFQ_OPT_SOFT_LIMIT); + stream->recv_buf_nonflow = 0; + Curl_h1_req_parse_init(&stream->h1, H1_PARSE_DEFAULT_MAX_LINE_LEN); + + H3_STREAM_LCTX(data) = stream; + return CURLE_OK; +} + +static void h3_data_done(struct Curl_cfilter *cf, struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + + (void)cf; + if(stream) { + CURL_TRC_CF(data, cf, "[%"PRId64"] easy handle is done", stream->s.id); + if(ctx->h3.conn && !stream->closed) { + nghttp3_conn_shutdown_stream_read(ctx->h3.conn, stream->s.id); + nghttp3_conn_close_stream(ctx->h3.conn, stream->s.id, + NGHTTP3_H3_REQUEST_CANCELLED); + nghttp3_conn_set_stream_user_data(ctx->h3.conn, stream->s.id, NULL); + stream->closed = TRUE; + } + + cf_osslq_stream_cleanup(&stream->s); + Curl_bufq_free(&stream->sendbuf); + Curl_bufq_free(&stream->recvbuf); + Curl_h1_req_parse_free(&stream->h1); + free(stream); + H3_STREAM_LCTX(data) = NULL; + } +} + +static struct cf_osslq_stream *cf_osslq_get_qstream(struct Curl_cfilter *cf, + struct Curl_easy *data, + int64_t stream_id) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + struct Curl_easy *sdata; + + if(stream && stream->s.id == stream_id) { + return &stream->s; + } + else if(ctx->h3.s_ctrl.id == stream_id) { + return &ctx->h3.s_ctrl; + } + else if(ctx->h3.s_qpack_enc.id == stream_id) { + return &ctx->h3.s_qpack_enc; + } + else if(ctx->h3.s_qpack_dec.id == stream_id) { + return &ctx->h3.s_qpack_dec; + } + else { + DEBUGASSERT(data->multi); + for(sdata = data->multi->easyp; sdata; sdata = sdata->next) { + if((sdata->conn == data->conn) && H3_STREAM_ID(sdata) == stream_id) { + stream = H3_STREAM_CTX(sdata); + return stream? &stream->s : NULL; + } + } + } + return NULL; +} + +static void h3_drain_stream(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + unsigned char bits; + + (void)cf; + bits = CURL_CSELECT_IN; + if(stream && stream->upload_left && !stream->send_closed) + bits |= CURL_CSELECT_OUT; + if(data->state.select_bits != bits) { + data->state.select_bits = bits; + Curl_expire(data, 0, EXPIRE_RUN_NOW); + } +} + +static CURLcode h3_data_pause(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool pause) +{ + if(!pause) { + /* unpaused. make it run again right away */ + h3_drain_stream(cf, data); + Curl_expire(data, 0, EXPIRE_RUN_NOW); + } + return CURLE_OK; +} + +static int cb_h3_stream_close(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + (void)conn; + (void)stream_id; + + /* we might be called by nghttp3 after we already cleaned up */ + if(!stream) + return 0; + + stream->closed = TRUE; + stream->error3 = app_error_code; + if(stream->error3 != NGHTTP3_H3_NO_ERROR) { + stream->reset = TRUE; + stream->send_closed = TRUE; + CURL_TRC_CF(data, cf, "[%" PRId64 "] RESET: error %" PRId64, + stream->s.id, stream->error3); + } + else { + CURL_TRC_CF(data, cf, "[%" PRId64 "] CLOSED", stream->s.id); + } + h3_drain_stream(cf, data); + return 0; +} + +/* + * write_resp_raw() copies response data in raw format to the `data`'s + * receive buffer. If not enough space is available, it appends to the + * `data`'s overflow buffer. + */ +static CURLcode write_resp_raw(struct Curl_cfilter *cf, + struct Curl_easy *data, + const void *mem, size_t memlen, + bool flow) +{ + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + CURLcode result = CURLE_OK; + ssize_t nwritten; + + (void)cf; + if(!stream) { + return CURLE_RECV_ERROR; + } + nwritten = Curl_bufq_write(&stream->recvbuf, mem, memlen, &result); + if(nwritten < 0) { + return result; + } + + if(!flow) + stream->recv_buf_nonflow += (size_t)nwritten; + + if((size_t)nwritten < memlen) { + /* This MUST not happen. Our recbuf is dimensioned to hold the + * full max_stream_window and then some for this very reason. */ + DEBUGASSERT(0); + return CURLE_RECV_ERROR; + } + return result; +} + +static int cb_h3_recv_data(nghttp3_conn *conn, int64_t stream3_id, + const uint8_t *buf, size_t buflen, + void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + CURLcode result; + + (void)conn; + (void)stream3_id; + + if(!stream) + return NGHTTP3_ERR_CALLBACK_FAILURE; + + result = write_resp_raw(cf, data, buf, buflen, TRUE); + if(result) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] DATA len=%zu, ERROR receiving %d", + stream->s.id, buflen, result); + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + stream->download_recvd += (curl_off_t)buflen; + CURL_TRC_CF(data, cf, "[%" PRId64 "] DATA len=%zu, total=%zd", + stream->s.id, buflen, stream->download_recvd); + h3_drain_stream(cf, data); + return 0; +} + +static int cb_h3_deferred_consume(nghttp3_conn *conn, int64_t stream_id, + size_t consumed, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + + (void)conn; + (void)stream_id; + if(stream) + CURL_TRC_CF(data, cf, "[%" PRId64 "] deferred consume %zu bytes", + stream->s.id, consumed); + return 0; +} + +static int cb_h3_recv_header(nghttp3_conn *conn, int64_t stream_id, + int32_t token, nghttp3_rcbuf *name, + nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + nghttp3_vec h3name = nghttp3_rcbuf_get_buf(name); + nghttp3_vec h3val = nghttp3_rcbuf_get_buf(value); + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + CURLcode result = CURLE_OK; + (void)conn; + (void)stream_id; + (void)token; + (void)flags; + (void)cf; + + /* we might have cleaned up this transfer already */ + if(!stream) + return 0; + + if(token == NGHTTP3_QPACK_TOKEN__STATUS) { + char line[14]; /* status line is always 13 characters long */ + size_t ncopy; + + result = Curl_http_decode_status(&stream->status_code, + (const char *)h3val.base, h3val.len); + if(result) + return -1; + ncopy = msnprintf(line, sizeof(line), "HTTP/3 %03d \r\n", + stream->status_code); + CURL_TRC_CF(data, cf, "[%" PRId64 "] status: %s", stream_id, line); + result = write_resp_raw(cf, data, line, ncopy, FALSE); + if(result) { + return -1; + } + } + else { + /* store as an HTTP1-style header */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] header: %.*s: %.*s", + stream_id, (int)h3name.len, h3name.base, + (int)h3val.len, h3val.base); + result = write_resp_raw(cf, data, h3name.base, h3name.len, FALSE); + if(result) { + return -1; + } + result = write_resp_raw(cf, data, ": ", 2, FALSE); + if(result) { + return -1; + } + result = write_resp_raw(cf, data, h3val.base, h3val.len, FALSE); + if(result) { + return -1; + } + result = write_resp_raw(cf, data, "\r\n", 2, FALSE); + if(result) { + return -1; + } + } + return 0; +} + +static int cb_h3_end_headers(nghttp3_conn *conn, int64_t stream_id, + int fin, void *user_data, void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + CURLcode result = CURLE_OK; + (void)conn; + (void)stream_id; + (void)fin; + (void)cf; + + if(!stream) + return 0; + /* add a CRLF only if we've received some headers */ + result = write_resp_raw(cf, data, "\r\n", 2, FALSE); + if(result) { + return -1; + } + + CURL_TRC_CF(data, cf, "[%" PRId64 "] end_headers, status=%d", + stream_id, stream->status_code); + if(stream->status_code / 100 != 1) { + stream->resp_hds_complete = TRUE; + } + h3_drain_stream(cf, data); + return 0; +} + +static int cb_h3_stop_sending(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + (void)conn; + (void)app_error_code; + + if(!stream || !stream->s.ssl) + return 0; + + CURL_TRC_CF(data, cf, "[%" PRId64 "] stop_sending", stream_id); + cf_osslq_stream_close(&stream->s); + return 0; +} + +static int cb_h3_reset_stream(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + int rv; + (void)conn; + + if(stream && stream->s.ssl) { + SSL_STREAM_RESET_ARGS args = {0}; + args.quic_error_code = app_error_code; + rv = !SSL_stream_reset(stream->s.ssl, &args, sizeof(args)); + CURL_TRC_CF(data, cf, "[%" PRId64 "] reset -> %d", stream_id, rv); + if(!rv) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + return 0; +} + +static nghttp3_ssize +cb_h3_read_req_body(nghttp3_conn *conn, int64_t stream_id, + nghttp3_vec *vec, size_t veccnt, + uint32_t *pflags, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + ssize_t nwritten = 0; + size_t nvecs = 0; + (void)cf; + (void)conn; + (void)stream_id; + (void)user_data; + (void)veccnt; + + if(!stream) + return NGHTTP3_ERR_CALLBACK_FAILURE; + /* nghttp3 keeps references to the sendbuf data until it is ACKed + * by the server (see `cb_h3_acked_req_body()` for updates). + * `sendbuf_len_in_flight` is the amount of bytes in `sendbuf` + * that we have already passed to nghttp3, but which have not been + * ACKed yet. + * Any amount beyond `sendbuf_len_in_flight` we need still to pass + * to nghttp3. Do that now, if we can. */ + if(stream->sendbuf_len_in_flight < Curl_bufq_len(&stream->sendbuf)) { + nvecs = 0; + while(nvecs < veccnt && + Curl_bufq_peek_at(&stream->sendbuf, + stream->sendbuf_len_in_flight, + (const unsigned char **)&vec[nvecs].base, + &vec[nvecs].len)) { + stream->sendbuf_len_in_flight += vec[nvecs].len; + nwritten += vec[nvecs].len; + ++nvecs; + } + DEBUGASSERT(nvecs > 0); /* we SHOULD have been be able to peek */ + } + + if(nwritten > 0 && stream->upload_left != -1) + stream->upload_left -= nwritten; + + /* When we stopped sending and everything in `sendbuf` is "in flight", + * we are at the end of the request body. */ + if(stream->upload_left == 0) { + *pflags = NGHTTP3_DATA_FLAG_EOF; + stream->send_closed = TRUE; + } + else if(!nwritten) { + /* Not EOF, and nothing to give, we signal WOULDBLOCK. */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] read req body -> AGAIN", + stream->s.id); + return NGHTTP3_ERR_WOULDBLOCK; + } + + CURL_TRC_CF(data, cf, "[%" PRId64 "] read req body -> " + "%d vecs%s with %zu (buffered=%zu, left=%" + CURL_FORMAT_CURL_OFF_T ")", + stream->s.id, (int)nvecs, + *pflags == NGHTTP3_DATA_FLAG_EOF?" EOF":"", + nwritten, Curl_bufq_len(&stream->sendbuf), + stream->upload_left); + return (nghttp3_ssize)nvecs; +} + +static int cb_h3_acked_stream_data(nghttp3_conn *conn, int64_t stream_id, + uint64_t datalen, void *user_data, + void *stream_user_data) +{ + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + size_t skiplen; + + (void)cf; + if(!stream) + return 0; + /* The server acknowledged `datalen` of bytes from our request body. + * This is a delta. We have kept this data in `sendbuf` for + * re-transmissions and can free it now. */ + if(datalen >= (uint64_t)stream->sendbuf_len_in_flight) + skiplen = stream->sendbuf_len_in_flight; + else + skiplen = (size_t)datalen; + Curl_bufq_skip(&stream->sendbuf, skiplen); + stream->sendbuf_len_in_flight -= skiplen; + + /* Everything ACKed, we resume upload processing */ + if(!stream->sendbuf_len_in_flight) { + int rv = nghttp3_conn_resume_stream(conn, stream_id); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + return 0; +} + +static nghttp3_callbacks ngh3_callbacks = { + cb_h3_acked_stream_data, + cb_h3_stream_close, + cb_h3_recv_data, + cb_h3_deferred_consume, + NULL, /* begin_headers */ + cb_h3_recv_header, + cb_h3_end_headers, + NULL, /* begin_trailers */ + cb_h3_recv_header, + NULL, /* end_trailers */ + cb_h3_stop_sending, + NULL, /* end_stream */ + cb_h3_reset_stream, + NULL, /* shutdown */ + NULL /* recv_settings */ +}; + +static CURLcode cf_osslq_h3conn_init(struct cf_osslq_ctx *ctx, SSL *conn, + void *user_data) +{ + struct cf_osslq_h3conn *h3 = &ctx->h3; + CURLcode result; + int rc; + + nghttp3_settings_default(&h3->settings); + rc = nghttp3_conn_client_new(&h3->conn, + &ngh3_callbacks, + &h3->settings, + nghttp3_mem_default(), + user_data); + if(rc) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + + result = cf_osslq_stream_open(&h3->s_ctrl, conn, + SSL_STREAM_FLAG_ADVANCE|SSL_STREAM_FLAG_UNI, + &ctx->stream_bufcp, NULL); + if(result) { + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + result = cf_osslq_stream_open(&h3->s_qpack_enc, conn, + SSL_STREAM_FLAG_ADVANCE|SSL_STREAM_FLAG_UNI, + &ctx->stream_bufcp, NULL); + if(result) { + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + result = cf_osslq_stream_open(&h3->s_qpack_dec, conn, + SSL_STREAM_FLAG_ADVANCE|SSL_STREAM_FLAG_UNI, + &ctx->stream_bufcp, NULL); + if(result) { + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + + rc = nghttp3_conn_bind_control_stream(h3->conn, h3->s_ctrl.id); + if(rc) { + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + rc = nghttp3_conn_bind_qpack_streams(h3->conn, h3->s_qpack_enc.id, + h3->s_qpack_dec.id); + if(rc) { + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + + result = CURLE_OK; +out: + return result; +} + +static CURLcode cf_osslq_ctx_start(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result; + int rv; + const struct Curl_sockaddr_ex *peer_addr = NULL; + int peer_port; + BIO *bio = NULL; + BIO_ADDR *baddr = NULL; + + Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE, + H3_STREAM_POOL_SPARES); + result = Curl_ssl_peer_init(&ctx->peer, cf); + if(result) + goto out; + +#define H3_ALPN "\x2h3" + result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, + H3_ALPN, sizeof(H3_ALPN) - 1, + NULL, NULL); + if(result) + goto out; + + result = vquic_ctx_init(&ctx->q); + if(result) + goto out; + + result = CURLE_QUIC_CONNECT_ERROR; + Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, + &peer_addr, NULL, &peer_port, NULL, NULL); + if(!peer_addr) + goto out; + + ctx->q.local_addrlen = sizeof(ctx->q.local_addr); + rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr, + &ctx->q.local_addrlen); + if(rv == -1) + goto out; + + result = make_bio_addr(&baddr, peer_addr); + if(result) { + failf(data, "error creating BIO_ADDR from sockaddr"); + goto out; + } + + bio = BIO_new_dgram(ctx->q.sockfd, BIO_NOCLOSE); + if(!bio) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + + if(!SSL_set1_initial_peer_addr(ctx->tls.ssl, baddr)) { + failf(data, "failed to set the initial peer address"); + result = CURLE_FAILED_INIT; + goto out; + } + if(!SSL_set_blocking_mode(ctx->tls.ssl, 0)) { + failf(data, "failed to turn off blocking mode"); + result = CURLE_FAILED_INIT; + goto out; + } + + SSL_set_bio(ctx->tls.ssl, bio, bio); + bio = NULL; + SSL_set_connect_state(ctx->tls.ssl); + SSL_set_incoming_stream_policy(ctx->tls.ssl, + SSL_INCOMING_STREAM_POLICY_ACCEPT, 0); + /* setup the H3 things on top of the QUIC connection */ + result = cf_osslq_h3conn_init(ctx, ctx->tls.ssl, cf); + +out: + if(bio) + BIO_free(bio); + if(baddr) + BIO_ADDR_free(baddr); + CURL_TRC_CF(data, cf, "QUIC tls init -> %d", result); + return result; +} + +struct h3_quic_recv_ctx { + struct Curl_cfilter *cf; + struct Curl_easy *data; + struct cf_osslq_stream *s; +}; + +static ssize_t h3_quic_recv(void *reader_ctx, + unsigned char *buf, size_t len, + CURLcode *err) +{ + struct h3_quic_recv_ctx *x = reader_ctx; + size_t nread; + int rv; + + *err = CURLE_OK; + rv = SSL_read_ex(x->s->ssl, buf, len, &nread); + if(rv <= 0) { + int detail = SSL_get_error(x->s->ssl, rv); + if(detail == SSL_ERROR_WANT_READ || detail == SSL_ERROR_WANT_WRITE) { + *err = CURLE_AGAIN; + return -1; + } + else if(detail == SSL_ERROR_ZERO_RETURN) { + CURL_TRC_CF(x->data, x->cf, "[%" PRId64 "] h3_quic_recv -> EOS", + x->s->id); + x->s->recvd_eos = TRUE; + return 0; + } + else if(SSL_get_stream_read_state(x->s->ssl) == + SSL_STREAM_STATE_RESET_REMOTE) { + uint64_t app_error_code = NGHTTP3_H3_NO_ERROR; + SSL_get_stream_read_error_code(x->s->ssl, &app_error_code); + CURL_TRC_CF(x->data, x->cf, "[%" PRId64 "] h3_quic_recv -> RESET, " + "rv=%d, app_err=%" PRIu64, + x->s->id, rv, app_error_code); + if(app_error_code != NGHTTP3_H3_NO_ERROR) { + x->s->reset = TRUE; + } + x->s->recvd_eos = TRUE; + return 0; + } + else { + *err = cf_osslq_ssl_err(x->cf, x->data, detail, CURLE_RECV_ERROR); + return -1; + } + } + else { + /* CURL_TRC_CF(x->data, x->cf, "[%" PRId64 "] h3_quic_recv -> %zu bytes", + x->s->id, nread); */ + } + return (ssize_t)nread; +} + +static CURLcode cf_osslq_stream_recv(struct cf_osslq_stream *s, + struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + ssize_t nread; + struct h3_quic_recv_ctx x; + int rv, eagain = FALSE; + size_t total_recv_len = 0; + + DEBUGASSERT(s); + if(s->closed) + return CURLE_OK; + + x.cf = cf; + x.data = data; + x.s = s; + while(s->ssl && !s->closed && !eagain && + (total_recv_len < H3_STREAM_CHUNK_SIZE)) { + if(Curl_bufq_is_empty(&s->recvbuf) && !s->recvd_eos) { + while(!eagain && !s->recvd_eos && !Curl_bufq_is_full(&s->recvbuf)) { + nread = Curl_bufq_sipn(&s->recvbuf, 0, h3_quic_recv, &x, &result); + if(nread < 0) { + if(result != CURLE_AGAIN) + goto out; + result = CURLE_OK; + eagain = TRUE; + } + } + } + + /* Forward what we have to nghttp3 */ + if(!Curl_bufq_is_empty(&s->recvbuf)) { + const unsigned char *buf; + size_t blen; + + while(Curl_bufq_peek(&s->recvbuf, &buf, &blen)) { + nread = nghttp3_conn_read_stream(ctx->h3.conn, s->id, + buf, blen, 0); + CURL_TRC_CF(data, cf, "[%" PRId64 "] forward %zu bytes " + "to nghttp3 -> %zd", s->id, blen, nread); + if(nread < 0) { + failf(data, "nghttp3_conn_read_stream(len=%zu) error: %s", + blen, nghttp3_strerror((int)nread)); + result = CURLE_RECV_ERROR; + goto out; + } + /* success, `nread` is the flow for QUIC to count as "consumed", + * not sure how that will work with OpenSSL. Anyways, without error, + * all data that we passed is not owned by nghttp3. */ + Curl_bufq_skip(&s->recvbuf, blen); + total_recv_len += blen; + } + } + + /* When we forwarded everything, handle RESET/EOS */ + if(Curl_bufq_is_empty(&s->recvbuf) && !s->closed) { + result = CURLE_OK; + if(s->reset) { + uint64_t app_error; + if(!SSL_get_stream_read_error_code(s->ssl, &app_error)) { + failf(data, "SSL_get_stream_read_error_code returned error"); + result = CURLE_RECV_ERROR; + goto out; + } + rv = nghttp3_conn_close_stream(ctx->h3.conn, s->id, app_error); + s->closed = TRUE; + if(rv < 0 && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + failf(data, "nghttp3_conn_close_stream returned error: %s", + nghttp3_strerror(rv)); + result = CURLE_RECV_ERROR; + goto out; + } + } + else if(s->recvd_eos) { + rv = nghttp3_conn_close_stream(ctx->h3.conn, s->id, + NGHTTP3_H3_NO_ERROR); + s->closed = TRUE; + CURL_TRC_CF(data, cf, "[%" PRId64 "] close nghttp3 stream -> %d", + s->id, rv); + if(rv < 0 && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + failf(data, "nghttp3_conn_close_stream returned error: %s", + nghttp3_strerror(rv)); + result = CURLE_RECV_ERROR; + goto out; + } + } + } + } +out: + if(result) + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_osslq_stream_recv -> %d", + s->id, result); + return result; +} + +static CURLcode cf_progress_ingress(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + + if(!ctx->tls.ssl) + goto out; + + ERR_clear_error(); + + /* 1. Check for new incoming streams */ + while(1) { + SSL *snew = SSL_accept_stream(ctx->tls.ssl, SSL_ACCEPT_STREAM_NO_BLOCK); + if(!snew) + break; + + (void)cf_osslq_h3conn_add_stream(&ctx->h3, snew, cf, data); + } + + if(!SSL_handle_events(ctx->tls.ssl)) { + int detail = SSL_get_error(ctx->tls.ssl, 0); + result = cf_osslq_ssl_err(cf, data, detail, CURLE_RECV_ERROR); + } + + if(ctx->h3.conn) { + size_t i; + for(i = 0; i < ctx->h3.remote_ctrl_n; ++i) { + result = cf_osslq_stream_recv(&ctx->h3.remote_ctrl[i], cf, data); + if(result) + goto out; + } + } + + if(ctx->h3.conn) { + struct Curl_easy *sdata; + struct h3_stream_ctx *stream; + /* PULL all open streams */ + DEBUGASSERT(data->multi); + for(sdata = data->multi->easyp; sdata; sdata = sdata->next) { + if(sdata->conn == data->conn && CURL_WANT_RECV(sdata)) { + stream = H3_STREAM_CTX(sdata); + if(stream && !stream->closed && + !Curl_bufq_is_full(&stream->recvbuf)) { + result = cf_osslq_stream_recv(&stream->s, cf, sdata); + if(result) + goto out; + } + } + } + } + +out: + CURL_TRC_CF(data, cf, "progress_ingress -> %d", result); + return result; +} + +/* Iterate over all streams and check if blocked can be unblocked */ +static CURLcode cf_osslq_check_and_unblock(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct Curl_easy *sdata; + struct h3_stream_ctx *stream; + + if(ctx->h3.conn) { + for(sdata = data->multi->easyp; sdata; sdata = sdata->next) { + if(sdata->conn == data->conn) { + stream = H3_STREAM_CTX(sdata); + if(stream && stream->s.ssl && stream->s.send_blocked && + !SSL_want_write(stream->s.ssl)) { + nghttp3_conn_unblock_stream(ctx->h3.conn, stream->s.id); + stream->s.send_blocked = FALSE; + h3_drain_stream(cf, sdata); + CURL_TRC_CF(sdata, cf, "unblocked"); + } + } + } + } + return CURLE_OK; +} + +static CURLcode h3_send_streams(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + + if(!ctx->tls.ssl || !ctx->h3.conn) + goto out; + + for(;;) { + struct cf_osslq_stream *s = NULL; + nghttp3_vec vec[16]; + nghttp3_ssize n, i; + int64_t stream_id; + size_t written; + int eos, ok, rv; + size_t total_len, acked_len = 0; + bool blocked = FALSE; + + n = nghttp3_conn_writev_stream(ctx->h3.conn, &stream_id, &eos, + vec, ARRAYSIZE(vec)); + if(n < 0) { + failf(data, "nghttp3_conn_writev_stream returned error: %s", + nghttp3_strerror((int)n)); + result = CURLE_SEND_ERROR; + goto out; + } + if(stream_id < 0) { + result = CURLE_OK; + goto out; + } + + /* Get the stream for this data */ + s = cf_osslq_get_qstream(cf, data, stream_id); + if(!s) { + failf(data, "nghttp3_conn_writev_stream gave unknown stream %" PRId64, + stream_id); + result = CURLE_SEND_ERROR; + goto out; + } + /* Now write the data to the stream's SSL*, it may not all fit! */ + DEBUGASSERT(s->id == stream_id); + for(i = 0, total_len = 0; i < n; ++i) { + total_len += vec[i].len; + } + for(i = 0; (i < n) && !blocked; ++i) { + /* Without stream->s.ssl, we closed that already, so + * pretend the write did succeed. */ + written = vec[i].len; + ok = !s->ssl || SSL_write_ex(s->ssl, vec[i].base, vec[i].len, + &written); + if(ok) { + /* As OpenSSL buffers the data, we count this as acknowledged + * from nghttp3's point of view */ + CURL_TRC_CF(data, cf, "[%"PRId64"] send %zu bytes to QUIC ok", + s->id, vec[i].len); + acked_len += vec[i].len; + } + else { + int detail = SSL_get_error(s->ssl, 0); + switch(detail) { + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + /* QUIC blocked us from writing more */ + CURL_TRC_CF(data, cf, "[%"PRId64"] send %zu bytes to QUIC blocked", + s->id, vec[i].len); + written = 0; + nghttp3_conn_block_stream(ctx->h3.conn, s->id); + s->send_blocked = blocked = TRUE; + break; + default: + failf(data, "[%"PRId64"] send %zu bytes to QUIC, SSL error %d", + s->id, vec[i].len, detail); + result = cf_osslq_ssl_err(cf, data, detail, CURLE_SEND_ERROR); + goto out; + } + } + } + + if(acked_len > 0 || (eos && !s->send_blocked)) { + /* Since QUIC buffers the data written internally, we can tell + * nghttp3 that it can move forward on it */ + rv = nghttp3_conn_add_write_offset(ctx->h3.conn, s->id, acked_len); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + failf(data, "nghttp3_conn_add_write_offset returned error: %s\n", + nghttp3_strerror(rv)); + result = CURLE_SEND_ERROR; + goto out; + } + rv = nghttp3_conn_add_ack_offset(ctx->h3.conn, s->id, acked_len); + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { + failf(data, "nghttp3_conn_add_ack_offset returned error: %s\n", + nghttp3_strerror(rv)); + result = CURLE_SEND_ERROR; + goto out; + } + CURL_TRC_CF(data, cf, "[%" PRId64 "] forwarded %zu/%zu h3 bytes " + "to QUIC, eos=%d", s->id, acked_len, total_len, eos); + } + + if(eos && !s->send_blocked) { + /* wrote everything and H3 indicates end of stream */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] closing QUIC stream", s->id); + SSL_stream_conclude(s->ssl, 0); + } + } + +out: + CURL_TRC_CF(data, cf, "h3_send_streams -> %d", result); + return result; +} + +static CURLcode cf_progress_egress(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + + if(!ctx->tls.ssl) + goto out; + + ERR_clear_error(); + result = h3_send_streams(cf, data); + if(result) + goto out; + + if(!SSL_handle_events(ctx->tls.ssl)) { + int detail = SSL_get_error(ctx->tls.ssl, 0); + result = cf_osslq_ssl_err(cf, data, detail, CURLE_SEND_ERROR); + } + + result = cf_osslq_check_and_unblock(cf, data); + +out: + CURL_TRC_CF(data, cf, "progress_egress -> %d", result); + return result; +} + +static CURLcode check_and_set_expiry(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + struct timeval tv; + timediff_t timeoutms; + int is_infinite = TRUE; + + if(ctx->tls.ssl && + SSL_get_event_timeout(ctx->tls.ssl, &tv, &is_infinite) && + !is_infinite) { + timeoutms = curlx_tvtoms(&tv); + /* QUIC want to be called again latest at the returned timeout */ + if(timeoutms <= 0) { + result = cf_progress_ingress(cf, data); + if(result) + goto out; + result = cf_progress_egress(cf, data); + if(result) + goto out; + if(SSL_get_event_timeout(ctx->tls.ssl, &tv, &is_infinite)) { + timeoutms = curlx_tvtoms(&tv); + } + } + if(!is_infinite) { + Curl_expire(data, timeoutms, EXPIRE_QUIC); + CURL_TRC_CF(data, cf, "QUIC expiry in %ldms", (long)timeoutms); + } + } +out: + return result; +} + +static CURLcode cf_osslq_connect(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool blocking, bool *done) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + struct cf_call_data save; + struct curltime now; + int err; + + if(cf->connected) { + *done = TRUE; + return CURLE_OK; + } + + /* Connect the UDP filter first */ + if(!cf->next->connected) { + result = Curl_conn_cf_connect(cf->next, data, blocking, done); + if(result || !*done) + return result; + } + + *done = FALSE; + now = Curl_now(); + CF_DATA_SAVE(save, cf, data); + + if(ctx->reconnect_at.tv_sec && Curl_timediff(now, ctx->reconnect_at) < 0) { + /* Not time yet to attempt the next connect */ + CURL_TRC_CF(data, cf, "waiting for reconnect time"); + goto out; + } + + if(!ctx->tls.ssl) { + ctx->started_at = now; + result = cf_osslq_ctx_start(cf, data); + if(result) + goto out; + } + + if(!ctx->got_first_byte) { + int readable = SOCKET_READABLE(ctx->q.sockfd, 0); + if(readable > 0 && (readable & CURL_CSELECT_IN)) { + ctx->got_first_byte = TRUE; + ctx->first_byte_at = Curl_now(); + } + } + + ERR_clear_error(); + err = SSL_do_handshake(ctx->tls.ssl); + + if(err == 1) { + /* connected */ + ctx->handshake_at = now; + CURL_TRC_CF(data, cf, "handshake complete after %dms", + (int)Curl_timediff(now, ctx->started_at)); + result = cf_osslq_verify_peer(cf, data); + if(!result) { + CURL_TRC_CF(data, cf, "peer verified"); + cf->connected = TRUE; + cf->conn->alpn = CURL_HTTP_VERSION_3; + *done = TRUE; + connkeep(cf->conn, "HTTP/3 default"); + } + } + else { + int detail = SSL_get_error(ctx->tls.ssl, err); + switch(detail) { + case SSL_ERROR_WANT_READ: + CURL_TRC_CF(data, cf, "QUIC SSL_connect() -> WANT_RECV"); + result = Curl_vquic_tls_before_recv(&ctx->tls, cf, data); + goto out; + case SSL_ERROR_WANT_WRITE: + CURL_TRC_CF(data, cf, "QUIC SSL_connect() -> WANT_SEND"); + result = CURLE_OK; + goto out; +#ifdef SSL_ERROR_WANT_ASYNC + case SSL_ERROR_WANT_ASYNC: + CURL_TRC_CF(data, cf, "QUIC SSL_connect() -> WANT_ASYNC"); + result = CURLE_OK; + goto out; +#endif +#ifdef SSL_ERROR_WANT_RETRY_VERIFY + case SSL_ERROR_WANT_RETRY_VERIFY: + result = CURLE_OK; + goto out; +#endif + default: + result = cf_osslq_ssl_err(cf, data, detail, CURLE_COULDNT_CONNECT); + goto out; + } + } + +out: + if(result == CURLE_RECV_ERROR && ctx->tls.ssl && ctx->protocol_shutdown) { + /* When a QUIC server instance is shutting down, it may send us a + * CONNECTION_CLOSE right away. Our connection then enters the DRAINING + * state. The CONNECT may work in the near future again. Indicate + * that as a "weird" reply. */ + result = CURLE_WEIRD_SERVER_REPLY; + } + +#ifndef CURL_DISABLE_VERBOSE_STRINGS + if(result) { + const char *r_ip = NULL; + int r_port = 0; + + Curl_cf_socket_peek(cf->next, data, NULL, NULL, + &r_ip, &r_port, NULL, NULL); + infof(data, "QUIC connect to %s port %u failed: %s", + r_ip, r_port, curl_easy_strerror(result)); + } +#endif + if(!result) + result = check_and_set_expiry(cf, data); + if(result || *done) + CURL_TRC_CF(data, cf, "connect -> %d, done=%d", result, *done); + CF_DATA_RESTORE(cf, save); + return result; +} + +static ssize_t h3_stream_open(struct Curl_cfilter *cf, + struct Curl_easy *data, + const void *buf, size_t len, + CURLcode *err) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct h3_stream_ctx *stream = NULL; + struct dynhds h2_headers; + size_t nheader; + nghttp3_nv *nva = NULL; + int rc = 0; + unsigned int i; + ssize_t nwritten = -1; + nghttp3_data_reader reader; + nghttp3_data_reader *preader = NULL; + + Curl_dynhds_init(&h2_headers, 0, DYN_HTTP_REQUEST); + + *err = h3_data_setup(cf, data); + if(*err) + goto out; + stream = H3_STREAM_CTX(data); + DEBUGASSERT(stream); + if(!stream) { + *err = CURLE_FAILED_INIT; + goto out; + } + + nwritten = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, 0, err); + if(nwritten < 0) + goto out; + if(!stream->h1.done) { + /* need more data */ + goto out; + } + DEBUGASSERT(stream->h1.req); + + *err = Curl_http_req_to_h2(&h2_headers, stream->h1.req, data); + if(*err) { + nwritten = -1; + goto out; + } + /* no longer needed */ + Curl_h1_req_parse_free(&stream->h1); + + nheader = Curl_dynhds_count(&h2_headers); + nva = malloc(sizeof(nghttp3_nv) * nheader); + if(!nva) { + *err = CURLE_OUT_OF_MEMORY; + nwritten = -1; + goto out; + } + + for(i = 0; i < nheader; ++i) { + struct dynhds_entry *e = Curl_dynhds_getn(&h2_headers, i); + nva[i].name = (unsigned char *)e->name; + nva[i].namelen = e->namelen; + nva[i].value = (unsigned char *)e->value; + nva[i].valuelen = e->valuelen; + nva[i].flags = NGHTTP3_NV_FLAG_NONE; + } + + DEBUGASSERT(stream->s.id == -1); + *err = cf_osslq_stream_open(&stream->s, ctx->tls.ssl, 0, + &ctx->stream_bufcp, data); + if(*err) { + failf(data, "can't get bidi streams"); + *err = CURLE_SEND_ERROR; + goto out; + } + + switch(data->state.httpreq) { + case HTTPREQ_POST: + case HTTPREQ_POST_FORM: + case HTTPREQ_POST_MIME: + case HTTPREQ_PUT: + /* known request body size or -1 */ + if(data->state.infilesize != -1) + stream->upload_left = data->state.infilesize; + else + /* data sending without specifying the data amount up front */ + stream->upload_left = -1; /* unknown */ + break; + default: + /* there is not request body */ + stream->upload_left = 0; /* no request body */ + break; + } + + stream->send_closed = (stream->upload_left == 0); + if(!stream->send_closed) { + reader.read_data = cb_h3_read_req_body; + preader = &reader; + } + + rc = nghttp3_conn_submit_request(ctx->h3.conn, stream->s.id, + nva, nheader, preader, data); + if(rc) { + switch(rc) { + case NGHTTP3_ERR_CONN_CLOSING: + CURL_TRC_CF(data, cf, "h3sid[%"PRId64"] failed to send, " + "connection is closing", stream->s.id); + break; + default: + CURL_TRC_CF(data, cf, "h3sid[%"PRId64"] failed to send -> %d (%s)", + stream->s.id, rc, nghttp3_strerror(rc)); + break; + } + *err = CURLE_SEND_ERROR; + nwritten = -1; + goto out; + } + + if(Curl_trc_is_verbose(data)) { + infof(data, "[HTTP/3] [%" PRId64 "] OPENED stream for %s", + stream->s.id, data->state.url); + for(i = 0; i < nheader; ++i) { + infof(data, "[HTTP/3] [%" PRId64 "] [%.*s: %.*s]", stream->s.id, + (int)nva[i].namelen, nva[i].name, + (int)nva[i].valuelen, nva[i].value); + } + } + +out: + free(nva); + Curl_dynhds_free(&h2_headers); + return nwritten; +} + +static ssize_t cf_osslq_send(struct Curl_cfilter *cf, struct Curl_easy *data, + const void *buf, size_t len, CURLcode *err) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + struct cf_call_data save; + ssize_t nwritten; + CURLcode result; + + CF_DATA_SAVE(save, cf, data); + DEBUGASSERT(cf->connected); + DEBUGASSERT(ctx->tls.ssl); + DEBUGASSERT(ctx->h3.conn); + *err = CURLE_OK; + + result = cf_progress_ingress(cf, data); + if(result) { + *err = result; + nwritten = -1; + goto out; + } + + result = cf_progress_egress(cf, data); + if(result) { + *err = result; + nwritten = -1; + goto out; + } + + if(!stream || stream->s.id < 0) { + nwritten = h3_stream_open(cf, data, buf, len, err); + if(nwritten < 0) { + CURL_TRC_CF(data, cf, "failed to open stream -> %d", *err); + goto out; + } + stream = H3_STREAM_CTX(data); + } + else if(stream->upload_blocked_len) { + /* the data in `buf` has already been submitted or added to the + * buffers, but have been EAGAINed on the last invocation. */ + DEBUGASSERT(len >= stream->upload_blocked_len); + if(len < stream->upload_blocked_len) { + /* Did we get called again with a smaller `len`? This should not + * happen. We are not prepared to handle that. */ + failf(data, "HTTP/3 send again with decreased length"); + *err = CURLE_HTTP3; + nwritten = -1; + goto out; + } + nwritten = (ssize_t)stream->upload_blocked_len; + stream->upload_blocked_len = 0; + } + else if(stream->closed) { + if(stream->resp_hds_complete) { + /* Server decided to close the stream after having sent us a final + * response. This is valid if it is not interested in the request + * body. This happens on 30x or 40x responses. + * We silently discard the data sent, since this is not a transport + * error situation. */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] discarding data" + "on closed stream with response", stream->s.id); + *err = CURLE_OK; + nwritten = (ssize_t)len; + goto out; + } + CURL_TRC_CF(data, cf, "[%" PRId64 "] send_body(len=%zu) " + "-> stream closed", stream->s.id, len); + *err = CURLE_HTTP3; + nwritten = -1; + goto out; + } + else { + nwritten = Curl_bufq_write(&stream->sendbuf, buf, len, err); + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_send, add to " + "sendbuf(len=%zu) -> %zd, %d", + stream->s.id, len, nwritten, *err); + if(nwritten < 0) { + goto out; + } + + (void)nghttp3_conn_resume_stream(ctx->h3.conn, stream->s.id); + } + + result = cf_progress_egress(cf, data); + if(result) { + *err = result; + nwritten = -1; + } + + if(stream && nwritten > 0 && stream->sendbuf_len_in_flight) { + /* We have unacknowledged DATA and cannot report success to our + * caller. Instead we EAGAIN and remember how much we have already + * "written" into our various internal connection buffers. */ + stream->upload_blocked_len = nwritten; + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_send(len=%zu), " + "%zu bytes in flight -> EGAIN", stream->s.id, len, + stream->sendbuf_len_in_flight); + *err = CURLE_AGAIN; + nwritten = -1; + } + +out: + result = check_and_set_expiry(cf, data); + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_send(len=%zu) -> %zd, %d", + stream? stream->s.id : -1, len, nwritten, *err); + CF_DATA_RESTORE(cf, save); + return nwritten; +} + +static ssize_t recv_closed_stream(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct h3_stream_ctx *stream, + CURLcode *err) +{ + ssize_t nread = -1; + + (void)cf; + if(stream->reset) { + failf(data, + "HTTP/3 stream %" PRId64 " reset by server", stream->s.id); + *err = stream->resp_hds_complete? CURLE_PARTIAL_FILE : CURLE_HTTP3; + goto out; + } + else if(!stream->resp_hds_complete) { + failf(data, + "HTTP/3 stream %" PRId64 " was closed cleanly, but before getting" + " all response header fields, treated as error", + stream->s.id); + *err = CURLE_HTTP3; + goto out; + } + *err = CURLE_OK; + nread = 0; + +out: + return nread; +} + +static ssize_t cf_osslq_recv(struct Curl_cfilter *cf, struct Curl_easy *data, + char *buf, size_t len, CURLcode *err) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + ssize_t nread = -1; + struct cf_call_data save; + CURLcode result; + + (void)ctx; + CF_DATA_SAVE(save, cf, data); + DEBUGASSERT(cf->connected); + DEBUGASSERT(ctx); + DEBUGASSERT(ctx->tls.ssl); + DEBUGASSERT(ctx->h3.conn); + *err = CURLE_OK; + + if(!stream) { + *err = CURLE_RECV_ERROR; + goto out; + } + + if(!Curl_bufq_is_empty(&stream->recvbuf)) { + nread = Curl_bufq_read(&stream->recvbuf, + (unsigned char *)buf, len, err); + if(nread < 0) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] read recvbuf(len=%zu) " + "-> %zd, %d", stream->s.id, len, nread, *err); + goto out; + } + } + + result = cf_progress_ingress(cf, data); + if(result) { + *err = result; + nread = -1; + goto out; + } + + /* recvbuf had nothing before, maybe after progressing ingress? */ + if(nread < 0 && !Curl_bufq_is_empty(&stream->recvbuf)) { + nread = Curl_bufq_read(&stream->recvbuf, + (unsigned char *)buf, len, err); + if(nread < 0) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] read recvbuf(len=%zu) " + "-> %zd, %d", stream->s.id, len, nread, *err); + goto out; + } + } + + if(nread > 0) { + h3_drain_stream(cf, data); + } + else { + if(stream->closed) { + nread = recv_closed_stream(cf, data, stream, err); + goto out; + } + *err = CURLE_AGAIN; + nread = -1; + } + +out: + if(cf_progress_egress(cf, data)) { + *err = CURLE_SEND_ERROR; + nread = -1; + } + else { + CURLcode result2 = check_and_set_expiry(cf, data); + if(result2) { + *err = result2; + nread = -1; + } + } + CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_recv(len=%zu) -> %zd, %d", + stream? stream->s.id : -1, len, nread, *err); + CF_DATA_RESTORE(cf, save); + return nread; +} + +/* + * Called from transfer.c:data_pending to know if we should keep looping + * to receive more data from the connection. + */ +static bool cf_osslq_data_pending(struct Curl_cfilter *cf, + const struct Curl_easy *data) +{ + const struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + (void)cf; + return stream && !Curl_bufq_is_empty(&stream->recvbuf); +} + +static CURLcode cf_osslq_data_event(struct Curl_cfilter *cf, + struct Curl_easy *data, + int event, int arg1, void *arg2) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + struct cf_call_data save; + + CF_DATA_SAVE(save, cf, data); + (void)arg1; + (void)arg2; + switch(event) { + case CF_CTRL_DATA_SETUP: + break; + case CF_CTRL_DATA_PAUSE: + result = h3_data_pause(cf, data, (arg1 != 0)); + break; + case CF_CTRL_DATA_DETACH: + h3_data_done(cf, data); + break; + case CF_CTRL_DATA_DONE: + h3_data_done(cf, data); + break; + case CF_CTRL_DATA_DONE_SEND: { + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + if(stream && !stream->send_closed) { + stream->send_closed = TRUE; + stream->upload_left = Curl_bufq_len(&stream->sendbuf); + (void)nghttp3_conn_resume_stream(ctx->h3.conn, stream->s.id); + } + break; + } + case CF_CTRL_DATA_IDLE: { + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); + CURL_TRC_CF(data, cf, "data idle"); + if(stream && !stream->closed) { + result = check_and_set_expiry(cf, data); + } + break; + } + default: + break; + } + CF_DATA_RESTORE(cf, save); + return result; +} + +static bool cf_osslq_conn_is_alive(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool *input_pending) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + bool alive = FALSE; + struct cf_call_data save; + + CF_DATA_SAVE(save, cf, data); + *input_pending = FALSE; + if(!ctx->tls.ssl) + goto out; + + /* TODO: how to check negotiated connection idle time? */ + + if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending)) + goto out; + + alive = TRUE; + if(*input_pending) { + CURLcode result; + /* This happens before we've sent off a request and the connection is + not in use by any other transfer, there shouldn't be any data here, + only "protocol frames" */ + *input_pending = FALSE; + result = cf_progress_ingress(cf, data); + CURL_TRC_CF(data, cf, "is_alive, progress ingress -> %d", result); + alive = result? FALSE : TRUE; + } + +out: + CF_DATA_RESTORE(cf, save); + return alive; +} + +static void cf_osslq_adjust_pollset(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct easy_pollset *ps) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + + if(!ctx->tls.ssl) { + /* NOP */ + } + else if(!cf->connected) { + /* during handshake, transfer has not started yet. we always + * add our socket for polling if SSL wants to send/recv */ + Curl_pollset_set(data, ps, ctx->q.sockfd, + SSL_net_read_desired(ctx->tls.ssl), + SSL_net_write_desired(ctx->tls.ssl)); + } + else { + /* once connected, we only modify the socket if it is present. + * this avoids adding it for paused transfers. */ + bool want_recv, want_send; + Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send); + if(want_recv || want_send) { + Curl_pollset_set(data, ps, ctx->q.sockfd, + SSL_net_read_desired(ctx->tls.ssl), + SSL_net_write_desired(ctx->tls.ssl)); + } + } +} + +static CURLcode cf_osslq_query(struct Curl_cfilter *cf, + struct Curl_easy *data, + int query, int *pres1, void *pres2) +{ + struct cf_osslq_ctx *ctx = cf->ctx; + struct cf_call_data save; + + switch(query) { + case CF_QUERY_MAX_CONCURRENT: { + /* TODO: how to get this? */ + CF_DATA_SAVE(save, cf, data); + *pres1 = 100; + CURL_TRC_CF(data, cf, "query max_conncurrent -> %d", *pres1); + CF_DATA_RESTORE(cf, save); + return CURLE_OK; + } + case CF_QUERY_CONNECT_REPLY_MS: + if(ctx->got_first_byte) { + timediff_t ms = Curl_timediff(ctx->first_byte_at, ctx->started_at); + *pres1 = (ms < INT_MAX)? (int)ms : INT_MAX; + } + else + *pres1 = -1; + return CURLE_OK; + case CF_QUERY_TIMER_CONNECT: { + struct curltime *when = pres2; + if(ctx->got_first_byte) + *when = ctx->first_byte_at; + return CURLE_OK; + } + case CF_QUERY_TIMER_APPCONNECT: { + struct curltime *when = pres2; + if(cf->connected) + *when = ctx->handshake_at; + return CURLE_OK; + } + default: + break; + } + return cf->next? + cf->next->cft->query(cf->next, data, query, pres1, pres2) : + CURLE_UNKNOWN_OPTION; +} + +struct Curl_cftype Curl_cft_http3 = { + "HTTP/3", + CF_TYPE_IP_CONNECT | CF_TYPE_SSL | CF_TYPE_MULTIPLEX, + 0, + cf_osslq_destroy, + cf_osslq_connect, + cf_osslq_close, + Curl_cf_def_get_host, + cf_osslq_adjust_pollset, + cf_osslq_data_pending, + cf_osslq_send, + cf_osslq_recv, + cf_osslq_data_event, + cf_osslq_conn_is_alive, + Curl_cf_def_conn_keep_alive, + cf_osslq_query, +}; + +CURLcode Curl_cf_osslq_create(struct Curl_cfilter **pcf, + struct Curl_easy *data, + struct connectdata *conn, + const struct Curl_addrinfo *ai) +{ + struct cf_osslq_ctx *ctx = NULL; + struct Curl_cfilter *cf = NULL, *udp_cf = NULL; + CURLcode result; + + (void)data; + ctx = calloc(1, sizeof(*ctx)); + if(!ctx) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + cf_osslq_ctx_clear(ctx); + + result = Curl_cf_create(&cf, &Curl_cft_http3, ctx); + if(result) + goto out; + + result = Curl_cf_udp_create(&udp_cf, data, conn, ai, TRNSPRT_QUIC); + if(result) + goto out; + + cf->conn = conn; + udp_cf->conn = cf->conn; + udp_cf->sockindex = cf->sockindex; + cf->next = udp_cf; + +out: + *pcf = (!result)? cf : NULL; + if(result) { + if(udp_cf) + Curl_conn_cf_discard_sub(cf, udp_cf, data, TRUE); + Curl_safefree(cf); + Curl_safefree(ctx); + } + return result; +} + +bool Curl_conn_is_osslq(const struct Curl_easy *data, + const struct connectdata *conn, + int sockindex) +{ + struct Curl_cfilter *cf = conn? conn->cfilter[sockindex] : NULL; + + (void)data; + for(; cf; cf = cf->next) { + if(cf->cft == &Curl_cft_http3) + return TRUE; + if(cf->cft->flags & CF_TYPE_IP_CONNECT) + return FALSE; + } + return FALSE; +} + +/* + * Store ngtcp2 version info in this buffer. + */ +void Curl_osslq_ver(char *p, size_t len) +{ + const nghttp3_info *ht3 = nghttp3_version(0); + char tmp[128]; + Curl_ssl_version(tmp, sizeof(tmp)-1); + (void)msnprintf(p, len, "%s nghttp3/%s", tmp, ht3->version_str); +} + +#endif /* USE_OPENSSL_QUIC && USE_NGHTTP3 */ diff --git a/lib/vquic/curl_osslq.h b/lib/vquic/curl_osslq.h new file mode 100644 index 0000000000..0e12d7023e --- /dev/null +++ b/lib/vquic/curl_osslq.h @@ -0,0 +1,51 @@ +#ifndef HEADER_CURL_VQUIC_CURL_OSSLQ_H +#define HEADER_CURL_VQUIC_CURL_OSSLQ_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ + +#include "curl_setup.h" + +#if defined(USE_OPENSSL_QUIC) && defined(USE_NGHTTP3) + +#ifdef HAVE_NETINET_UDP_H +#include +#endif + +struct Curl_cfilter; + +#include "urldata.h" + +void Curl_osslq_ver(char *p, size_t len); + +CURLcode Curl_cf_osslq_create(struct Curl_cfilter **pcf, + struct Curl_easy *data, + struct connectdata *conn, + const struct Curl_addrinfo *ai); + +bool Curl_conn_is_osslq(const struct Curl_easy *data, + const struct connectdata *conn, + int sockindex); +#endif + +#endif /* HEADER_CURL_VQUIC_CURL_OSSLQ_H */ diff --git a/lib/vquic/vquic-tls.c b/lib/vquic/vquic-tls.c index e9f16773b9..cc7794e405 100644 --- a/lib/vquic/vquic-tls.c +++ b/lib/vquic/vquic-tls.c @@ -97,7 +97,11 @@ static CURLcode curl_ossl_init_ctx(struct quic_tls_ctx *ctx, CURLcode result = CURLE_FAILED_INIT; DEBUGASSERT(!ctx->ssl_ctx); +#ifdef USE_OPENSSL_QUIC + ctx->ssl_ctx = SSL_CTX_new(OSSL_QUIC_client_method()); +#else ctx->ssl_ctx = SSL_CTX_new(TLS_method()); +#endif if(!ctx->ssl_ctx) { result = CURLE_OUT_OF_MEMORY; goto out; @@ -216,7 +220,9 @@ static CURLcode curl_ossl_init_ssl(struct quic_tls_ctx *ctx, SSL_set_app_data(ctx->ssl, user_data); SSL_set_connect_state(ctx->ssl); +#ifndef USE_OPENSSL_QUIC SSL_set_quic_use_legacy_codepoint(ctx->ssl, 0); +#endif if(alpn) SSL_set_alpn_protos(ctx->ssl, (const uint8_t *)alpn, (int)alpn_len); diff --git a/lib/vquic/vquic.c b/lib/vquic/vquic.c index 7da4641468..612d25bb0a 100644 --- a/lib/vquic/vquic.c +++ b/lib/vquic/vquic.c @@ -46,6 +46,7 @@ #include "curl_trc.h" #include "curl_msh3.h" #include "curl_ngtcp2.h" +#include "curl_osslq.h" #include "curl_quiche.h" #include "rand.h" #include "vquic.h" @@ -74,6 +75,8 @@ void Curl_quic_ver(char *p, size_t len) { #if defined(USE_NGTCP2) && defined(USE_NGHTTP3) Curl_ngtcp2_ver(p, len); +#elif defined(USE_OPENSSL_QUIC) && defined(USE_NGHTTP3) + Curl_osslq_ver(p, len); #elif defined(USE_QUICHE) Curl_quiche_ver(p, len); #elif defined(USE_MSH3) @@ -608,6 +611,8 @@ CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf, DEBUGASSERT(transport == TRNSPRT_QUIC); #if defined(USE_NGTCP2) && defined(USE_NGHTTP3) return Curl_cf_ngtcp2_create(pcf, data, conn, ai); +#elif defined(USE_OPENSSL_QUIC) && defined(USE_NGHTTP3) + return Curl_cf_osslq_create(pcf, data, conn, ai); #elif defined(USE_QUICHE) return Curl_cf_quiche_create(pcf, data, conn, ai); #elif defined(USE_MSH3) @@ -627,6 +632,8 @@ bool Curl_conn_is_http3(const struct Curl_easy *data, { #if defined(USE_NGTCP2) && defined(USE_NGHTTP3) return Curl_conn_is_ngtcp2(data, conn, sockindex); +#elif defined(USE_OPENSSL_QUIC) && defined(USE_NGHTTP3) + return Curl_conn_is_osslq(data, conn, sockindex); #elif defined(USE_QUICHE) return Curl_conn_is_quiche(data, conn, sockindex); #elif defined(USE_MSH3) diff --git a/m4/curl-openssl.m4 b/m4/curl-openssl.m4 index 3f3b3c238e..c329b760fa 100644 --- a/m4/curl-openssl.m4 +++ b/m4/curl-openssl.m4 @@ -422,4 +422,23 @@ AS_HELP_STRING([--disable-openssl-auto-load-config],[Disable automatic loading o ]) fi +dnl --- +dnl We may use OpenSSL QUIC. +dnl --- +if test "$OPENSSL_ENABLED" = "1"; then + AC_MSG_CHECKING([for QUIC support in OpenSSL]) + AC_LINK_IFELSE([ + AC_LANG_PROGRAM([[ +#include + ]],[[ + OSSL_QUIC_client_method(); + ]]) + ],[ + AC_MSG_RESULT([yes]) + AC_DEFINE(HAVE_OPENSSL_QUIC, 1, [if you have the functions OSSL_QUIC_client_method]) + AC_SUBST(HAVE_OPENSSL_QUIC, [1]) + ],[ + AC_MSG_RESULT([no]) + ]) +fi ]) diff --git a/tests/http/scorecard.py b/tests/http/scorecard.py index ae466129c0..1b539ffdc4 100644 --- a/tests/http/scorecard.py +++ b/tests/http/scorecard.py @@ -332,7 +332,7 @@ class ScoreCard: p['name'] = 'h3' if not self.env.have_h3_curl(): raise ScoreCardException('curl does not support HTTP/3') - for lib in ['ngtcp2', 'quiche', 'msh3']: + for lib in ['ngtcp2', 'quiche', 'msh3', 'nghttp3']: if self.env.curl_uses_lib(lib): p['implementation'] = lib break diff --git a/tests/http/test_08_caddy.py b/tests/http/test_08_caddy.py index c95d8457a8..c35bd27b76 100644 --- a/tests/http/test_08_caddy.py +++ b/tests/http/test_08_caddy.py @@ -163,3 +163,20 @@ class TestCaddy: assert r.total_connects > 1, r.dump_logs() else: assert r.total_connects == 1, r.dump_logs() + + # upload data parallel, check that they were echoed + @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs") + @pytest.mark.parametrize("proto", ['h2', 'h3']) + def test_08_06_upload_parallel(self, env: Env, caddy, repeat, proto): + if proto == 'h3' and not env.have_h3(): + pytest.skip("h3 not supported") + if proto == 'h3' and env.curl_uses_lib('msh3'): + pytest.skip("msh3 stalls here") + # limit since we use a separate connection in h1 + count = 20 + data = '0123456789' + curl = CurlClient(env=env) + url = f'https://{env.domain1}:{caddy.port}/data10.data?[0-{count-1}]' + r = curl.http_upload(urls=[url], data=data, alpn_proto=proto, + extra_args=['--parallel']) + r.check_stats(count=count, http_status=200, exitcode=0) diff --git a/tests/http/test_14_auth.py b/tests/http/test_14_auth.py index 0cc0f925f2..6d3db59312 100644 --- a/tests/http/test_14_auth.py +++ b/tests/http/test_14_auth.py @@ -133,5 +133,7 @@ class TestAuth: r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, extra_args=[ '--basic', '--user', f'test:{password}' ]) - # request was never sent - r.check_response(exitcode=55, http_status=0) + # Depending on protocl, we might have an error sending or + # the server might shutdown the connection and we see the error + # on receiving + assert r.exit_code in [55, 56], f'{self.dump_logs()}' diff --git a/tests/http/testenv/nghttpx.py b/tests/http/testenv/nghttpx.py index 4be060b567..9544ce05d9 100644 --- a/tests/http/testenv/nghttpx.py +++ b/tests/http/testenv/nghttpx.py @@ -129,7 +129,9 @@ class Nghttpx: try_until = datetime.now() + timeout while datetime.now() < try_until: check_url = f'https://{self.env.domain1}:{self._port}/' - r = curl.http_get(url=check_url, extra_args=['--http3-only']) + r = curl.http_get(url=check_url, extra_args=[ + '--http3-only', '--connect-timeout', '1' + ]) if r.exit_code != 0: return True log.debug(f'waiting for nghttpx to stop responding: {r}') @@ -143,7 +145,8 @@ class Nghttpx: while datetime.now() < try_until: check_url = f'https://{self.env.domain1}:{self._port}/' r = curl.http_get(url=check_url, extra_args=[ - '--http3-only', '--trace', 'curl.trace', '--trace-time' + '--http3-only', '--trace', 'curl.trace', '--trace-time', + '--connect-timeout', '1' ]) if r.exit_code == 0: return True @@ -193,6 +196,7 @@ class NghttpxQuic(Nghttpx): f'--frontend-http3-max-window-size=10M', f'--frontend-http3-connection-window-size=10M', f'--frontend-http3-max-connection-window-size=100M', + # f'--frontend-quic-debug-log', ] ngerr = open(self._stderr, 'a') self._process = subprocess.Popen(args=args, stderr=ngerr)