diff --git a/lib/http.c b/lib/http.c index 9ce216ef4..4ec38735a 100644 --- a/lib/http.c +++ b/lib/http.c @@ -1493,6 +1493,10 @@ static CURLcode expect100(struct SessionHandle *data, const char *ptr; data->state.expect100header = FALSE; /* default to false unless it is set to TRUE below */ + if(conn->httpversion == 20) { + /* We don't use Expect in HTTP2 */ + return CURLE_OK; + } if(use_http_1_1plus(data, conn)) { /* if not doing HTTP 1.0 or disabled explicitly, we add a Expect: 100-continue to the headers which actually speeds up post operations @@ -1797,35 +1801,40 @@ CURLcode Curl_http(struct connectdata *conn, bool *done) } #endif - ptr = Curl_checkheaders(data, "Transfer-Encoding:"); - if(ptr) { - /* Some kind of TE is requested, check if 'chunked' is chosen */ - data->req.upload_chunky = - Curl_compareheader(ptr, "Transfer-Encoding:", "chunked"); - } + if(conn->httpversion == 20) + /* In HTTP2 forbids Transfer-Encoding: chunked */ + ptr = NULL; else { - if((conn->handler->protocol&CURLPROTO_HTTP) && - data->set.upload && - (data->set.infilesize == -1)) { - if(conn->bits.authneg) - /* don't enable chunked during auth neg */ - ; - else if(use_http_1_1plus(data, conn)) { - /* HTTP, upload, unknown file size and not HTTP 1.0 */ - data->req.upload_chunky = TRUE; - } - else { - failf(data, "Chunky upload is not supported by HTTP 1.0"); - return CURLE_UPLOAD_FAILED; - } + ptr = Curl_checkheaders(data, "Transfer-Encoding:"); + if(ptr) { + /* Some kind of TE is requested, check if 'chunked' is chosen */ + data->req.upload_chunky = + Curl_compareheader(ptr, "Transfer-Encoding:", "chunked"); } else { - /* else, no chunky upload */ - data->req.upload_chunky = FALSE; - } + if((conn->handler->protocol&CURLPROTO_HTTP) && + data->set.upload && + (data->set.infilesize == -1)) { + if(conn->bits.authneg) + /* don't enable chunked during auth neg */ + ; + else if(use_http_1_1plus(data, conn)) { + /* HTTP, upload, unknown file size and not HTTP 1.0 */ + data->req.upload_chunky = TRUE; + } + else { + failf(data, "Chunky upload is not supported by HTTP 1.0"); + return CURLE_UPLOAD_FAILED; + } + } + else { + /* else, no chunky upload */ + data->req.upload_chunky = FALSE; + } - if(data->req.upload_chunky) - te = "Transfer-Encoding: chunked\r\n"; + if(data->req.upload_chunky) + te = "Transfer-Encoding: chunked\r\n"; + } } Curl_safefree(conn->allocptr.host); @@ -2465,7 +2474,10 @@ CURLcode Curl_http(struct connectdata *conn, bool *done) if(data->set.postfields) { - if(!data->state.expect100header && + /* In HTTP2, we send request body in DATA frame regardless of + its size. */ + if(conn->httpversion != 20 && + !data->state.expect100header && (postsize < MAX_INITIAL_POST_SIZE)) { /* if we don't use expect: 100 AND postsize is less than MAX_INITIAL_POST_SIZE diff --git a/lib/http.h b/lib/http.h index db322bf96..407f7b621 100644 --- a/lib/http.h +++ b/lib/http.h @@ -158,7 +158,7 @@ struct http_conn { nghttp2_session *h2; uint8_t binsettings[H2_BINSETTINGS_LEN]; size_t binlen; /* length of the binsettings data */ - char *mem; /* points to a buffer in memory to store or read from */ + char *mem; /* points to a buffer in memory to store */ size_t len; /* size of the buffer 'mem' points to */ bool bodystarted; sending send_underlying; /* underlying send Curl_send callback */ @@ -172,6 +172,14 @@ struct http_conn { on_data_chunk */ size_t datalen; /* the number of bytes left in data */ char *inbuf; /* buffer to receive data from underlying socket */ + /* We need separate buffer for transmission and reception because we + may call nghttp2_session_send() after the + nghttp2_session_mem_recv() but mem buffer is still not full. In + this case, we wrongly sends the content of mem buffer if we share + them for both cases. */ + const uint8_t *upload_mem; /* points to a buffer to read from */ + size_t upload_len; /* size of the buffer 'upload_mem' points to */ + size_t upload_left; /* number of bytes left to upload */ #else int unused; /* prevent a compiler warning */ #endif diff --git a/lib/http2.c b/lib/http2.c index a4baca9f1..e8d31d5ca 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -34,6 +34,7 @@ #include "curl_base64.h" #include "curl_memory.h" #include "rawstr.h" +#include "multiif.h" /* include memdebug.h last */ #include "memdebug.h" @@ -42,6 +43,38 @@ #error too old nghttp2 version, upgrade! #endif +static int http2_perform_getsock(const struct connectdata *conn, + curl_socket_t *sock, /* points to + numsocks + number of + sockets */ + int numsocks) +{ + const struct http_conn *httpc = &conn->proto.httpc; + int bitmap = GETSOCK_BLANK; + (void)numsocks; + + /* TODO We should check underlying socket state if it is SSL socket + because of renegotiation. */ + sock[0] = conn->sock[FIRSTSOCKET]; + + if(nghttp2_session_want_read(httpc->h2)) + bitmap |= GETSOCK_READSOCK(FIRSTSOCKET); + + if(nghttp2_session_want_write(httpc->h2)) + bitmap |= GETSOCK_WRITESOCK(FIRSTSOCKET); + + return bitmap; +} + +static int http2_getsock(struct connectdata *conn, + curl_socket_t *sock, /* points to numsocks + number of sockets */ + int numsocks) +{ + return http2_perform_getsock(conn, sock, numsocks); +} + /* * HTTP2 handler interface. This isn't added to the general list of protocols * but will be used at run-time when the protocol is dynamically switched from @@ -56,10 +89,10 @@ const struct Curl_handler Curl_handler_http2 = { ZERO_NULL, /* connect_it */ ZERO_NULL, /* connecting */ ZERO_NULL, /* doing */ - ZERO_NULL, /* proto_getsock */ - ZERO_NULL, /* doing_getsock */ + http2_getsock, /* proto_getsock */ + http2_getsock, /* doing_getsock */ ZERO_NULL, /* domore_getsock */ - ZERO_NULL, /* perform_getsock */ + http2_perform_getsock, /* perform_getsock */ ZERO_NULL, /* disconnect */ ZERO_NULL, /* readwrite */ PORT_HTTP, /* defport */ @@ -67,6 +100,25 @@ const struct Curl_handler Curl_handler_http2 = { PROTOPT_NONE /* flags */ }; +const struct Curl_handler Curl_handler_http2_ssl = { + "HTTP2", /* scheme */ + ZERO_NULL, /* setup_connection */ + ZERO_NULL, /* do_it */ + ZERO_NULL , /* done */ + ZERO_NULL, /* do_more */ + ZERO_NULL, /* connect_it */ + ZERO_NULL, /* connecting */ + ZERO_NULL, /* doing */ + http2_getsock, /* proto_getsock */ + http2_getsock, /* doing_getsock */ + ZERO_NULL, /* domore_getsock */ + http2_perform_getsock, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* readwrite */ + PORT_HTTP, /* defport */ + CURLPROTO_HTTP | CURLPROTO_HTTPS, /* protocol */ + PROTOPT_SSL /* flags */ +}; /* * Store nghttp2 version info in this buffer, Prefix with a space. Return @@ -326,6 +378,37 @@ static const nghttp2_session_callbacks callbacks = { on_header /* nghttp2_on_header_callback */ }; +static ssize_t data_source_read_callback(nghttp2_session *session, + int32_t stream_id, + uint8_t *buf, size_t length, + int *eof, + nghttp2_data_source *source, + void *userp) +{ + struct connectdata *conn = (struct connectdata *)userp; + struct http_conn *c = &conn->proto.httpc; + size_t nread; + (void)session; + (void)stream_id; + (void)eof; + (void)source; + + nread = c->upload_len < length ? c->upload_len : length; + if(nread > 0) { + memcpy(buf, c->upload_mem, nread); + c->upload_mem += nread; + c->upload_len -= nread; + c->upload_left -= nread; + } + + if(c->upload_left == 0) + *eof = 1; + else if(nread == 0) + return NGHTTP2_ERR_DEFERRED; + + return nread; +} + /* * The HTTP2 settings we send in the Upgrade request */ @@ -428,6 +511,11 @@ static ssize_t http2_recv(struct connectdata *conn, int sockindex, (void)sockindex; /* we always do HTTP2 on sockindex 0 */ + /* Nullify here because we call nghttp2_session_send() and they + might refer to the old buffer. */ + httpc->upload_mem = NULL; + httpc->upload_len = 0; + if(httpc->bodystarted && httpc->nread_header_recvbuf < httpc->header_recvbuf->size_used) { size_t left = @@ -527,10 +615,25 @@ static ssize_t http2_send(struct connectdata *conn, int sockindex, size_t i; char *hdbuf = (char*)mem; char *end; + nghttp2_data_provider data_prd; (void)sockindex; infof(conn->data, "http2_send len=%zu\n", len); + if(httpc->stream_id != -1) { + /* If stream_id != -1, we have dispatched request HEADERS, and now + are going to send or sending request body in DATA frame */ + httpc->upload_mem = mem; + httpc->upload_len = len; + nghttp2_session_resume_data(httpc->h2, httpc->stream_id); + rv = nghttp2_session_send(httpc->h2); + if(nghttp2_is_fatal(rv)) { + *err = CURLE_SEND_ERROR; + return -1; + } + return len - httpc->upload_len; + } + /* Calculate number of headers contained in [mem, mem + len) */ /* Here, we assume the curl http code generate *correct* HTTP header field block */ @@ -594,9 +697,31 @@ static ssize_t http2_send(struct connectdata *conn, int sockindex, nva[i].valuelen = (uint16_t)(end - hdbuf); hdbuf = end + 2; + /* Inspect Content-Length header field and retrieve the request + entity length so that we can set END_STREAM to the last DATA + frame. */ + if(nva[i].namelen == 14 && + Curl_raw_nequal("content-length", (char*)nva[i].name, 14)) { + size_t j; + for(j = 0; j < nva[i].valuelen; ++j) { + httpc->upload_left *= 10; + httpc->upload_left += nva[i].value[j] - '0'; + } + infof(conn->data, "request content-length=%zu\n", httpc->upload_left); + } } - rv = nghttp2_submit_request(httpc->h2, 0, nva, nheader, NULL, NULL); + switch(conn->data->set.httpreq) { + case HTTPREQ_POST: + case HTTPREQ_POST_FORM: + case HTTPREQ_PUT: + data_prd.read_callback = data_source_read_callback; + data_prd.source.ptr = NULL; + rv = nghttp2_submit_request(httpc->h2, 0, nva, nheader, &data_prd, NULL); + break; + default: + rv = nghttp2_submit_request(httpc->h2, 0, nva, nheader, NULL, NULL); + } free(nva); @@ -612,9 +737,17 @@ static ssize_t http2_send(struct connectdata *conn, int sockindex, return -1; } - /* TODO: Still whole HEADERS frame may have not been sent because of - EAGAIN. But I don't know how to setup to call - nghttp2_session_send() when socket becomes writable. */ + if(httpc->stream_id != -1) { + /* If whole HEADERS frame was sent off to the underlying socket, + the nghttp2 library calls data_source_read_callback. But only + it found that no data available, so it deferred the DATA + transmission. Which means that nghttp2_session_want_write() + returns 0 on http2_perform_getsock(), which results that no + writable socket check is performed. To workaround this, we + issue nghttp2_session_resume_data() here to bring back DATA + transmission from deferred state. */ + nghttp2_session_resume_data(httpc->h2, httpc->stream_id); + } return len; } @@ -627,7 +760,11 @@ int Curl_http2_switched(struct connectdata *conn) /* we are switched! */ /* Don't know this is needed here at this moment. Original handler->flags is still useful. */ - /* conn->handler = &Curl_handler_http2; */ + if(conn->handler->flags & PROTOPT_SSL) + conn->handler = &Curl_handler_http2_ssl; + else + conn->handler = &Curl_handler_http2; + httpc->recv_underlying = (recving)conn->recv[FIRSTSOCKET]; httpc->send_underlying = (sending)conn->send[FIRSTSOCKET]; conn->recv[FIRSTSOCKET] = http2_recv; @@ -639,6 +776,9 @@ int Curl_http2_switched(struct connectdata *conn) httpc->nread_header_recvbuf = 0; httpc->data = NULL; httpc->datalen = 0; + httpc->upload_left = 0; + httpc->upload_mem = NULL; + httpc->upload_len = 0; conn->httpversion = 20;