curl/lib/vquic/curl_quiche.c
Viktor Szakats 3058ed3df8
lib: use lib source directory as base include path
Backtrack on previous change that aimed to solve the wrong `share.h`
being included. It turns out it did not fix this issue. At the same time
it introduced relative header filenames and the need to include the same
headers differently depending on the source files' location, reducing
readability and editability.

Replace this method by re-adding curl's lib source directory to the
header path and addressing headers by the their full, relative name to
that base directory. Aligning with this method already used in src and
tests.

With these advantages:
- makes includes easier to read, recognize, grep, sort, write, and copy
  between sources,
- syncs the way these headers are included across curl components,
- avoids the ambiguity between system `schannel.h`, `rustls.h` vs.
  local headers using the same names in `lib/vtls`,
- silences clang-tidy `readability-duplicate-include` checker, which
  detects the above issue,
  Ref: https://clang.llvm.org/extra/clang-tidy/checks/readability/duplicate-include.html
- possibly silences TIOBE coding standard warnings:
  `6.10.2.a: Don't use relative paths in #include statements.`
- long shot: it works well with concatenated test sources, for
  clang-tidy-friendly custom unity builds. Ref: #20667

Slight downside: it's not enforced.

If there happens to be a collision between a local `lib/*.h` header and
a system one, the solution is to rename (possibly with its `.c`
counterpart) into the `curl_` namespace. This is also the method used by
curl in the past.

Also:
- curlx/inet_pton: reduce scope of an include.
- toolx/tool_time: apply this to an include, and update VS project
  files accordingly. Also dropping unnecessary lib/curlx header path.
- clang-tidy: enable `readability-duplicate-include`.

Follow-up to 3887069c66 #19676
Follow-up to 625f2c1644 #16991 #16949

Closes #20623
2026-02-23 16:00:42 +01:00

1673 lines
50 KiB
C

