mirror of
https://github.com/moparisthebest/curl
synced 2024-11-11 03:55:03 -05:00
80b9db1283
quiche has the potential to log qlog files. To enable this, you must build quiche with the qlog feature enabled `cargo build --features qlog`. curl then passes a file descriptor to quiche, which takes ownership of the file. The FD transfer only works on UNIX. The convention is to enable logging when the QLOGDIR environment is set. This should be a path to a folder where files are written with the naming template <SCID>.qlog. Co-authored-by: Lucas Pardue Replaces #5337 Closes #5341
849 lines
24 KiB
C
849 lines
24 KiB
C
/***************************************************************************
|
|
* _ _ ____ _
|
|
* Project ___| | | | _ \| |
|
|
* / __| | | | |_) | |
|
|
* | (__| |_| | _ <| |___
|
|
* \___|\___/|_| \_\_____|
|
|
*
|
|
* Copyright (C) 1998 - 2020, 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.haxx.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 <quiche.h>
|
|
#include <openssl/err.h>
|
|
#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 "dynbuf.h"
|
|
#ifdef HAVE_FCNTL_H
|
|
#include <fcntl.h>
|
|
#endif
|
|
|
|
/* 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 connectdata *conn,
|
|
curl_socket_t sockfd,
|
|
struct quicsocket *qs);
|
|
|
|
static CURLcode flush_egress(struct connectdata *conn, curl_socket_t sockfd,
|
|
struct quicsocket *qs);
|
|
|
|
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)
|
|
{
|
|
struct quicsocket *qs = conn->quic;
|
|
(void)dead_connection;
|
|
quiche_h3_config_free(qs->h3config);
|
|
quiche_h3_conn_free(qs->h3c);
|
|
quiche_config_free(qs->cfg);
|
|
quiche_conn_free(qs->conn);
|
|
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 CURLcode quiche_do(struct connectdata *conn, bool *done)
|
|
{
|
|
struct HTTP *stream = conn->data->req.protop;
|
|
stream->h3req = FALSE; /* not sent */
|
|
return Curl_http(conn, 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_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 */
|
|
};
|
|
|
|
#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 connectdata *conn, curl_socket_t sockfd,
|
|
int sockindex,
|
|
const struct sockaddr *addr, socklen_t addrlen)
|
|
{
|
|
CURLcode result;
|
|
struct quicsocket *qs = &conn->hequic[sockindex];
|
|
struct Curl_easy *data = conn->data;
|
|
|
|
#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->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;
|
|
|
|
if(getenv("SSLKEYLOGFILE"))
|
|
quiche_config_log_keys(qs->cfg);
|
|
|
|
qs->conn = quiche_connect(conn->host.name, (const uint8_t *) qs->scid,
|
|
sizeof(qs->scid), qs->cfg);
|
|
if(!qs->conn) {
|
|
failf(data, "can't create quiche connection");
|
|
return CURLE_OUT_OF_MEMORY;
|
|
}
|
|
|
|
/* Known to not work on Windows */
|
|
#if !defined(WIN32) && defined(HAVE_QUICHE_CONN_SET_QLOG_FD)
|
|
#ifdef O_BINARY
|
|
#define QLOGMODE O_WRONLY|O_CREAT|O_BINARY
|
|
#else
|
|
#define QLOGMODE O_WRONLY|O_CREAT
|
|
#endif
|
|
{
|
|
const char *qlog_dir = getenv("QLOGDIR");
|
|
if(qlog_dir) {
|
|
struct dynbuf fname;
|
|
unsigned int i;
|
|
Curl_dyn_init(&fname, DYN_QLOG_NAME);
|
|
result = Curl_dyn_add(&fname, qlog_dir);
|
|
if(!result)
|
|
result = Curl_dyn_add(&fname, "/");
|
|
for(i = 0; (i < sizeof(qs->scid)) && !result; i++) {
|
|
char hex[3];
|
|
msnprintf(hex, 3, "%02x", qs->scid[i]);
|
|
result = Curl_dyn_add(&fname, hex);
|
|
}
|
|
if(!result)
|
|
result = Curl_dyn_add(&fname, ".qlog");
|
|
|
|
if(!result) {
|
|
int qlogfd = open(Curl_dyn_ptr(&fname), QLOGMODE,
|
|
data->set.new_file_perms);
|
|
if(qlogfd != -1)
|
|
quiche_conn_set_qlog_fd(qs->conn, qlogfd,
|
|
"qlog title", "curl qlog");
|
|
}
|
|
Curl_dyn_free(&fname);
|
|
if(result)
|
|
return result;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
result = flush_egress(conn, sockfd, qs);
|
|
if(result)
|
|
return result;
|
|
|
|
/* store the used address as a string */
|
|
if(!Curl_addr2string((struct sockaddr*)addr, addrlen,
|
|
conn->primary_ip, &conn->primary_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;
|
|
}
|
|
memcpy(conn->ip_addr_str, conn->primary_ip, MAX_IPADR_LEN);
|
|
Curl_persistconninfo(conn);
|
|
|
|
/* for connection reuse purposes: */
|
|
conn->ssl[FIRSTSOCKET].state = ssl_connection_complete;
|
|
|
|
infof(data, "Sent QUIC client Initial, ALPN: %s\n",
|
|
QUICHE_H3_APPLICATION_PROTOCOL + 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 connectdata *conn, int sockindex,
|
|
bool *done)
|
|
{
|
|
CURLcode result;
|
|
struct quicsocket *qs = &conn->hequic[sockindex];
|
|
curl_socket_t sockfd = conn->tempsock[sockindex];
|
|
|
|
result = process_ingress(conn, sockfd, qs);
|
|
if(result)
|
|
return result;
|
|
|
|
result = flush_egress(conn, sockfd, qs);
|
|
if(result)
|
|
return result;
|
|
|
|
if(quiche_conn_is_established(qs->conn)) {
|
|
*done = TRUE;
|
|
result = quiche_has_connected(conn, 0, sockindex);
|
|
DEBUGF(infof(conn->data, "quiche established connection!\n"));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static CURLcode process_ingress(struct connectdata *conn, int sockfd,
|
|
struct quicsocket *qs)
|
|
{
|
|
ssize_t recvd;
|
|
struct Curl_easy *data = conn->data;
|
|
uint8_t *buf = (uint8_t *)data->state.buffer;
|
|
size_t bufsize = data->set.buffer_size;
|
|
|
|
/* in case the timeout expired */
|
|
quiche_conn_on_timeout(qs->conn);
|
|
|
|
do {
|
|
recvd = recv(sockfd, buf, bufsize, 0);
|
|
if((recvd < 0) && ((SOCKERRNO == EAGAIN) || (SOCKERRNO == EWOULDBLOCK)))
|
|
break;
|
|
|
|
if(recvd < 0) {
|
|
failf(conn->data, "quiche: recv() unexpectedly returned %d "
|
|
"(errno: %d, socket %d)", recvd, SOCKERRNO, sockfd);
|
|
return CURLE_RECV_ERROR;
|
|
}
|
|
|
|
recvd = quiche_conn_recv(qs->conn, buf, recvd);
|
|
if(recvd == QUICHE_ERR_DONE)
|
|
break;
|
|
|
|
if(recvd < 0) {
|
|
failf(conn->data, "quiche_conn_recv() == %d", 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 connectdata *conn, int sockfd,
|
|
struct quicsocket *qs)
|
|
{
|
|
ssize_t sent;
|
|
static uint8_t out[1200];
|
|
int64_t timeout_ns;
|
|
|
|
do {
|
|
sent = quiche_conn_send(qs->conn, out, sizeof(out));
|
|
if(sent == QUICHE_ERR_DONE)
|
|
break;
|
|
|
|
if(sent < 0) {
|
|
failf(conn->data, "quiche_conn_send returned %zd\n",
|
|
sent);
|
|
return CURLE_SEND_ERROR;
|
|
}
|
|
|
|
sent = send(sockfd, out, sent, 0);
|
|
if(sent < 0) {
|
|
failf(conn->data, "send() returned %zd\n", 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(conn->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 connectdata *conn,
|
|
int sockindex,
|
|
char *buf,
|
|
size_t buffersize,
|
|
CURLcode *curlcode)
|
|
{
|
|
ssize_t recvd = -1;
|
|
ssize_t rcode;
|
|
struct quicsocket *qs = conn->quic;
|
|
curl_socket_t sockfd = conn->sock[sockindex];
|
|
quiche_h3_event *ev;
|
|
int rc;
|
|
struct h3h1header headers;
|
|
struct Curl_easy *data = conn->data;
|
|
struct HTTP *stream = data->req.protop;
|
|
headers.dest = buf;
|
|
headers.destlen = buffersize;
|
|
headers.nlen = 0;
|
|
|
|
if(process_ingress(conn, 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(conn, 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 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];
|
|
struct HTTP *stream = conn->data->req.protop;
|
|
|
|
if(!stream->h3req) {
|
|
CURLcode result = http_request(conn, mem, len);
|
|
if(result) {
|
|
*curlcode = CURLE_SEND_ERROR;
|
|
return -1;
|
|
}
|
|
sent = len;
|
|
}
|
|
else {
|
|
H3BUGF(infof(conn->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(conn, 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 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;
|
|
CURLcode result = CURLE_OK;
|
|
struct Curl_easy *data = conn->data;
|
|
|
|
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 != 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(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 %zu bytes and that could cause the "
|
|
"stream to be rejected.\n", MAX_ACC);
|
|
}
|
|
}
|
|
|
|
switch(data->set.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 connectdata *conn)
|
|
{
|
|
if(conn->handler == &Curl_handler_http3) {
|
|
/* only for HTTP/3 transfers */
|
|
ssize_t sent;
|
|
struct HTTP *stream = conn->data->req.protop;
|
|
struct quicsocket *qs = conn->quic;
|
|
fprintf(stderr, "!!! Curl_quic_done_sending\n");
|
|
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
|