/*************************************************************************** * _ _ ____ _ * Project ___| | | | _ \| | * / __| | | | |_) | | * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * * Copyright (C) 1998 - 2021, 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. * ***************************************************************************/ #include "curl_setup.h" #ifdef USE_QUICHE #include #include #include "urldata.h" #include "sendf.h" #include "strdup.h" #include "rand.h" #include "quic.h" #include "strcase.h" #include "multiif.h" #include "connect.h" #include "strerror.h" #include "vquic.h" /* The last 3 #include files should be in this order */ #include "curl_printf.h" #include "curl_memory.h" #include "memdebug.h" #define DEBUG_HTTP3 /* #define DEBUG_QUICHE */ #ifdef DEBUG_HTTP3 #define H3BUGF(x) x #else #define H3BUGF(x) do { } while(0) #endif #define QUIC_MAX_STREAMS (256*1024) #define QUIC_MAX_DATA (1*1024*1024) #define QUIC_IDLE_TIMEOUT (60 * 1000) /* milliseconds */ static CURLcode process_ingress(struct Curl_easy *data, curl_socket_t sockfd, struct quicsocket *qs); static CURLcode flush_egress(struct Curl_easy *data, curl_socket_t sockfd, struct quicsocket *qs); static CURLcode http_request(struct Curl_easy *data, const void *mem, size_t len); static Curl_recv h3_stream_recv; static Curl_send h3_stream_send; static int quiche_getsock(struct Curl_easy *data, struct connectdata *conn, curl_socket_t *socks) { struct SingleRequest *k = &data->req; int bitmap = GETSOCK_BLANK; socks[0] = conn->sock[FIRSTSOCKET]; /* in a HTTP/2 connection we can basically always get a frame so we should always be ready for one */ bitmap |= GETSOCK_READSOCK(FIRSTSOCKET); /* we're still uploading or the HTTP/2 layer wants to send data */ if((k->keepon & (KEEP_SEND|KEEP_SEND_PAUSE)) == KEEP_SEND) bitmap |= GETSOCK_WRITESOCK(FIRSTSOCKET); return bitmap; } static CURLcode qs_disconnect(struct Curl_easy *data, struct quicsocket *qs) { DEBUGASSERT(qs); if(qs->conn) { (void)quiche_conn_close(qs->conn, TRUE, 0, NULL, 0); /* flushing the egress is not a failsafe way to deliver all the outstanding packets, but we also don't want to get stuck here... */ (void)flush_egress(data, qs->sockfd, qs); quiche_conn_free(qs->conn); qs->conn = NULL; } if(qs->h3config) quiche_h3_config_free(qs->h3config); if(qs->h3c) quiche_h3_conn_free(qs->h3c); if(qs->cfg) { quiche_config_free(qs->cfg); qs->cfg = NULL; } return CURLE_OK; } static CURLcode quiche_disconnect(struct Curl_easy *data, struct connectdata *conn, bool dead_connection) { struct quicsocket *qs = conn->quic; (void)dead_connection; return qs_disconnect(data, qs); } void Curl_quic_disconnect(struct Curl_easy *data, struct connectdata *conn, int tempindex) { if(conn->transport == TRNSPRT_QUIC) qs_disconnect(data, &conn->hequic[tempindex]); } static unsigned int quiche_conncheck(struct Curl_easy *data, struct connectdata *conn, unsigned int checks_to_perform) { (void)data; (void)conn; (void)checks_to_perform; return CONNRESULT_NONE; } static CURLcode quiche_do(struct Curl_easy *data, bool *done) { struct HTTP *stream = data->req.p.http; stream->h3req = FALSE; /* not sent */ return Curl_http(data, done); } static const struct Curl_handler Curl_handler_http3 = { "HTTPS", /* scheme */ ZERO_NULL, /* setup_connection */ quiche_do, /* do_it */ Curl_http_done, /* done */ ZERO_NULL, /* do_more */ ZERO_NULL, /* connect_it */ ZERO_NULL, /* connecting */ ZERO_NULL, /* doing */ quiche_getsock, /* proto_getsock */ quiche_getsock, /* doing_getsock */ ZERO_NULL, /* domore_getsock */ quiche_getsock, /* perform_getsock */ quiche_disconnect, /* disconnect */ ZERO_NULL, /* readwrite */ quiche_conncheck, /* connection_check */ ZERO_NULL, /* attach connection */ PORT_HTTP, /* defport */ CURLPROTO_HTTPS, /* protocol */ CURLPROTO_HTTP, /* family */ PROTOPT_SSL | PROTOPT_STREAM /* flags */ }; #ifdef DEBUG_QUICHE static void quiche_debug_log(const char *line, void *argp) { (void)argp; fprintf(stderr, "%s\n", line); } #endif CURLcode Curl_quic_connect(struct Curl_easy *data, struct connectdata *conn, curl_socket_t sockfd, int sockindex, const struct sockaddr *addr, socklen_t addrlen) { CURLcode result; struct quicsocket *qs = &conn->hequic[sockindex]; char *keylog_file = NULL; char ipbuf[40]; int port; #ifdef DEBUG_QUICHE /* initialize debug log callback only once */ static int debug_log_init = 0; if(!debug_log_init) { quiche_enable_debug_logging(quiche_debug_log, NULL); debug_log_init = 1; } #endif (void)addr; (void)addrlen; qs->sockfd = sockfd; qs->cfg = quiche_config_new(QUICHE_PROTOCOL_VERSION); if(!qs->cfg) { failf(data, "can't create quiche config"); return CURLE_FAILED_INIT; } quiche_config_set_max_idle_timeout(qs->cfg, QUIC_IDLE_TIMEOUT); quiche_config_set_initial_max_data(qs->cfg, QUIC_MAX_DATA); quiche_config_set_initial_max_stream_data_bidi_local(qs->cfg, QUIC_MAX_DATA); quiche_config_set_initial_max_stream_data_bidi_remote(qs->cfg, QUIC_MAX_DATA); quiche_config_set_initial_max_stream_data_uni(qs->cfg, QUIC_MAX_DATA); quiche_config_set_initial_max_streams_bidi(qs->cfg, QUIC_MAX_STREAMS); quiche_config_set_initial_max_streams_uni(qs->cfg, QUIC_MAX_STREAMS); quiche_config_set_application_protos(qs->cfg, (uint8_t *) QUICHE_H3_APPLICATION_PROTOCOL, sizeof(QUICHE_H3_APPLICATION_PROTOCOL) - 1); result = Curl_rand(data, qs->scid, sizeof(qs->scid)); if(result) return result; keylog_file = getenv("SSLKEYLOGFILE"); if(keylog_file) quiche_config_log_keys(qs->cfg); qs->conn = quiche_connect(conn->host.name, (const uint8_t *) qs->scid, sizeof(qs->scid), addr, addrlen, qs->cfg); if(!qs->conn) { failf(data, "can't create quiche connection"); return CURLE_OUT_OF_MEMORY; } if(keylog_file) quiche_conn_set_keylog_path(qs->conn, keylog_file); /* Known to not work on Windows */ #if !defined(WIN32) && defined(HAVE_QUICHE_CONN_SET_QLOG_FD) { int qfd; (void)Curl_qlogdir(data, qs->scid, sizeof(qs->scid), &qfd); if(qfd != -1) quiche_conn_set_qlog_fd(qs->conn, qfd, "qlog title", "curl qlog"); } #endif result = flush_egress(data, sockfd, qs); if(result) return result; /* extract the used address as a string */ if(!Curl_addr2string((struct sockaddr*)addr, addrlen, ipbuf, &port)) { char buffer[STRERROR_LEN]; failf(data, "ssrem inet_ntop() failed with errno %d: %s", SOCKERRNO, Curl_strerror(SOCKERRNO, buffer, sizeof(buffer))); return CURLE_BAD_FUNCTION_ARGUMENT; } infof(data, "Connect socket %d over QUIC to %s:%ld\n", sockfd, ipbuf, port); Curl_persistconninfo(data, conn, NULL, -1); /* for connection reuse purposes: */ conn->ssl[FIRSTSOCKET].state = ssl_connection_complete; { 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; } infof(data, "Sent QUIC client Initial, ALPN: %s\n", alpn_protocols + 1); } return CURLE_OK; } static CURLcode quiche_has_connected(struct connectdata *conn, int sockindex, int tempindex) { CURLcode result; struct quicsocket *qs = conn->quic = &conn->hequic[tempindex]; conn->recv[sockindex] = h3_stream_recv; conn->send[sockindex] = h3_stream_send; conn->handler = &Curl_handler_http3; conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ conn->httpversion = 30; conn->bundle->multiuse = BUNDLE_MULTIPLEX; qs->h3config = quiche_h3_config_new(); if(!qs->h3config) return CURLE_OUT_OF_MEMORY; /* Create a new HTTP/3 connection on the QUIC connection. */ qs->h3c = quiche_h3_conn_new_with_transport(qs->conn, qs->h3config); if(!qs->h3c) { result = CURLE_OUT_OF_MEMORY; goto fail; } if(conn->hequic[1-tempindex].cfg) { qs = &conn->hequic[1-tempindex]; quiche_config_free(qs->cfg); quiche_conn_free(qs->conn); qs->cfg = NULL; qs->conn = NULL; } return CURLE_OK; fail: quiche_h3_config_free(qs->h3config); quiche_h3_conn_free(qs->h3c); return result; } /* * This function gets polled to check if this QUIC connection has connected. */ CURLcode Curl_quic_is_connected(struct Curl_easy *data, struct connectdata *conn, int sockindex, bool *done) { CURLcode result; struct quicsocket *qs = &conn->hequic[sockindex]; curl_socket_t sockfd = conn->tempsock[sockindex]; result = process_ingress(data, sockfd, qs); if(result) goto error; result = flush_egress(data, sockfd, qs); if(result) goto error; if(quiche_conn_is_established(qs->conn)) { *done = TRUE; result = quiche_has_connected(conn, 0, sockindex); DEBUGF(infof(data, "quiche established connection!\n")); } return result; error: qs_disconnect(data, qs); return result; } static CURLcode process_ingress(struct Curl_easy *data, int sockfd, struct quicsocket *qs) { ssize_t recvd; uint8_t *buf = (uint8_t *)data->state.buffer; size_t bufsize = data->set.buffer_size; struct sockaddr_storage from; socklen_t from_len; quiche_recv_info recv_info; DEBUGASSERT(qs->conn); /* in case the timeout expired */ quiche_conn_on_timeout(qs->conn); do { from_len = sizeof(from); recvd = recvfrom(sockfd, buf, bufsize, 0, (struct sockaddr *)&from, &from_len); if((recvd < 0) && ((SOCKERRNO == EAGAIN) || (SOCKERRNO == EWOULDBLOCK))) break; if(recvd < 0) { failf(data, "quiche: recvfrom() unexpectedly returned %zd " "(errno: %d, socket %d)", recvd, SOCKERRNO, sockfd); return CURLE_RECV_ERROR; } recv_info.from = (struct sockaddr *) &from; recv_info.from_len = from_len; recvd = quiche_conn_recv(qs->conn, buf, recvd, &recv_info); if(recvd == QUICHE_ERR_DONE) break; if(recvd < 0) { failf(data, "quiche_conn_recv() == %zd", recvd); return CURLE_RECV_ERROR; } } while(1); return CURLE_OK; } /* * flush_egress drains the buffers and sends off data. * Calls failf() on errors. */ static CURLcode flush_egress(struct Curl_easy *data, int sockfd, struct quicsocket *qs) { ssize_t sent; uint8_t out[1200]; int64_t timeout_ns; quiche_send_info send_info; do { sent = quiche_conn_send(qs->conn, out, sizeof(out), &send_info); if(sent == QUICHE_ERR_DONE) break; if(sent < 0) { failf(data, "quiche_conn_send returned %zd", sent); return CURLE_SEND_ERROR; } sent = sendto(sockfd, out, sent, 0, (struct sockaddr *)&send_info.to, send_info.to_len); if(sent < 0) { failf(data, "sendto() returned %zd", sent); return CURLE_SEND_ERROR; } } while(1); /* time until the next timeout event, as nanoseconds. */ timeout_ns = quiche_conn_timeout_as_nanos(qs->conn); if(timeout_ns) /* expire uses milliseconds */ Curl_expire(data, (timeout_ns + 999999) / 1000000, EXPIRE_QUIC); return CURLE_OK; } struct h3h1header { char *dest; size_t destlen; /* left to use */ size_t nlen; /* used */ }; static int cb_each_header(uint8_t *name, size_t name_len, uint8_t *value, size_t value_len, void *argp) { struct h3h1header *headers = (struct h3h1header *)argp; size_t olen = 0; if((name_len == 7) && !strncmp(":status", (char *)name, 7)) { msnprintf(headers->dest, headers->destlen, "HTTP/3 %.*s\n", (int) value_len, value); } else if(!headers->nlen) { return CURLE_HTTP3; } else { msnprintf(headers->dest, headers->destlen, "%.*s: %.*s\n", (int)name_len, name, (int) value_len, value); } olen = strlen(headers->dest); headers->destlen -= olen; headers->nlen += olen; headers->dest += olen; return 0; } static ssize_t h3_stream_recv(struct Curl_easy *data, int sockindex, char *buf, size_t buffersize, CURLcode *curlcode) { ssize_t recvd = -1; ssize_t rcode; struct connectdata *conn = data->conn; struct quicsocket *qs = conn->quic; curl_socket_t sockfd = conn->sock[sockindex]; quiche_h3_event *ev; int rc; struct h3h1header headers; struct HTTP *stream = data->req.p.http; headers.dest = buf; headers.destlen = buffersize; headers.nlen = 0; if(process_ingress(data, sockfd, qs)) { infof(data, "h3_stream_recv returns on ingress\n"); *curlcode = CURLE_RECV_ERROR; return -1; } while(recvd < 0) { int64_t s = quiche_h3_conn_poll(qs->h3c, qs->conn, &ev); if(s < 0) /* nothing more to do */ break; if(s != stream->stream3_id) { /* another transfer, ignore for now */ infof(data, "Got h3 for stream %u, expects %u\n", s, stream->stream3_id); continue; } switch(quiche_h3_event_type(ev)) { case QUICHE_H3_EVENT_HEADERS: rc = quiche_h3_event_for_each_header(ev, cb_each_header, &headers); if(rc) { *curlcode = rc; failf(data, "Error in HTTP/3 response header"); break; } recvd = headers.nlen; break; case QUICHE_H3_EVENT_DATA: if(!stream->firstbody) { /* add a header-body separator CRLF */ buf[0] = '\r'; buf[1] = '\n'; buf += 2; buffersize -= 2; stream->firstbody = TRUE; recvd = 2; /* two bytes already */ } else recvd = 0; rcode = quiche_h3_recv_body(qs->h3c, qs->conn, s, (unsigned char *)buf, buffersize); if(rcode <= 0) { recvd = -1; break; } recvd += rcode; break; case QUICHE_H3_EVENT_FINISHED: streamclose(conn, "End of stream"); recvd = 0; /* end of stream */ break; default: break; } quiche_h3_event_free(ev); } if(flush_egress(data, sockfd, qs)) { *curlcode = CURLE_SEND_ERROR; return -1; } *curlcode = (-1 == recvd)? CURLE_AGAIN : CURLE_OK; if(recvd >= 0) /* Get this called again to drain the event queue */ Curl_expire(data, 0, EXPIRE_QUIC); data->state.drain = (recvd >= 0) ? 1 : 0; return recvd; } static ssize_t h3_stream_send(struct Curl_easy *data, int sockindex, const void *mem, size_t len, CURLcode *curlcode) { ssize_t sent; struct connectdata *conn = data->conn; struct quicsocket *qs = conn->quic; curl_socket_t sockfd = conn->sock[sockindex]; struct HTTP *stream = data->req.p.http; if(!stream->h3req) { CURLcode result = http_request(data, mem, len); if(result) { *curlcode = CURLE_SEND_ERROR; return -1; } sent = len; } else { H3BUGF(infof(data, "Pass on %zd body bytes to quiche\n", len)); sent = quiche_h3_send_body(qs->h3c, qs->conn, stream->stream3_id, (uint8_t *)mem, len, FALSE); if(sent < 0) { *curlcode = CURLE_SEND_ERROR; return -1; } } if(flush_egress(data, sockfd, qs)) { *curlcode = CURLE_SEND_ERROR; return -1; } *curlcode = CURLE_OK; return sent; } /* * Store quiche version info in this buffer, Prefix with a space. Return total * length written. */ int Curl_quic_ver(char *p, size_t len) { return msnprintf(p, len, "quiche/%s", quiche_version()); } /* Index where :authority header field will appear in request header field list. */ #define AUTHORITY_DST_IDX 3 static CURLcode http_request(struct Curl_easy *data, const void *mem, size_t len) { /* */ struct connectdata *conn = data->conn; struct HTTP *stream = data->req.p.http; size_t nheader; size_t i; size_t authority_idx; char *hdbuf = (char *)mem; char *end, *line_end; int64_t stream3_id; quiche_h3_header *nva = NULL; struct quicsocket *qs = conn->quic; CURLcode result = CURLE_OK; stream->h3req = TRUE; /* senf off! */ /* Calculate number of headers contained in [mem, mem + len). Assumes a correctly generated HTTP header field block. */ nheader = 0; for(i = 1; i < len; ++i) { if(hdbuf[i] == '\n' && hdbuf[i - 1] == '\r') { ++nheader; ++i; } } if(nheader < 2) goto fail; /* We counted additional 2 \r\n in the first and last line. We need 3 new headers: :method, :path and :scheme. Therefore we need one more space. */ nheader += 1; nva = malloc(sizeof(quiche_h3_header) * nheader); if(!nva) { result = CURLE_OUT_OF_MEMORY; goto fail; } /* Extract :method, :path from request line We do line endings with CRLF so checking for CR is enough */ line_end = memchr(hdbuf, '\r', len); if(!line_end) { result = CURLE_BAD_FUNCTION_ARGUMENT; /* internal error */ goto fail; } /* Method does not contain spaces */ end = memchr(hdbuf, ' ', line_end - hdbuf); if(!end || end == hdbuf) goto fail; nva[0].name = (unsigned char *)":method"; nva[0].name_len = strlen((char *)nva[0].name); nva[0].value = (unsigned char *)hdbuf; nva[0].value_len = (size_t)(end - hdbuf); hdbuf = end + 1; /* Path may contain spaces so scan backwards */ end = NULL; for(i = (size_t)(line_end - hdbuf); i; --i) { if(hdbuf[i - 1] == ' ') { end = &hdbuf[i - 1]; break; } } if(!end || end == hdbuf) goto fail; nva[1].name = (unsigned char *)":path"; nva[1].name_len = strlen((char *)nva[1].name); nva[1].value = (unsigned char *)hdbuf; nva[1].value_len = (size_t)(end - hdbuf); nva[2].name = (unsigned char *)":scheme"; nva[2].name_len = strlen((char *)nva[2].name); if(conn->handler->flags & PROTOPT_SSL) nva[2].value = (unsigned char *)"https"; else nva[2].value = (unsigned char *)"http"; nva[2].value_len = strlen((char *)nva[2].value); authority_idx = 0; i = 3; while(i < nheader) { size_t hlen; hdbuf = line_end + 2; /* check for next CR, but only within the piece of data left in the given buffer */ line_end = memchr(hdbuf, '\r', len - (hdbuf - (char *)mem)); if(!line_end || (line_end == hdbuf)) goto fail; /* header continuation lines are not supported */ if(*hdbuf == ' ' || *hdbuf == '\t') goto fail; for(end = hdbuf; end < line_end && *end != ':'; ++end) ; if(end == hdbuf || end == line_end) goto fail; hlen = end - hdbuf; if(hlen == 4 && strncasecompare("host", hdbuf, 4)) { authority_idx = i; nva[i].name = (unsigned char *)":authority"; nva[i].name_len = strlen((char *)nva[i].name); } else { nva[i].name_len = (size_t)(end - hdbuf); /* Lower case the header name for HTTP/3 */ Curl_strntolower((char *)hdbuf, hdbuf, nva[i].name_len); nva[i].name = (unsigned char *)hdbuf; } hdbuf = end + 1; while(*hdbuf == ' ' || *hdbuf == '\t') ++hdbuf; end = line_end; #if 0 /* This should probably go in more or less like this */ switch(inspect_header((const char *)nva[i].name, nva[i].namelen, hdbuf, end - hdbuf)) { case HEADERINST_IGNORE: /* skip header fields prohibited by HTTP/2 specification. */ --nheader; continue; case HEADERINST_TE_TRAILERS: nva[i].value = (uint8_t*)"trailers"; nva[i].value_len = sizeof("trailers") - 1; break; default: nva[i].value = (unsigned char *)hdbuf; nva[i].value_len = (size_t)(end - hdbuf); } #endif nva[i].value = (unsigned char *)hdbuf; nva[i].value_len = (size_t)(end - hdbuf); ++i; } /* :authority must come before non-pseudo header fields */ if(authority_idx && authority_idx != AUTHORITY_DST_IDX) { quiche_h3_header authority = nva[authority_idx]; for(i = authority_idx; i > AUTHORITY_DST_IDX; --i) { nva[i] = nva[i - 1]; } nva[i] = authority; } /* Warn stream may be rejected if cumulative length of headers is too large. */ #define MAX_ACC 60000 /* <64KB to account for some overhead */ { size_t acc = 0; for(i = 0; i < nheader; ++i) { acc += nva[i].name_len + nva[i].value_len; H3BUGF(infof(data, "h3 [%.*s: %.*s]\n", nva[i].name_len, nva[i].name, nva[i].value_len, nva[i].value)); } if(acc > MAX_ACC) { infof(data, "http_request: Warning: The cumulative length of all " "headers exceeds %d bytes and that could cause the " "stream to be rejected.\n", MAX_ACC); } } switch(data->state.httpreq) { case HTTPREQ_POST: case HTTPREQ_POST_FORM: case HTTPREQ_POST_MIME: case HTTPREQ_PUT: 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, but not zero */ stream3_id = quiche_h3_send_request(qs->h3c, qs->conn, nva, nheader, stream->upload_left ? FALSE: TRUE); if((stream3_id >= 0) && data->set.postfields) { ssize_t sent = quiche_h3_send_body(qs->h3c, qs->conn, stream3_id, (uint8_t *)data->set.postfields, stream->upload_left, TRUE); if(sent <= 0) { failf(data, "quiche_h3_send_body failed!"); result = CURLE_SEND_ERROR; } stream->upload_left = 0; /* nothing left to send */ } break; default: stream3_id = quiche_h3_send_request(qs->h3c, qs->conn, nva, nheader, TRUE); break; } Curl_safefree(nva); if(stream3_id < 0) { H3BUGF(infof(data, "quiche_h3_send_request returned %d\n", stream3_id)); result = CURLE_SEND_ERROR; goto fail; } infof(data, "Using HTTP/3 Stream ID: %x (easy handle %p)\n", stream3_id, (void *)data); stream->stream3_id = stream3_id; return CURLE_OK; fail: free(nva); return result; } /* * Called from transfer.c:done_sending when we stop HTTP/3 uploading. */ CURLcode Curl_quic_done_sending(struct Curl_easy *data) { struct connectdata *conn = data->conn; DEBUGASSERT(conn); if(conn->handler == &Curl_handler_http3) { /* only for HTTP/3 transfers */ ssize_t sent; struct HTTP *stream = data->req.p.http; struct quicsocket *qs = conn->quic; stream->upload_done = TRUE; sent = quiche_h3_send_body(qs->h3c, qs->conn, stream->stream3_id, NULL, 0, TRUE); if(sent < 0) return CURLE_SEND_ERROR; } return CURLE_OK; } /* * Called from http.c:Curl_http_done when a request completes. */ void Curl_quic_done(struct Curl_easy *data, bool premature) { (void)data; (void)premature; } /* * Called from transfer.c:data_pending to know if we should keep looping * to receive more data from the connection. */ bool Curl_quic_data_pending(const struct Curl_easy *data) { (void)data; return FALSE; } #endif