/***************************************************************************
* _ _ ____ _
* 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(USE_QUICHE)
#include <quiche.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include "bufq.h"
#include "uint-hash.h"
#include "urldata.h"
#include "cfilters.h"
#include "cf-socket.h"
#include "curl_trc.h"
#include "rand.h"
#include "multiif.h"
#include "connect.h"
#include "progress.h"
#include "select.h"
#include "http1.h"
#include "vquic/vquic.h"
#include "vquic/vquic_int.h"
#include "vquic/vquic-tls.h"
#include "vquic/curl_quiche.h"
#include "transfer.h"
#include "url.h"
#include "bufref.h"
#include "vtls/openssl.h"
#include "vtls/keylog.h"
#include "vtls/vtls.h"
/* HTTP/3 error values defined in RFC 9114, ch. 8.1 */
#define CURL_H3_NO_ERROR 0x0100
#define MAX_PKT_BURST 10
#define QUIC_MAX_STREAMS 100
#define H3_STREAM_WINDOW_SIZE (1024 * 128)
#define H3_STREAM_CHUNK_SIZE (1024 * 16)
/* 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)
/*
* Store quiche version info in this buffer.
*/
void Curl_quiche_ver(char *p, size_t len)
{
(void)curl_msnprintf(p, len, "quiche/%s", quiche_version());
}
struct cf_quiche_ctx {
struct cf_quic_ctx q;
struct ssl_peer peer;
struct curl_tls_ctx tls;
quiche_conn *qconn;
quiche_config *cfg;
quiche_h3_conn *h3c;
quiche_h3_config *h3config;
uint8_t scid[QUICHE_MAX_CONN_ID_LEN];
struct curltime started_at; /* time the current attempt started */
struct curltime handshake_at; /* time connect handshake finished */
struct uint_hash streams; /* hash `data->mid` to `stream_ctx` */
struct dynbuf h1hdr; /* temp buffer for header construction */
struct bufq writebuf; /* temp buffer for writing bodies */
curl_off_t data_recvd;
BIT(initialized);
BIT(goaway); /* got GOAWAY from server */
BIT(x509_store_setup); /* if x509 store has been set up */
BIT(shutdown_started); /* queued shutdown packets */
};
#ifdef DEBUG_QUICHE
/* initialize debug log callback only once */
static int debug_log_init = 0;
static void quiche_debug_log(const char *line, void *argp)
{
(void)argp;
curl_mfprintf(stderr, "%s\n", line);
}
#endif
static void h3_stream_hash_free(unsigned int id, void *stream);
static void cf_quiche_ctx_init(struct cf_quiche_ctx *ctx)
{
DEBUGASSERT(!ctx->initialized);
#ifdef DEBUG_QUICHE
if(!debug_log_init) {
quiche_enable_debug_logging(quiche_debug_log, NULL);
debug_log_init = 1;
}
#endif
curlx_dyn_init(&ctx->h1hdr, CURL_MAX_HTTP_HEADER);
Curl_uint32_hash_init(&ctx->streams, 63, h3_stream_hash_free);
Curl_bufq_init2(&ctx->writebuf, H3_STREAM_CHUNK_SIZE, H3_STREAM_RECV_CHUNKS,
BUFQ_OPT_SOFT_LIMIT);
ctx->data_recvd = 0;
ctx->initialized = TRUE;
}
static void cf_quiche_ctx_free(struct cf_quiche_ctx *ctx)
{
if(ctx && ctx->initialized) {
/* quiche just freed it */
ctx->tls.ossl.ssl = NULL;
Curl_vquic_tls_cleanup(&ctx->tls);
Curl_ssl_peer_cleanup(&ctx->peer);
vquic_ctx_free(&ctx->q);
Curl_uint32_hash_destroy(&ctx->streams);
curlx_dyn_free(&ctx->h1hdr);
Curl_bufq_free(&ctx->writebuf);
}
curlx_free(ctx);
}
static void cf_quiche_ctx_close(struct cf_quiche_ctx *ctx)
{
if(ctx->h3c) {
quiche_h3_conn_free(ctx->h3c);
ctx->h3c = NULL;
}
if(ctx->h3config) {
quiche_h3_config_free(ctx->h3config);
ctx->h3config = NULL;
}
if(ctx->qconn) {
quiche_conn_free(ctx->qconn);
ctx->qconn = NULL;
}
if(ctx->cfg) {
quiche_config_free(ctx->cfg);
ctx->cfg = NULL;
}
}
static CURLcode cf_flush_egress(struct Curl_cfilter *cf,
struct Curl_easy *data);
/**
* All about the H3 internals of a stream
*/
struct h3_stream_ctx {
uint64_t id; /* HTTP/3 protocol stream identifier */
struct h1_req_parser h1; /* h1 request parsing */
uint64_t error3; /* HTTP/3 stream error code */
int status_code; /* HTTP status code */
CURLcode xfer_result; /* result from cf_quiche_write_(hd/body) */
BIT(opened); /* TRUE after stream has been opened */
BIT(closed); /* TRUE on stream close */
BIT(reset); /* TRUE on stream reset */
BIT(send_closed); /* stream is locally closed */
BIT(resp_hds_complete); /* final response has been received */
BIT(resp_got_header); /* TRUE when h3 stream has recvd some HEADER */
BIT(quic_flow_blocked); /* stream is blocked by QUIC flow control */
};
static void h3_stream_ctx_free(struct h3_stream_ctx *stream)
{
Curl_h1_req_parse_free(&stream->h1);
curlx_free(stream);
}
static void h3_stream_hash_free(unsigned int id, void *stream)
{
(void)id;
DEBUGASSERT(stream);
h3_stream_ctx_free((struct h3_stream_ctx *)stream);
}
typedef bool cf_quiche_svisit(struct Curl_cfilter *cf,
struct Curl_easy *sdata,
struct h3_stream_ctx *stream,
void *user_data);
struct cf_quiche_visit_ctx {
struct Curl_cfilter *cf;
struct Curl_multi *multi;
cf_quiche_svisit *cb;
void *user_data;
};
static bool cf_quiche_stream_do(uint32_t mid, void *val, void *user_data)
{
struct cf_quiche_visit_ctx *vctx = user_data;
struct h3_stream_ctx *stream = val;
struct Curl_easy *sdata = Curl_multi_get_easy(vctx->multi, mid);
if(sdata)
return vctx->cb(vctx->cf, sdata, stream, vctx->user_data);
return TRUE;
}
static void cf_quiche_for_all_streams(struct Curl_cfilter *cf,
struct Curl_multi *multi,
cf_quiche_svisit *do_cb,
void *user_data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct cf_quiche_visit_ctx vctx;
vctx.cf = cf;
vctx.multi = multi;
vctx.cb = do_cb;
vctx.user_data = user_data;
Curl_uint32_hash_visit(&ctx->streams, cf_quiche_stream_do, &vctx);
}
static bool cf_quiche_do_resume(struct Curl_cfilter *cf,
struct Curl_easy *sdata,
struct h3_stream_ctx *stream,
void *user_data)
{
(void)user_data;
if(stream->quic_flow_blocked) {
stream->quic_flow_blocked = FALSE;
Curl_multi_mark_dirty(sdata);
CURL_TRC_CF(sdata, cf, "[%" PRIu64 "] unblock", stream->id);
}
return TRUE;
}
static bool cf_quiche_do_expire(struct Curl_cfilter *cf,
struct Curl_easy *sdata,
struct h3_stream_ctx *stream,
void *user_data)
{
(void)stream;
(void)user_data;
CURL_TRC_CF(sdata, cf, "conn closed, mark as dirty");
stream->xfer_result = CURLE_SEND_ERROR;
Curl_multi_mark_dirty(sdata);
return TRUE;
}
static CURLcode h3_data_setup(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
if(stream)
return CURLE_OK;
stream = curlx_calloc(1, sizeof(*stream));
if(!stream)
return CURLE_OUT_OF_MEMORY;
stream->id = -1;
Curl_h1_req_parse_init(&stream->h1, H1_PARSE_DEFAULT_MAX_LINE_LEN);
if(!Curl_uint32_hash_set(&ctx->streams, data->mid, stream)) {
h3_stream_ctx_free(stream);
return CURLE_OUT_OF_MEMORY;
}
return CURLE_OK;
}
static void cf_quiche_stream_close(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h3_stream_ctx *stream)
{
struct cf_quiche_ctx *ctx = cf->ctx;
CURLcode result;
if(ctx->qconn && !stream->closed) {
quiche_conn_stream_shutdown(ctx->qconn, stream->id,
QUICHE_SHUTDOWN_READ, CURL_H3_NO_ERROR);
if(!stream->send_closed) {
quiche_conn_stream_shutdown(ctx->qconn, stream->id,
QUICHE_SHUTDOWN_WRITE, CURL_H3_NO_ERROR);
stream->send_closed = TRUE;
}
stream->closed = TRUE;
result = cf_flush_egress(cf, data);
if(result)
CURL_TRC_CF(data, cf, "[%" PRIu64 "] stream close, flush egress -> %d",
stream->id, result);
}
}
static void h3_data_done(struct Curl_cfilter *cf, struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
(void)cf;
if(stream) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] easy handle is done", stream->id);
cf_quiche_stream_close(cf, data, stream);
Curl_uint32_hash_remove(&ctx->streams, data->mid);
}
}
static void cf_quiche_expire_conn_closed(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
DEBUGASSERT(data->multi);
CURL_TRC_CF(data, cf, "conn closed, expire all transfers");
cf_quiche_for_all_streams(cf, data->multi, cf_quiche_do_expire, NULL);
}
static void cf_quiche_write_hd(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h3_stream_ctx *stream,
const char *buf, size_t blen, bool eos)
{
/* This function returns no error intentionally, but records
* the result at the stream, skipping further writes once the
* `result` of the transfer is known.
* The stream is subsequently cancelled "higher up" in the filter's
* send/recv callbacks. Closing the stream here leads to SEND/RECV
* errors in other places that then overwrite the transfer's result. */
if(!stream->xfer_result) {
stream->xfer_result = Curl_xfer_write_resp_hd(data, buf, blen, eos);
if(stream->xfer_result)
CURL_TRC_CF(data, cf, "[%" PRId64 "] error %d writing %zu "
"bytes of headers", stream->id, stream->xfer_result, blen);
}
}
struct cb_ctx {
struct Curl_cfilter *cf;
struct Curl_easy *data;
struct h3_stream_ctx *stream;
};
static bool is_valid_h3_header(const uint8_t *hdr, size_t hlen)
{
while(hlen--) {
switch(*hdr++) {
case '\n':
case '\r':
case '\0':
return FALSE;
}
}
return TRUE;
}
static int cb_each_header(uint8_t *name, size_t name_len,
uint8_t *value, size_t value_len,
void *argp)
{
struct cb_ctx *x = argp;
struct Curl_cfilter *cf = x->cf;
struct Curl_easy *data = x->data;
struct h3_stream_ctx *stream = x->stream;
struct cf_quiche_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
if(!stream || stream->xfer_result)
return 1; /* abort iteration */
if((name_len == 7) && !strncmp(HTTP_PSEUDO_STATUS, (char *)name, 7) &&
is_valid_h3_header(value, value_len)) {
curlx_dyn_reset(&ctx->h1hdr);
result = Curl_http_decode_status(&stream->status_code,
(const char *)value, value_len);
if(!result)
result = curlx_dyn_addn(&ctx->h1hdr, STRCONST("HTTP/3 "));
if(!result)
result = curlx_dyn_addn(&ctx->h1hdr, (const char *)value, value_len);
if(!result)
result = curlx_dyn_addn(&ctx->h1hdr, STRCONST(" \r\n"));
if(!result)
cf_quiche_write_hd(cf, data, stream, curlx_dyn_ptr(&ctx->h1hdr),
curlx_dyn_len(&ctx->h1hdr), FALSE);
CURL_TRC_CF(data, cf, "[%" PRId64 "] status: %s",
stream->id, curlx_dyn_ptr(&ctx->h1hdr));
}
else {
if(is_valid_h3_header(value, value_len) &&
is_valid_h3_header(name, name_len)) {
/* store as an HTTP1-style header */
CURL_TRC_CF(data, cf, "[%" PRId64 "] header: %.*s: %.*s",
stream->id, (int)name_len, name,
(int)value_len, value);
curlx_dyn_reset(&ctx->h1hdr);
result = curlx_dyn_addn(&ctx->h1hdr, (const char *)name, name_len);
if(!result)
result = curlx_dyn_addn(&ctx->h1hdr, STRCONST(": "));
if(!result)
result = curlx_dyn_addn(&ctx->h1hdr, (const char *)value, value_len);
if(!result)
result = curlx_dyn_addn(&ctx->h1hdr, STRCONST("\r\n"));
if(!result)
cf_quiche_write_hd(cf, data, stream, curlx_dyn_ptr(&ctx->h1hdr),
curlx_dyn_len(&ctx->h1hdr), FALSE);
}
else
CURL_TRC_CF(x->data, x->cf, "[%" PRIu64 "] ignore %zu bytes bad header",
stream->id, value_len + name_len);
}
if(result) {
CURL_TRC_CF(x->data, x->cf, "[%" PRIu64 "] on header error %d",
stream->id, result);
if(!stream->xfer_result)
stream->xfer_result = result;
}
return stream->xfer_result ? 1 : 0;
}
static CURLcode stream_resp_read(void *reader_ctx,
unsigned char *buf, size_t len,
size_t *pnread)
{
struct cb_ctx *x = reader_ctx;
struct cf_quiche_ctx *ctx = x->cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, x->data);
ssize_t nread;
*pnread = 0;
if(!stream)
return CURLE_RECV_ERROR;
nread = quiche_h3_recv_body(ctx->h3c, ctx->qconn, stream->id, buf, len);
if(!curlx_sztouz(nread, pnread))
return CURLE_AGAIN;
return CURLE_OK;
}
static void cf_quiche_flush_body(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h3_stream_ctx *stream)
{
struct cf_quiche_ctx *ctx = cf->ctx;
const uint8_t *buf;
size_t blen;
while(stream && !stream->xfer_result) {
if(Curl_bufq_peek(&ctx->writebuf, &buf, &blen)) {
stream->xfer_result = Curl_xfer_write_resp(
data, (const char *)buf, blen, FALSE);
Curl_bufq_skip(&ctx->writebuf, blen);
if(stream->xfer_result) {
CURL_TRC_CF(data, cf, "[%" PRId64 "] error %d writing %zu bytes"
" of data", stream->id, stream->xfer_result, blen);
}
}
else
break;
}
Curl_bufq_reset(&ctx->writebuf);
}
static void cf_quiche_recv_body(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h3_stream_ctx *stream)
{
struct cf_quiche_ctx *ctx = cf->ctx;
size_t nread;
struct cb_ctx cb_ctx;
CURLcode result = CURLE_OK;
if(!stream)
return;
/* Even when the transfer has already errored, we need to receive
* the data from quiche, as quiche will otherwise get stuck and
* raise events to receive over and over again. */
cb_ctx.cf = cf;
cb_ctx.data = data;
cb_ctx.stream = stream;
Curl_bufq_reset(&ctx->writebuf);
while(!result) {
result = Curl_bufq_slurp(&ctx->writebuf,
stream_resp_read, &cb_ctx, &nread);
if(!result)
cf_quiche_flush_body(cf, data, stream);
else if(result == CURLE_AGAIN)
break;
else if(result) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] recv_body error %d",
stream->id, result);
failf(data, "[%" PRIu64 "] Error %d in HTTP/3 response body for stream",
stream->id, result);
stream->closed = TRUE;
stream->reset = TRUE;
stream->send_closed = TRUE;
if(!stream->xfer_result)
stream->xfer_result = result;
}
}
cf_quiche_flush_body(cf, data, stream);
}
static void cf_quiche_process_ev(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h3_stream_ctx *stream,
quiche_h3_event *ev)
{
if(!stream)
return;
switch(quiche_h3_event_type(ev)) {
case QUICHE_H3_EVENT_HEADERS: {
struct cb_ctx cb_ctx;
stream->resp_got_header = TRUE;
cb_ctx.cf = cf;
cb_ctx.data = data;
cb_ctx.stream = stream;
quiche_h3_event_for_each_header(ev, cb_each_header, &cb_ctx);
CURL_TRC_CF(data, cf, "[%" PRIu64 "] <- [HEADERS]", stream->id);
Curl_multi_mark_dirty(data);
break;
}
case QUICHE_H3_EVENT_DATA:
if(!stream->resp_hds_complete) {
stream->resp_hds_complete = TRUE;
cf_quiche_write_hd(cf, data, stream, "\r\n", 2, FALSE);
}
cf_quiche_recv_body(cf, data, stream);
CURL_TRC_CF(data, cf, "[%" PRIu64 "] <- [DATA]", stream->id);
Curl_multi_mark_dirty(data);
break;
case QUICHE_H3_EVENT_RESET:
CURL_TRC_CF(data, cf, "[%" PRIu64 "] RESET", stream->id);
stream->closed = TRUE;
stream->reset = TRUE;
stream->send_closed = TRUE;
Curl_multi_mark_dirty(data);
break;
case QUICHE_H3_EVENT_FINISHED:
CURL_TRC_CF(data, cf, "[%" PRIu64 "] CLOSED", stream->id);
if(!stream->resp_hds_complete) {
stream->resp_hds_complete = TRUE;
cf_quiche_write_hd(cf, data, stream, "\r\n", 2, TRUE);
}
stream->closed = TRUE;
Curl_multi_mark_dirty(data);
break;
case QUICHE_H3_EVENT_GOAWAY:
CURL_TRC_CF(data, cf, "[%" PRIu64 "] <- [GOAWAY]", stream->id);
break;
default:
CURL_TRC_CF(data, cf, "[%" PRIu64 "] recv, unhandled event %d",
stream->id, quiche_h3_event_type(ev));
break;
}
}
struct cf_quich_disp_ctx {
uint64_t stream_id;
struct Curl_cfilter *cf;
struct Curl_multi *multi;
quiche_h3_event *ev;
};
static bool cf_quiche_disp_event(uint32_t mid, void *val, void *user_data)
{
struct cf_quich_disp_ctx *dctx = user_data;
struct h3_stream_ctx *stream = val;
if(stream->id == dctx->stream_id) {
struct Curl_easy *sdata = Curl_multi_get_easy(dctx->multi, mid);
if(sdata)
cf_quiche_process_ev(dctx->cf, sdata, stream, dctx->ev);
return FALSE; /* stop iterating */
}
return TRUE;
}
static CURLcode cf_poll_events(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = NULL;
quiche_h3_event *ev;
/* Take in the events and distribute them to the transfers. */
while(ctx->h3c) {
int64_t rv = quiche_h3_conn_poll(ctx->h3c, ctx->qconn, &ev);
if(rv == QUICHE_H3_ERR_DONE) {
break;
}
else if(rv < 0) {
CURL_TRC_CF(data, cf, "error poll: %" PRId64, rv);
return CURLE_HTTP3;
}
else {
stream = H3_STREAM_CTX(ctx, data);
if(stream && stream->id == (uint64_t)rv) {
/* event for calling transfer */
cf_quiche_process_ev(cf, data, stream, ev);
quiche_h3_event_free(ev);
if(stream->xfer_result)
return stream->xfer_result;
}
else {
/* another transfer, do not return errors, as they are not for
* the calling transfer */
struct cf_quich_disp_ctx dctx;
dctx.stream_id = (uint64_t)rv;
dctx.cf = cf;
dctx.multi = data->multi;
dctx.ev = ev;
Curl_uint32_hash_visit(&ctx->streams, cf_quiche_disp_event, &dctx);
quiche_h3_event_free(ev);
}
}
}
return CURLE_OK;
}
struct recv_ctx {
struct Curl_cfilter *cf;
struct Curl_easy *data;
int pkts;
};
static CURLcode cf_quiche_recv_pkts(const unsigned char *buf, size_t buflen,
size_t gso_size,
struct sockaddr_storage *remote_addr,
socklen_t remote_addrlen, int ecn,
void *userp)
{
struct recv_ctx *r = userp;
struct cf_quiche_ctx *ctx = r->cf->ctx;
quiche_recv_info recv_info;
size_t pktlen, offset, nread;
ssize_t rv;
(void)ecn;
recv_info.to = (struct sockaddr *)&ctx->q.local_addr;
recv_info.to_len = ctx->q.local_addrlen;
recv_info.from = (struct sockaddr *)remote_addr;
recv_info.from_len = remote_addrlen;
for(offset = 0; offset < buflen; offset += gso_size) {
pktlen = ((offset + gso_size) <= buflen) ? gso_size : (buflen - offset);
rv = quiche_conn_recv(ctx->qconn,
(unsigned char *)CURL_UNCONST(buf + offset),
pktlen, &recv_info);
if(!curlx_sztouz(rv, &nread)) {
if(QUICHE_ERR_DONE == rv) {
if(quiche_conn_is_draining(ctx->qconn)) {
CURL_TRC_CF(r->data, r->cf, "ingress, connection is draining");
return CURLE_RECV_ERROR;
}
if(quiche_conn_is_closed(ctx->qconn)) {
CURL_TRC_CF(r->data, r->cf, "ingress, connection is closed");
return CURLE_RECV_ERROR;
}
CURL_TRC_CF(r->data, r->cf, "ingress, quiche is DONE");
return CURLE_OK;
}
else if(QUICHE_ERR_TLS_FAIL == rv) {
long verify_ok = SSL_get_verify_result(ctx->tls.ossl.ssl);
if(verify_ok != X509_V_OK) {
failf(r->data, "SSL certificate problem: %s",
X509_verify_cert_error_string(verify_ok));
return CURLE_PEER_FAILED_VERIFICATION;
}
failf(r->data, "ingress, quiche reports TLS fail");
return CURLE_RECV_ERROR;
}
else {
failf(r->data, "quiche reports error %zd on receive", rv);
return CURLE_RECV_ERROR;
}
}
else if(nread < pktlen) {
CURL_TRC_CF(r->data, r->cf, "ingress, quiche only read %zu/%zu bytes",
nread, pktlen);
}
++r->pkts;
}
return CURLE_OK;
}
static CURLcode cf_process_ingress(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct recv_ctx rctx;
CURLcode result;
DEBUGASSERT(ctx->qconn);
result = Curl_vquic_tls_before_recv(&ctx->tls, cf, data);
if(result)
return result;
rctx.cf = cf;
rctx.data = data;
rctx.pkts = 0;
result = vquic_recv_packets(cf, data, &ctx->q, 1000,
cf_quiche_recv_pkts, &rctx);
if(result)
return result;
if(rctx.pkts > 0) {
/* quiche digested ingress packets. It might have opened flow control
* windows again. */
DEBUGASSERT(data->multi);
cf_quiche_for_all_streams(cf, data->multi, cf_quiche_do_resume, NULL);
}
return cf_poll_events(cf, data);
}
struct read_ctx {
struct Curl_cfilter *cf;
struct Curl_easy *data;
quiche_send_info send_info;
};
static CURLcode read_pkt_to_send(void *userp,
unsigned char *buf, size_t buflen,
size_t *pnread)
{
struct read_ctx *x = userp;
struct cf_quiche_ctx *ctx = x->cf->ctx;
ssize_t rv;
*pnread = 0;
rv = quiche_conn_send(ctx->qconn, buf, buflen, &x->send_info);
if(rv == QUICHE_ERR_DONE)
return CURLE_AGAIN;
if(!curlx_sztouz(rv, pnread)) {
failf(x->data, "quiche_conn_send returned %zd", rv);
return CURLE_SEND_ERROR;
}
return CURLE_OK;
}
/*
* flush_egress drains the buffers and sends off data.
* Calls failf() on errors.
*/
static CURLcode cf_flush_egress(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
size_t nread;
CURLcode result;
int64_t expiry_ns;
int64_t timeout_ns;
struct read_ctx readx;
size_t pkt_count, gsolen;
expiry_ns = quiche_conn_timeout_as_nanos(ctx->qconn);
if(!expiry_ns) {
quiche_conn_on_timeout(ctx->qconn);
if(quiche_conn_is_closed(ctx->qconn)) {
if(quiche_conn_is_timed_out(ctx->qconn))
failf(data, "connection closed by idle timeout");
else
failf(data, "connection closed by server");
/* Connection timed out, expire all transfers belonging to it
* as will not get any more POLL events here. */
cf_quiche_expire_conn_closed(cf, data);
return CURLE_SEND_ERROR;
}
}
result = vquic_flush(cf, data, &ctx->q);
if(result) {
if(result == CURLE_AGAIN) {
Curl_expire(data, 1, EXPIRE_QUIC);
return CURLE_OK;
}
return result;
}
readx.cf = cf;
readx.data = data;
memset(&readx.send_info, 0, sizeof(readx.send_info));
pkt_count = 0;
gsolen = quiche_conn_max_send_udp_payload_size(ctx->qconn);
for(;;) {
/* add the next packet to send, if any, to our buffer */
result = Curl_bufq_sipn(&ctx->q.sendbuf, 0,
read_pkt_to_send, &readx, &nread);
if(result) {
if(result != CURLE_AGAIN)
return result;
/* Nothing more to add, flush and leave */
result = vquic_send(cf, data, &ctx->q, gsolen);
if(result) {
if(result == CURLE_AGAIN) {
Curl_expire(data, 1, EXPIRE_QUIC);
return CURLE_OK;
}
return result;
}
goto out;
}
++pkt_count;
if(nread < gsolen || pkt_count >= MAX_PKT_BURST) {
result = vquic_send(cf, data, &ctx->q, gsolen);
if(result) {
if(result == CURLE_AGAIN) {
Curl_expire(data, 1, EXPIRE_QUIC);
return CURLE_OK;
}
goto out;
}
pkt_count = 0;
}
}
out:
timeout_ns = quiche_conn_timeout_as_nanos(ctx->qconn);
if(timeout_ns % 1000000)
timeout_ns += 1000000;
/* expire resolution is milliseconds */
Curl_expire(data, (timeout_ns / 1000000), EXPIRE_QUIC);
return result;
}
static CURLcode recv_closed_stream(struct Curl_cfilter *cf,
struct Curl_easy *data,
size_t *pnread)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
CURLcode result = CURLE_OK;
DEBUGASSERT(stream);
*pnread = 0;
if(stream->reset) {
if(stream->error3 == CURL_H3_ERR_REQUEST_REJECTED) {
infof(data, "HTTP/3 stream %" PRIu64 " refused by server, try again "
"on a new connection", stream->id);
connclose(cf->conn, "REFUSED_STREAM"); /* do not use this anymore */
data->state.refused_stream = TRUE;
return CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */
}
else if(stream->resp_hds_complete && data->req.no_body) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] error after response headers, "
"but we did not want a body anyway, ignore error 0x%"
PRIx64 " %s", stream->id, stream->error3,
vquic_h3_err_str(stream->error3));
return CURLE_OK;
}
failf(data, "HTTP/3 stream %" PRId64 " reset by server (error 0x%" PRIx64
" %s)", stream->id, stream->error3,
vquic_h3_err_str(stream->error3));
result = data->req.bytecount ? CURLE_PARTIAL_FILE : CURLE_HTTP3;
CURL_TRC_CF(data, cf, "[%" PRIu64 "] cf_recv, was reset -> %d",
stream->id, result);
}
else if(!stream->resp_got_header) {
failf(data, "HTTP/3 stream %" PRIu64 " was closed cleanly, but before "
"getting all response header fields, treated as error",
stream->id);
result = CURLE_HTTP3;
}
return result;
}
static CURLcode cf_quiche_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
char *buf, size_t blen, size_t *pnread)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
CURLcode result = CURLE_OK;
*pnread = 0;
(void)buf;
(void)blen;
vquic_ctx_update_time(&ctx->q, Curl_pgrs_now(data));
if(!stream)
return CURLE_RECV_ERROR;
result = cf_process_ingress(cf, data);
if(result) {
CURL_TRC_CF(data, cf, "cf_recv, error on ingress");
goto out;
}
if(stream->xfer_result) {
cf_quiche_stream_close(cf, data, stream);
result = stream->xfer_result;
goto out;
}
else if(stream->closed)
result = recv_closed_stream(cf, data, pnread);
else if(quiche_conn_is_draining(ctx->qconn)) {
failf(data, "QUIC connection is draining");
result = CURLE_HTTP3;
}
else
result = CURLE_AGAIN;
out:
result = Curl_1st_err(result, cf_flush_egress(cf, data));
if(*pnread > 0)
ctx->data_recvd += *pnread;
CURL_TRC_CF(data, cf, "[%" PRIu64 "] cf_recv(len=%zu) -> %d, %zu, total=%"
FMT_OFF_T, stream->id, blen, result, *pnread, ctx->data_recvd);
return result;
}
static CURLcode cf_quiche_send_body(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h3_stream_ctx *stream,
const uint8_t *buf, size_t len, bool eos,
size_t *pnwritten)
{
struct cf_quiche_ctx *ctx = cf->ctx;
ssize_t rv;
*pnwritten = 0;
rv = quiche_h3_send_body(ctx->h3c, ctx->qconn, stream->id,
(uint8_t *)CURL_UNCONST(buf), len, eos);
if(rv == QUICHE_H3_ERR_DONE || (rv == 0 && len > 0)) {
/* Blocked on flow control and should HOLD sending. But when do we open
* again? */
if(!quiche_conn_stream_writable(ctx->qconn, stream->id, len)) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] send_body(len=%zu) "
"-> window exhausted", stream->id, len);
stream->quic_flow_blocked = TRUE;
}
return CURLE_AGAIN;
}
else if(rv == QUICHE_H3_TRANSPORT_ERR_INVALID_STREAM_STATE) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] send_body(len=%zu) "
"-> invalid stream state", stream->id, len);
return CURLE_HTTP3;
}
else if(rv == QUICHE_H3_TRANSPORT_ERR_FINAL_SIZE) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] send_body(len=%zu) -> exceeds size",
stream->id, len);
return CURLE_SEND_ERROR;
}
else if(!curlx_sztouz(rv, pnwritten)) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] send_body(len=%zu) -> quiche err %zd",
stream->id, len, rv);
return CURLE_SEND_ERROR;
}
else {
if(eos && (len == *pnwritten))
stream->send_closed = TRUE;
CURL_TRC_CF(data, cf, "[%" PRIu64 "] send body(len=%zu, eos=%d) -> %zu",
stream->id, len, stream->send_closed, *pnwritten);
return CURLE_OK;
}
}
static CURLcode h3_open_stream(struct Curl_cfilter *cf,
struct Curl_easy *data,
const uint8_t *buf, size_t blen, bool eos,
size_t *pnwritten)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
size_t nheader, i;
int64_t rv;
struct dynhds h2_headers;
quiche_h3_header *nva = NULL;
CURLcode result = CURLE_OK;
*pnwritten = 0;
if(!stream) {
result = h3_data_setup(cf, data);
if(result)
return result;
stream = H3_STREAM_CTX(ctx, data);
DEBUGASSERT(stream);
}
Curl_dynhds_init(&h2_headers, 0, DYN_HTTP_REQUEST);
DEBUGASSERT(stream);
result = Curl_h1_req_parse_read(&stream->h1, buf, blen, NULL,
!data->state.http_ignorecustom ?
data->set.str[STRING_CUSTOMREQUEST] : NULL,
0, pnwritten);
if(result)
goto out;
if(!stream->h1.done) {
/* need more data */
goto out;
}
DEBUGASSERT(stream->h1.req);
result = Curl_http_req_to_h2(&h2_headers, stream->h1.req, data);
if(result)
goto out;
/* no longer needed */
Curl_h1_req_parse_free(&stream->h1);
nheader = Curl_dynhds_count(&h2_headers);
nva = curlx_malloc(sizeof(quiche_h3_header) * nheader);
if(!nva) {
result = CURLE_OUT_OF_MEMORY;
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].name_len = e->namelen;
nva[i].value = (unsigned char *)e->value;
nva[i].value_len = e->valuelen;
}
buf += *pnwritten;
blen -= *pnwritten;
if(eos && !blen)
stream->send_closed = TRUE;
rv = quiche_h3_send_request(ctx->h3c, ctx->qconn, nva, nheader,
stream->send_closed);
CURL_TRC_CF(data, cf, "quiche_send_request() -> %" PRId64, rv);
if(rv < 0) {
if(QUICHE_H3_ERR_STREAM_BLOCKED == rv) {
/* quiche seems to report this error if the connection window is
* exhausted. Which happens frequently and intermittent. */
CURL_TRC_CF(data, cf, "[%" PRIu64 "] blocked", stream->id);
stream->quic_flow_blocked = TRUE;
result = CURLE_AGAIN;
goto out;
}
else {
CURL_TRC_CF(data, cf, "send_request(%s) -> %" PRIu64,
Curl_bufref_ptr(&data->state.url), rv);
}
result = CURLE_SEND_ERROR;
goto out;
}
DEBUGASSERT(!stream->opened);
stream->id = (uint64_t)rv;
stream->opened = TRUE;
stream->closed = FALSE;
stream->reset = FALSE;
if(Curl_trc_is_verbose(data)) {
infof(data, "[HTTP/3] [%" PRIu64 "] OPENED stream for %s",
stream->id, Curl_bufref_ptr(&data->state.url));
for(i = 0; i < nheader; ++i) {
infof(data, "[HTTP/3] [%" PRIu64 "] [%.*s: %.*s]", stream->id,
(int)nva[i].name_len, nva[i].name,
(int)nva[i].value_len, nva[i].value);
}
}
if(blen) { /* after the headers, there was request BODY data */
size_t nwritten;
CURLcode r2 = CURLE_OK;
r2 = cf_quiche_send_body(cf, data, stream, buf, blen, eos, &nwritten);
if(r2 && (r2 != CURLE_AGAIN)) { /* real error, fail */
result = r2;
}
else if(nwritten > 0) {
*pnwritten += nwritten;
}
}
out:
curlx_free(nva);
Curl_dynhds_free(&h2_headers);
return result;
}
static CURLcode cf_quiche_send(struct Curl_cfilter *cf, struct Curl_easy *data,
const uint8_t *buf, size_t len, bool eos,
size_t *pnwritten)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
CURLcode result;
*pnwritten = 0;
vquic_ctx_update_time(&ctx->q, Curl_pgrs_now(data));
result = cf_process_ingress(cf, data);
if(result)
goto out;
if(!stream || !stream->opened) {
result = h3_open_stream(cf, data, buf, len, eos, pnwritten);
if(result)
goto out;
stream = H3_STREAM_CTX(ctx, data);
}
else if(stream->xfer_result) {
cf_quiche_stream_close(cf, data, stream);
result = stream->xfer_result;
}
else if(stream->closed) {
if(stream->resp_hds_complete) {
/* sending request body on a stream that has been closed by the
* server. If the server has send us a final response, we should
* silently discard the send data.
* This happens for example on redirects where the server, instead
* of reading the full request body just closed the stream after
* sending the 30x response.
* This is sort of a race: had the transfer loop called recv first,
* it would see the response and stop/discard sending on its own- */
CURL_TRC_CF(data, cf, "[%" PRIu64 "] discarding data"
"on closed stream with response", stream->id);
result = CURLE_OK;
*pnwritten = len;
goto out;
}
CURL_TRC_CF(data, cf, "[%" PRIu64 "] send_body(len=%zu) "
"-> stream closed", stream->id, len);
result = CURLE_HTTP3;
goto out;
}
else {
result = cf_quiche_send_body(cf, data, stream, buf, len, eos, pnwritten);
}
out:
result = Curl_1st_err(result, cf_flush_egress(cf, data));
CURL_TRC_CF(data, cf, "[%" PRIu64 "] cf_send(len=%zu) -> %d, %zu",
stream ? stream->id : (uint64_t)~0, len,
result, *pnwritten);
return result;
}
static bool stream_is_writeable(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
return stream && (quiche_conn_stream_writable(
ctx->qconn, stream->id, 1) > 0);
}
static CURLcode cf_quiche_adjust_pollset(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct easy_pollset *ps)
{
struct cf_quiche_ctx *ctx = cf->ctx;
bool want_recv, want_send;
CURLcode result = CURLE_OK;
if(!ctx->qconn)
return CURLE_OK;
Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send);
if(want_recv || want_send) {
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
bool c_exhaust, s_exhaust;
c_exhaust = FALSE; /* Have not found any call in quiche that tells
us if the connection itself is blocked */
s_exhaust = want_send && stream && stream->opened &&
(stream->quic_flow_blocked || !stream_is_writeable(cf, data));
want_recv = (want_recv || c_exhaust || s_exhaust);
want_send = (!s_exhaust && want_send) ||
!Curl_bufq_is_empty(&ctx->q.sendbuf);
result = Curl_pollset_set(data, ps, ctx->q.sockfd, want_recv, want_send);
}
return result;
}
static CURLcode h3_data_pause(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool pause)
{
/* There seems to exist no API in quiche to shrink/enlarge the streams
* windows. As we do in HTTP/2. */
(void)cf;
if(!pause) {
Curl_multi_mark_dirty(data);
}
return CURLE_OK;
}
static CURLcode cf_quiche_cntrl(struct Curl_cfilter *cf,
struct Curl_easy *data,
int event, int arg1, void *arg2)
{
struct cf_quiche_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
(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_DONE:
h3_data_done(cf, data);
break;
case CF_CTRL_DATA_DONE_SEND: {
struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
if(stream && !stream->send_closed) {
unsigned char body[1];
size_t sent;
stream->send_closed = TRUE;
body[0] = 'X';
result = cf_quiche_send(cf, data, body, 0, TRUE, &sent);
CURL_TRC_CF(data, cf, "[%" PRIu64 "] DONE_SEND -> %d, %zu",
stream->id, result, sent);
}
break;
}
case CF_CTRL_CONN_INFO_UPDATE:
if(!cf->sockindex && cf->connected) {
cf->conn->httpversion_seen = 30;
Curl_conn_set_multiplex(cf->conn);
}
break;
default:
break;
}
return result;
}
static CURLcode cf_quiche_ctx_open(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
int rv;
CURLcode result;
const struct Curl_sockaddr_ex *sockaddr;
static const struct alpn_spec ALPN_SPEC_H3 = { { "h3" }, 1 };
DEBUGASSERT(ctx->q.sockfd != CURL_SOCKET_BAD);
DEBUGASSERT(ctx->initialized);
result = vquic_ctx_init(data, &ctx->q);
if(result)
return result;
ctx->cfg = quiche_config_new(QUICHE_PROTOCOL_VERSION);
if(!ctx->cfg) {
failf(data, "cannot create quiche config");
return CURLE_FAILED_INIT;
}
quiche_config_enable_pacing(ctx->cfg, FALSE);
quiche_config_set_initial_max_data(ctx->cfg, (1 * 1024 * 1024)
/* (QUIC_MAX_STREAMS/2) * H3_STREAM_WINDOW_SIZE */);
quiche_config_set_initial_max_streams_bidi(ctx->cfg, QUIC_MAX_STREAMS);
quiche_config_set_initial_max_streams_uni(ctx->cfg, QUIC_MAX_STREAMS);
quiche_config_set_initial_max_stream_data_bidi_local(ctx->cfg,
H3_STREAM_WINDOW_SIZE);
quiche_config_set_initial_max_stream_data_bidi_remote(ctx->cfg,
H3_STREAM_WINDOW_SIZE);
quiche_config_set_initial_max_stream_data_uni(ctx->cfg,
H3_STREAM_WINDOW_SIZE);
quiche_config_set_disable_active_migration(ctx->cfg, TRUE);
quiche_config_set_max_connection_window(ctx->cfg,
10 * QUIC_MAX_STREAMS * H3_STREAM_WINDOW_SIZE);
quiche_config_set_max_stream_window(ctx->cfg, 10 * H3_STREAM_WINDOW_SIZE);
quiche_config_set_application_protos(ctx->cfg,
(uint8_t *)CURL_UNCONST(QUICHE_H3_APPLICATION_PROTOCOL),
sizeof(QUICHE_H3_APPLICATION_PROTOCOL)
- 1);
result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer,
&ALPN_SPEC_H3, NULL, NULL, cf, NULL);
if(result)
return result;
result = Curl_rand(data, ctx->scid, sizeof(ctx->scid));
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;
ctx->qconn = quiche_conn_new_with_tls((const uint8_t *)ctx->scid,
sizeof(ctx->scid), NULL, 0,
(struct sockaddr *)&ctx->q.local_addr,
ctx->q.local_addrlen,
&sockaddr->curl_sa_addr,
sockaddr->addrlen,
ctx->cfg, ctx->tls.ossl.ssl, FALSE);
if(!ctx->qconn) {
failf(data, "cannot create quiche connection");
return CURLE_OUT_OF_MEMORY;
}
/* Known to not work on Windows */
#if !defined(_WIN32) && defined(HAVE_QUICHE_CONN_SET_QLOG_FD)
{
int qfd;
(void)Curl_qlogdir(data, ctx->scid, sizeof(ctx->scid), &qfd);
if(qfd != -1)
quiche_conn_set_qlog_fd(ctx->qconn, qfd, "qlog title", "curl qlog");
}
#endif
result = cf_flush_egress(cf, data);
if(result)
return result;
{
unsigned char alpn_protocols[] = QUICHE_H3_APPLICATION_PROTOCOL;
unsigned alpn_len, offset = 0;
/* Replace each ALPN length prefix by a comma. */
while(offset < sizeof(alpn_protocols) - 1) {
alpn_len = alpn_protocols[offset];
alpn_protocols[offset] = ',';
offset += 1 + alpn_len;
}
CURL_TRC_CF(data, cf, "Sent QUIC client Initial, ALPN: %s",
alpn_protocols + 1);
}
return CURLE_OK;
}
static CURLcode cf_quiche_verify_peer(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_quiche_ctx *ctx = cf->ctx;
return Curl_vquic_tls_verify_peer(&ctx->tls, cf, data, &ctx->peer);
}
static CURLcode cf_quiche_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
struct cf_quiche_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
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, done);
if(result || !*done)
return result;
}
*done = FALSE;
vquic_ctx_update_time(&ctx->q, Curl_pgrs_now(data));
if(!ctx->qconn) {
result = cf_quiche_ctx_open(cf, data);
if(result)
goto out;
ctx->started_at = ctx->q.last_op;
result = cf_flush_egress(cf, data);
/* we do not expect to be able to recv anything yet */
goto out;
}
result = cf_process_ingress(cf, data);
if(result)
goto out;
result = cf_flush_egress(cf, data);
if(result)
goto out;
if(quiche_conn_is_established(ctx->qconn)) {
ctx->handshake_at = ctx->q.last_op;
CURL_TRC_CF(data, cf, "handshake complete after %" FMT_TIMEDIFF_T "ms",
curlx_ptimediff_ms(&ctx->handshake_at, &ctx->started_at));
result = cf_quiche_verify_peer(cf, data);
if(!result) {
CURL_TRC_CF(data, cf, "peer verified");
ctx->h3config = quiche_h3_config_new();
if(!ctx->h3config) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
/* Create a new HTTP/3 connection on the QUIC connection. */
ctx->h3c = quiche_h3_conn_new_with_transport(ctx->qconn, ctx->h3config);
if(!ctx->h3c) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
cf->connected = TRUE;
*done = TRUE;
}
}
else if(quiche_conn_is_draining(ctx->qconn)) {
/* 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;
}
out:
#ifdef CURLVERBOSE
if(result && result != CURLE_AGAIN) {
struct ip_quadruple ip;
if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip))
infof(data, "connect to %s port %u failed: %s",
ip.remote_ip, ip.remote_port, curl_easy_strerror(result));
else
infof(data, "connect failed: %s", curl_easy_strerror(result));
}
#endif
return result;
}
static CURLcode cf_quiche_shutdown(struct Curl_cfilter *cf,
struct Curl_easy *data, bool *done)
{
struct cf_quiche_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
if(cf->shutdown || !ctx || !ctx->qconn) {
*done = TRUE;
return CURLE_OK;
}
*done = FALSE;
if(!ctx->shutdown_started) {
int err;
ctx->shutdown_started = TRUE;
vquic_ctx_update_time(&ctx->q, Curl_pgrs_now(data));
err = quiche_conn_close(ctx->qconn, TRUE, 0, NULL, 0);
if(err) {
CURL_TRC_CF(data, cf, "error %d adding shutdown packet, "
"aborting shutdown", err);
result = CURLE_SEND_ERROR;
goto out;
}
}
if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) {
CURL_TRC_CF(data, cf, "shutdown, flushing sendbuf");
result = cf_flush_egress(cf, data);
if(result)
goto out;
}
if(Curl_bufq_is_empty(&ctx->q.sendbuf)) {
/* sent everything, quiche does not seem to support a graceful
* shutdown waiting for a reply, so ware done. */
CURL_TRC_CF(data, cf, "shutdown completely sent off, done");
*done = TRUE;
}
else {
CURL_TRC_CF(data, cf, "shutdown sending blocked");
}
out:
return result;
}
static void cf_quiche_close(struct Curl_cfilter *cf, struct Curl_easy *data)
{
if(cf->ctx) {
bool done;
(void)cf_quiche_shutdown(cf, data, &done);
cf_quiche_ctx_close(cf->ctx);
cf->connected = FALSE;
}
}
static void cf_quiche_destroy(struct Curl_cfilter *cf, struct Curl_easy *data)
{
(void)data;
if(cf->ctx) {
cf_quiche_ctx_close(cf->ctx);
cf_quiche_ctx_free(cf->ctx);
cf->ctx = NULL;
}
}
static CURLcode cf_quiche_query(struct Curl_cfilter *cf,
struct Curl_easy *data,
int query, int *pres1, void *pres2)
{
struct cf_quiche_ctx *ctx = cf->ctx;
switch(query) {
case CF_QUERY_MAX_CONCURRENT: {
uint64_t max_streams = cf->conn->attached_xfers;
if(!ctx->goaway && ctx->qconn) {
max_streams += quiche_conn_peer_streams_left_bidi(ctx->qconn);
}
*pres1 = (max_streams > INT_MAX) ? INT_MAX : (int)max_streams;
CURL_TRC_CF(data, cf, "query conn[%" FMT_OFF_T "]: "
"MAX_CONCURRENT -> %d (%u in use)",
cf->conn->connection_id, *pres1, cf->conn->attached_xfers);
return CURLE_OK;
}
case CF_QUERY_CONNECT_REPLY_MS:
if(ctx->q.got_first_byte) {
timediff_t ms = curlx_ptimediff_ms(&ctx->q.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->q.got_first_byte)
*when = ctx->q.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;
}
case CF_QUERY_HTTP_VERSION:
*pres1 = 30;
return CURLE_OK;
case CF_QUERY_SSL_INFO:
case CF_QUERY_SSL_CTX_INFO: {
struct curl_tlssessioninfo *info = pres2;
if(Curl_vquic_tls_get_ssl_info(&ctx->tls,
(query == CF_QUERY_SSL_CTX_INFO), info))
return CURLE_OK;
break;
}
case CF_QUERY_ALPN_NEGOTIATED: {
const char **palpn = pres2;
DEBUGASSERT(palpn);
*palpn = cf->connected ? "h3" : NULL;
return CURLE_OK;
}
default:
break;
}
return cf->next ?
cf->next->cft->query(cf->next, data, query, pres1, pres2) :
CURLE_UNKNOWN_OPTION;
}
static bool cf_quiche_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *input_pending)
{
struct cf_quiche_ctx *ctx = cf->ctx;
bool alive = TRUE;
*input_pending = FALSE;
if(!ctx->qconn)
return FALSE;
if(quiche_conn_is_closed(ctx->qconn)) {
if(quiche_conn_is_timed_out(ctx->qconn))
CURL_TRC_CF(data, cf, "connection was closed due to idle timeout");
else
CURL_TRC_CF(data, cf, "connection is closed");
return FALSE;
}
if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending))
return FALSE;
if(*input_pending) {
/* This happens before we have sent off a request and the connection is
not in use by any other transfer, there should not be any data here,
only "protocol frames" */
*input_pending = FALSE;
if(cf_process_ingress(cf, data))
alive = FALSE;
else {
alive = TRUE;
}
}
return alive;
}
struct Curl_cftype Curl_cft_http3 = {
"HTTP/3",
CF_TYPE_IP_CONNECT | CF_TYPE_SSL | CF_TYPE_MULTIPLEX | CF_TYPE_HTTP,
0,
cf_quiche_destroy,
cf_quiche_connect,
cf_quiche_close,
cf_quiche_shutdown,
cf_quiche_adjust_pollset,
Curl_cf_def_data_pending,
cf_quiche_send,
cf_quiche_recv,
cf_quiche_cntrl,
cf_quiche_conn_is_alive,
Curl_cf_def_conn_keep_alive,
cf_quiche_query,
};
CURLcode Curl_cf_quiche_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
const struct Curl_addrinfo *ai)
{
struct cf_quiche_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_quiche_ctx_init(ctx);
result = Curl_cf_create(&cf, &Curl_cft_http3, ctx);
if(result)
goto out;
cf->conn = conn;
result = Curl_cf_udp_create(&cf->next, data, conn, ai, TRNSPRT_QUIC);
if(result)
goto out;
cf->next->conn = cf->conn;
cf->next->sockindex = cf->sockindex;
out:
*pcf = (!result) ? cf : NULL;
if(result) {
if(cf)
Curl_conn_cf_discard_chain(&cf, data);
else if(ctx)
cf_quiche_ctx_free(ctx);
}
return result;
}
#endif