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 bufc_pool stream_bufcp; /* chunk pool for streams */
struct uint_hash streams; /* hash `data->mid` to `stream_ctx` */
+ struct dynbuf scratch; /* 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 */
debug_log_init = 1;
}
#endif
- Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
- H3_STREAM_POOL_SPARES);
+ curlx_dyn_init(&ctx->scratch, 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;
}
Curl_vquic_tls_cleanup(&ctx->tls);
Curl_ssl_peer_cleanup(&ctx->peer);
vquic_ctx_free(&ctx->q);
- Curl_bufcp_free(&ctx->stream_bufcp);
Curl_uint32_hash_destroy(&ctx->streams);
+ curlx_dyn_free(&ctx->scratch);
+ Curl_bufq_free(&ctx->writebuf);
}
curlx_free(ctx);
}
*/
struct h3_stream_ctx {
uint64_t id; /* HTTP/3 protocol stream identifier */
- struct bufq recvbuf; /* h3 response */
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 */
static void h3_stream_ctx_free(struct h3_stream_ctx *stream)
{
- Curl_bufq_free(&stream->recvbuf);
Curl_h1_req_parse_free(&stream->h1);
curlx_free(stream);
}
(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;
}
return CURLE_OUT_OF_MEMORY;
stream->id = -1;
- Curl_bufq_initp(&stream->recvbuf, &ctx->stream_bufcp,
- H3_STREAM_RECV_CHUNKS, BUFQ_OPT_SOFT_LIMIT);
Curl_h1_req_parse_init(&stream->h1, H1_PARSE_DEFAULT_MAX_LINE_LEN);
if(!Curl_uint32_hash_set(&ctx->streams, data->mid, stream)) {
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);
- CURLcode result;
(void)cf;
if(stream) {
CURL_TRC_CF(data, cf, "[%" PRIu64 "] easy handle is done", stream->id);
- 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, "data_done, flush egress -> %d", result);
- }
+ cf_quiche_stream_close(cf, data, stream);
Curl_uint32_hash_remove(&ctx->streams, data->mid);
}
}
cf_quiche_for_all_streams(cf, data->multi, cf_quiche_do_expire, NULL);
}
-/*
- * 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,
+static void cf_quiche_write_hd(struct Curl_cfilter *cf,
struct Curl_easy *data,
- const void *mem, size_t memlen)
+ struct h3_stream_ctx *stream,
+ const char *buf, size_t blen, bool eos)
{
- struct cf_quiche_ctx *ctx = cf->ctx;
- struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
- CURLcode result = CURLE_OK;
- size_t nwritten;
-
- (void)cf;
- if(!stream)
- return CURLE_RECV_ERROR;
- result = Curl_bufq_write(&stream->recvbuf, mem, memlen, &nwritten);
- if(result)
- return result;
-
- if(nwritten < memlen) {
- /* This MUST not happen. Our recbuf is dimensioned to hold the
- * full max_stream_window and then some for this reason. */
- DEBUGASSERT(0);
- return CURLE_RECV_ERROR;
+ /* 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);
}
- return result;
}
struct cb_ctx {
struct Curl_cfilter *cf;
struct Curl_easy *data;
+ struct h3_stream_ctx *stream;
};
static int cb_each_header(uint8_t *name, size_t name_len,
void *argp)
{
struct cb_ctx *x = argp;
- struct cf_quiche_ctx *ctx = x->cf->ctx;
- struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, x->data);
+ 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;
- if(!stream)
- return CURLE_OK;
+ if(!stream || stream->xfer_result)
+ return 1; /* abort iteration */
if((name_len == 7) && !strncmp(HTTP_PSEUDO_STATUS, (char *)name, 7)) {
- CURL_TRC_CF(x->data, x->cf, "[%" PRIu64 "] status: %.*s",
- stream->id, (int)value_len, value);
- result = write_resp_raw(x->cf, x->data, "HTTP/3 ", sizeof("HTTP/3 ") - 1);
+ curlx_dyn_reset(&ctx->scratch);
+ result = Curl_http_decode_status(&stream->status_code,
+ (const char *)value, value_len);
+ if(!result)
+ result = curlx_dyn_addn(&ctx->scratch, STRCONST("HTTP/3 "));
+ if(!result)
+ result = curlx_dyn_addn(&ctx->scratch,
+ (const char *)value, value_len);
if(!result)
- result = write_resp_raw(x->cf, x->data, value, value_len);
+ result = curlx_dyn_addn(&ctx->scratch, STRCONST(" \r\n"));
if(!result)
- result = write_resp_raw(x->cf, x->data, " \r\n", 3);
+ cf_quiche_write_hd(cf, data, stream, curlx_dyn_ptr(&ctx->scratch),
+ curlx_dyn_len(&ctx->scratch), FALSE);
+ CURL_TRC_CF(data, cf, "[%" PRId64 "] status: %s",
+ stream->id, curlx_dyn_ptr(&ctx->scratch));
}
else {
- CURL_TRC_CF(x->data, x->cf, "[%" PRIu64 "] header: %.*s: %.*s",
+ /* 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);
- result = write_resp_raw(x->cf, x->data, name, name_len);
+ curlx_dyn_reset(&ctx->scratch);
+ result = curlx_dyn_addn(&ctx->scratch,
+ (const char *)name, name_len);
if(!result)
- result = write_resp_raw(x->cf, x->data, ": ", 2);
+ result = curlx_dyn_addn(&ctx->scratch, STRCONST(": "));
if(!result)
- result = write_resp_raw(x->cf, x->data, value, value_len);
+ result = curlx_dyn_addn(&ctx->scratch,
+ (const char *)value, value_len);
if(!result)
- result = write_resp_raw(x->cf, x->data, "\r\n", 2);
+ result = curlx_dyn_addn(&ctx->scratch, STRCONST("\r\n"));
+ if(!result)
+ cf_quiche_write_hd(cf, data, stream, curlx_dyn_ptr(&ctx->scratch),
+ curlx_dyn_len(&ctx->scratch), FALSE);
}
+
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 result;
+ return stream->xfer_result ? 1 : 0;
}
static CURLcode stream_resp_read(void *reader_ctx,
return CURLE_OK;
}
-static CURLcode cf_recv_body(struct Curl_cfilter *cf,
- struct Curl_easy *data)
+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;
- struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
size_t nread;
struct cb_ctx cb_ctx;
CURLcode result = CURLE_OK;
if(!stream)
- return CURLE_RECV_ERROR;
-
- if(!stream->resp_hds_complete) {
- result = write_resp_raw(cf, data, "\r\n", 2);
- if(result)
- return result;
- stream->resp_hds_complete = TRUE;
- }
+ 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;
- result = Curl_bufq_slurp(&stream->recvbuf,
- stream_resp_read, &cb_ctx, &nread);
-
- if(result && result != CURLE_AGAIN) {
- CURL_TRC_CF(data, cf, "[%" PRIu64 "] recv_body error %zu",
- stream->id, nread);
- failf(data, "Error %d in HTTP/3 response body for stream[%" PRIu64 "]",
- result, stream->id);
- stream->closed = TRUE;
- stream->reset = TRUE;
- stream->send_closed = TRUE;
- return result;
- }
- return CURLE_OK;
-}
-
-#ifdef DEBUGBUILD
-static const char *cf_ev_name(quiche_h3_event *ev)
-{
- switch(quiche_h3_event_type(ev)) {
- case QUICHE_H3_EVENT_HEADERS:
- return "HEADERS";
- case QUICHE_H3_EVENT_DATA:
- return "DATA";
- case QUICHE_H3_EVENT_RESET:
- return "RESET";
- case QUICHE_H3_EVENT_FINISHED:
- return "FINISHED";
- case QUICHE_H3_EVENT_GOAWAY:
- return "GOAWAY";
- default:
- return "Unknown";
+ 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);
}
-#else
-#define cf_ev_name(x) ""
-#endif
-static CURLcode h3_process_event(struct Curl_cfilter *cf,
+static void cf_quiche_process_ev(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h3_stream_ctx *stream,
quiche_h3_event *ev)
{
- struct cb_ctx cb_ctx;
- CURLcode result = CURLE_OK;
- int rc;
-
if(!stream)
- return CURLE_OK;
+ return;
+
switch(quiche_h3_event_type(ev)) {
- case QUICHE_H3_EVENT_HEADERS:
+ case QUICHE_H3_EVENT_HEADERS: {
+ struct cb_ctx cb_ctx;
stream->resp_got_header = TRUE;
cb_ctx.cf = cf;
cb_ctx.data = data;
- rc = quiche_h3_event_for_each_header(ev, cb_each_header, &cb_ctx);
- if(rc) {
- failf(data, "Error %d in HTTP/3 response header for stream[%" PRIu64 "]",
- rc, stream->id);
- return CURLE_RECV_ERROR;
- }
+ 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->closed) {
- result = cf_recv_body(cf, 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:
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) {
- result = write_resp_raw(cf, data, "\r\n", 2);
- if(result)
- return result;
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:
stream->id, quiche_h3_event_type(ev));
break;
}
- return result;
-}
-
-static CURLcode cf_quiche_ev_process(struct Curl_cfilter *cf,
- struct Curl_easy *data,
- struct h3_stream_ctx *stream,
- quiche_h3_event *ev)
-{
- CURLcode result = h3_process_event(cf, data, stream, ev);
- Curl_multi_mark_dirty(data);
- if(result)
- CURL_TRC_CF(data, cf, "error processing event %s for [%" PRIu64 "] -> %d",
- cf_ev_name(ev), stream->id, result);
- return result;
}
struct cf_quich_disp_ctx {
struct Curl_cfilter *cf;
struct Curl_multi *multi;
quiche_h3_event *ev;
- CURLcode result;
};
static bool cf_quiche_disp_event(uint32_t mid, void *val, void *user_data)
if(stream->id == dctx->stream_id) {
struct Curl_easy *sdata = Curl_multi_get_easy(dctx->multi, mid);
if(sdata)
- dctx->result = cf_quiche_ev_process(dctx->cf, sdata, stream, dctx->ev);
+ cf_quiche_process_ev(dctx->cf, sdata, stream, dctx->ev);
return FALSE; /* stop iterating */
}
return TRUE;
return CURLE_HTTP3;
}
else {
- struct cf_quich_disp_ctx dctx;
- dctx.stream_id = (uint64_t)rv;
- dctx.cf = cf;
- dctx.multi = data->multi;
- dctx.ev = ev;
- dctx.result = CURLE_OK;
stream = H3_STREAM_CTX(ctx, data);
- if(stream && stream->id == dctx.stream_id) {
+ if(stream && stream->id == (uint64_t)rv) {
/* event for calling transfer */
- CURLcode result = cf_quiche_ev_process(cf, data, stream, ev);
+ cf_quiche_process_ev(cf, data, stream, ev);
quiche_h3_event_free(ev);
- if(result)
- return result;
+ 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);
}
}
static CURLcode cf_quiche_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
- char *buf, size_t len, size_t *pnread)
+ 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);
if(!stream)
return CURLE_RECV_ERROR;
- if(!Curl_bufq_is_empty(&stream->recvbuf)) {
- result = Curl_bufq_cread(&stream->recvbuf, buf, len, pnread);
- CURL_TRC_CF(data, cf, "[%" PRIu64 "] read recvbuf(len=%zu) "
- "-> %d, %zu", stream->id, len, result, *pnread);
- if(result)
- goto out;
- }
-
result = cf_process_ingress(cf, data);
if(result) {
CURL_TRC_CF(data, cf, "cf_recv, error on ingress");
goto out;
}
- /* recvbuf had nothing before, maybe after progressing ingress? */
- if(!*pnread && !Curl_bufq_is_empty(&stream->recvbuf)) {
- result = Curl_bufq_cread(&stream->recvbuf, buf, len, pnread);
- CURL_TRC_CF(data, cf, "[%" PRIu64 "] read recvbuf(len=%zu) "
- "-> %d, %zu", stream->id, len, result, *pnread);
- if(result)
- goto out;
- }
-
- if(*pnread) {
- if(stream->closed)
- Curl_multi_mark_dirty(data);
+ 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;
+ 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(total=%"
- FMT_OFF_T ") -> %d, %zu",
- stream->id, ctx->data_recvd, result, *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;
}
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
return result;
}
-/*
- * Called from transfer.c:data_pending to know if we should keep looping
- * to receive more data from the connection.
- */
-static bool cf_quiche_data_pending(struct Curl_cfilter *cf,
- const struct Curl_easy *data)
-{
- struct cf_quiche_ctx *ctx = cf->ctx;
- const struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
- (void)cf;
- return stream && !Curl_bufq_is_empty(&stream->recvbuf);
-}
-
static CURLcode h3_data_pause(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool pause)
cf_quiche_close,
cf_quiche_shutdown,
cf_quiche_adjust_pollset,
- cf_quiche_data_pending,
+ Curl_cf_def_data_pending,
cf_quiche_send,
cf_quiche_recv,
cf_quiche_cntrl,