aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorTatsuhiro Tsujikawa <tatsuhiro.t@gmail.com>2014-02-14 22:02:44 +0900
committerDaniel Stenberg <daniel@haxx.se>2014-02-17 23:45:58 +0100
commit035b91a26c363db566578f49aec30ab8527581a7 (patch)
tree158869798ec2e44c3647a64742d2da9f829f90b4 /lib
parente9dfdef4111dca11d3cbf84d64372af56e9d9e09 (diff)
http2: Support HTTP POST/PUT
This patch enables HTTP POST/PUT in HTTP2. We disabled Expect header field and chunked transfer encoding since HTTP2 forbids them. In HTTP1, Curl sends small upload data with request headers, but HTTP2 requires upload data must be in DATA frame separately. So we added some conditionals to achieve this.
Diffstat (limited to 'lib')
-rw-r--r--lib/http.c62
-rw-r--r--lib/http.h10
-rw-r--r--lib/http2.c156
3 files changed, 194 insertions, 34 deletions
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;
+ 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 {
+ 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 {
- failf(data, "Chunky upload is not supported by HTTP 1.0");
- return CURLE_UPLOAD_FAILED;
+ /* else, no chunky upload */
+ data->req.upload_chunky = FALSE;
}
- }
- 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;