From 4173868f663c6fe7ecd1ba2abab20381002adc6b Mon Sep 17 00:00:00 2001 From: Daniel Stenberg Date: Mon, 5 Aug 2019 10:19:48 +0200 Subject: [PATCH] quiche: initial h3 request send/receive --- lib/http.h | 5 + lib/vquic/quiche.c | 390 ++++++++++++++++++++++++++++++++++++++++++--- lib/vquic/quiche.h | 2 + 3 files changed, 373 insertions(+), 24 deletions(-) diff --git a/lib/http.h b/lib/http.h index 72161f6b0..ea1310c39 100644 --- a/lib/http.h +++ b/lib/http.h @@ -186,6 +186,11 @@ struct HTTP { size_t push_headers_used; /* number of entries filled in */ size_t push_headers_alloc; /* number of entries allocated */ #endif + +#ifdef ENABLE_QUIC + /*********** for HTTP/3 we store stream-local data here *************/ + int64_t stream3_id; /* stream we are interested in */ +#endif }; #ifdef USE_NGHTTP2 diff --git a/lib/vquic/quiche.c b/lib/vquic/quiche.c index ac1ba8d48..2ccee1142 100644 --- a/lib/vquic/quiche.c +++ b/lib/vquic/quiche.c @@ -30,12 +30,21 @@ #include "strdup.h" #include "rand.h" #include "quic.h" +#include "strcase.h" +#include "multiif.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 +#ifdef DEBUG_HTTP3 +#define H3BUGF(x) x +#else +#define H3BUGF(x) do { } WHILE_FALSE +#endif + #define QUIC_MAX_STREAMS (256*1024) #define QUIC_MAX_DATA (1*1024*1024) #define QUIC_IDLE_TIMEOUT 60 * 1000 /* milliseconds */ @@ -45,10 +54,73 @@ static CURLcode process_ingress(struct connectdata *conn, static CURLcode flush_egress(struct connectdata *conn, curl_socket_t sockfd); -static Curl_recv quic_stream_recv; -static Curl_send quic_stream_send; +static CURLcode http_request(struct connectdata *conn, const void *mem, + size_t len); +static Curl_recv h3_stream_recv; +static Curl_send h3_stream_send; +static int quiche_getsock(struct connectdata *conn, curl_socket_t *socks) +{ + struct SingleRequest *k = &conn->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 int quiche_perform_getsock(const struct connectdata *conn, + curl_socket_t *socks) +{ + return quiche_getsock((struct connectdata *)conn, socks); +} + +static CURLcode quiche_disconnect(struct connectdata *conn, + bool dead_connection) +{ + (void)conn; + (void)dead_connection; + return CURLE_OK; +} + +static unsigned int quiche_conncheck(struct connectdata *conn, + unsigned int checks_to_perform) +{ + (void)conn; + (void)checks_to_perform; + return CONNRESULT_NONE; +} + +static const struct Curl_handler Curl_handler_h3_quiche = { + "HTTPS", /* scheme */ + ZERO_NULL, /* setup_connection */ + Curl_http, /* 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_perform_getsock, /* perform_getsock */ + quiche_disconnect, /* disconnect */ + ZERO_NULL, /* readwrite */ + quiche_conncheck, /* connection_check */ + PORT_HTTP, /* defport */ + CURLPROTO_HTTPS, /* protocol */ + PROTOPT_SSL | PROTOPT_STREAM /* flags */ +}; + CURLcode Curl_quic_connect(struct connectdata *conn, curl_socket_t sockfd, const struct sockaddr *addr, socklen_t addrlen) { @@ -66,7 +138,8 @@ CURLcode Curl_quic_connect(struct connectdata *conn, curl_socket_t sockfd, quiche_config_set_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_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); @@ -89,7 +162,8 @@ CURLcode Curl_quic_connect(struct connectdata *conn, curl_socket_t sockfd, if(result) return CURLE_FAILED_INIT; /* TODO: better return code */ - infof(conn->data, "Sent QUIC client Initial\n"); + infof(conn->data, "Sent QUIC client Initial, ALPN: %s\n", + QUICHE_H3_APPLICATION_PROTOCOL + 1); return CURLE_OK; } @@ -110,9 +184,11 @@ CURLcode Curl_quic_is_connected(struct connectdata *conn, int sockindex, return result; if(quiche_conn_is_established(qs->conn)) { - conn->recv[sockindex] = quic_stream_recv; - conn->send[sockindex] = quic_stream_send; + conn->recv[sockindex] = h3_stream_recv; + conn->send[sockindex] = h3_stream_send; *done = TRUE; + conn->handler = &Curl_handler_h3_quiche; + DEBUGF(infof(conn->data, "quiche established connection!\n")); } return CURLE_OK; @@ -169,51 +245,103 @@ static CURLcode flush_egress(struct connectdata *conn, int sockfd) return CURLE_OK; } -static ssize_t quic_stream_recv(struct connectdata *conn, - int sockindex, - char *buf, - size_t buffersize, - CURLcode *curlcode) +static int cb_each_header(uint8_t *name, size_t name_len, + uint8_t *value, size_t value_len, + void *argp) +{ + (void)argp; + fprintf(stderr, "got HTTP header: %.*s=%.*s\n", + (int) name_len, name, (int) value_len, value); + return 0; +} + +static ssize_t h3_stream_recv(struct connectdata *conn, + int sockindex, + char *buf, + size_t buffersize, + CURLcode *curlcode) { bool fin; ssize_t recvd; struct quicsocket *qs = &conn->quic; curl_socket_t sockfd = conn->sock[sockindex]; + quiche_h3_event *ev; + int rc; if(process_ingress(conn, sockfd)) { *curlcode = CURLE_RECV_ERROR; return -1; } - recvd = quiche_conn_stream_recv(qs->conn, 0, (uint8_t *) buf, buffersize, &fin); + recvd = quiche_conn_stream_recv(qs->conn, 0, (uint8_t *) buf, buffersize, + &fin); if(recvd == QUICHE_ERR_DONE) { *curlcode = CURLE_AGAIN; return -1; } - if(recvd < 0) { - *curlcode = CURLE_RECV_ERROR; - return -1; + infof(conn->data, "%zd bytes of H3 to deal with\n", recvd); + + while(1) { + int64_t s = quiche_h3_conn_poll(qs->h3c, qs->conn, &ev); + if(s < 0) + /* nothing more to do */ + break; + + switch(quiche_h3_event_type(ev)) { + case QUICHE_H3_EVENT_HEADERS: + rc = quiche_h3_event_for_each_header(ev, cb_each_header, NULL); + if(rc) { + fprintf(stderr, "failed to process headers"); + /* what do we do about this? */ + } + break; + case QUICHE_H3_EVENT_DATA: + recvd = quiche_h3_recv_body(qs->h3c, qs->conn, s, (unsigned char *)buf, + buffersize); + if(recvd <= 0) { + break; + } + break; + + case QUICHE_H3_EVENT_FINISHED: + if(quiche_conn_close(qs->conn, true, 0, NULL, 0) < 0) { + fprintf(stderr, "failed to close connection\n"); + } + break; + } + + quiche_h3_event_free(ev); } *curlcode = CURLE_OK; return recvd; } -static ssize_t quic_stream_send(struct connectdata *conn, - int sockindex, - const void *mem, - size_t len, - CURLcode *curlcode) +static ssize_t h3_stream_send(struct connectdata *conn, + int sockindex, + const void *mem, + size_t len, + CURLcode *curlcode) { ssize_t sent; struct quicsocket *qs = &conn->quic; curl_socket_t sockfd = conn->sock[sockindex]; - sent = quiche_conn_stream_send(qs->conn, 0, mem, len, true); - if(sent < 0) { - *curlcode = CURLE_SEND_ERROR; - return -1; + if(!qs->h3c) { + CURLcode result = http_request(conn, mem, len); + if(result) { + *curlcode = CURLE_SEND_ERROR; + return -1; + } + return len; + } + else { + sent = quiche_conn_stream_send(qs->conn, 0, mem, len, true); + if(sent < 0) { + *curlcode = CURLE_SEND_ERROR; + return -1; + } } if(flush_egress(conn, sockfd)) { @@ -234,4 +362,218 @@ int Curl_quic_ver(char *p, size_t len) return msnprintf(p, len, " quiche"); } +/* Index where :authority header field will appear in request header + field list. */ +#define AUTHORITY_DST_IDX 3 + +static CURLcode http_request(struct connectdata *conn, const void *mem, + size_t len) +{ + /* + */ + struct HTTP *stream = conn->data->req.protop; + 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; + + qs->config = quiche_h3_config_new(0, 1024, 0, 0); + /* TODO: handle failure */ + + /* Create a new HTTP/3 connection on the QUIC connection. */ + qs->h3c = quiche_h3_conn_new_with_transport(qs->conn, qs->config); + /* TODO: handle failure */ + + /* 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) + return CURLE_OUT_OF_MEMORY; + + /* 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) + 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 = (unsigned char *)hdbuf; + nva[i].name_len = (size_t)(end - 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 != 0 && 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(conn->data, "h3 [%.*s: %.*s]\n", + nva[i].name_len, nva[i].name, + nva[i].value_len, nva[i].value)); + } + + if(acc > MAX_ACC) { + infof(conn->data, "http_request: Warning: The cumulative length of all " + "headers exceeds %zu bytes and that could cause the " + "stream to be rejected.\n", MAX_ACC); + } + } + + switch(conn->data->set.httpreq) { + case HTTPREQ_POST: + case HTTPREQ_POST_FORM: + case HTTPREQ_POST_MIME: + case HTTPREQ_PUT: + if(conn->data->state.infilesize != -1) + stream->upload_left = conn->data->state.infilesize; + else + /* data sending without specifying the data amount up front */ + stream->upload_left = -1; /* unknown, but not zero */ + + /* fix the body submission */ + 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(conn->data, "http3_send() send error\n")); + return CURLE_SEND_ERROR; + } + + infof(conn->data, "Using HTTP/3 Stream ID: %x (easy handle %p)\n", + stream3_id, (void *)conn->data); + stream->stream3_id = stream3_id; + + return CURLE_OK; + +fail: + free(nva); + return CURLE_SEND_ERROR; +} + + #endif diff --git a/lib/vquic/quiche.h b/lib/vquic/quiche.h index cf5432962..b09359ac3 100644 --- a/lib/vquic/quiche.h +++ b/lib/vquic/quiche.h @@ -38,6 +38,8 @@ struct quic_handshake { struct quicsocket { quiche_config *cfg; quiche_conn *conn; + quiche_h3_conn *h3c; + quiche_h3_config *config; uint8_t scid[QUICHE_MAX_CONN_ID_LEN]; uint32_t version; };