]> granicus.if.org Git - curl/commitdiff
HTTP2: add layer between existing http and socket(TLS) layer
authorTatsuhiro Tsujikawa <tatsuhiro.t@gmail.com>
Fri, 31 Jan 2014 15:51:24 +0000 (00:51 +0900)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 4 Feb 2014 13:49:49 +0000 (14:49 +0100)
This patch chooses different approach to integrate HTTP2 into HTTP curl
stack. The idea is that we insert HTTP2 layer between HTTP code and
socket(TLS) layer. When HTTP2 is initialized (either in NPN or Upgrade),
we replace the Curl_recv/Curl_send callbacks with HTTP2's, but keep the
original callbacks in http_conn struct. When sending serialized data by
nghttp2, we use original Curl_send callback. Likewise, when reading data
from network, we use original Curl_recv callback. In this way we can
treat both TLS and non-TLS connections.

With this patch, one can transfer contents from https://twitter.com and
from nghttp2 test server in plain HTTP as well.

The code still has rough edges. The notable one is I could not figure
out how to call nghttp2_session_send() when underlying socket is
writable.

lib/http.c
lib/http.h
lib/http2.c

index 063e1fa5153082421fc13753c9e91b52d110deae..2ea6f7ea5e12369b5dc5197453fb2833b4b3996a 100644 (file)
@@ -1056,6 +1056,7 @@ CURLcode Curl_add_buffer_send(Curl_send_buffer *in,
     return res;
   }
 
+
   if(conn->handler->flags & PROTOPT_SSL) {
     /* We never send more than CURL_MAX_WRITE_SIZE bytes in one single chunk
        when we speak HTTPS, as if only a fraction of it is sent now, this data
@@ -1673,7 +1674,6 @@ CURLcode Curl_http(struct connectdata *conn, bool *done)
       infof(data, "http, we have to use HTTP-draft-09/2\n");
       Curl_http2_init(conn);
       Curl_http2_switched(conn);
-      Curl_http2_send_request(conn);
       break;
     case NPN_HTTP1_1:
       /* continue with HTTP/1.1 when explicitly requested */
index a32a71d7819fe81f7d89b312749f440d0f4e7058..d568114d72a0f6e082579151289bd4956c4ba396 100644 (file)
@@ -149,6 +149,9 @@ struct HTTP {
                         points to an allocated send_buffer struct */
 };
 
+typedef int (*sending)(void); /* Curl_send */
+typedef int (*recving)(void); /* Curl_recv */
+
 struct http_conn {
 #ifdef USE_NGHTTP2
 #define H2_BINSETTINGS_LEN 80
@@ -158,6 +161,9 @@ struct http_conn {
   char *mem;     /* points to a buffer in memory to store or read from */
   size_t len;    /* size of the buffer 'mem' points to */
   bool bodystarted;
+  sending send_underlying; /* underlying send Curl_send callback */
+  recving recv_underlying; /* underlying recv Curl_recv callback */
+  bool closed; /* TRUE on HTTP2 stream close */
 #else
   int unused; /* prevent a compiler warning */
 #endif
index b54df25e377e8ff6689852aa1fcad30f3b9b56ff..b1b49a3cd0de562bf5999ec87e712958f08487b5 100644 (file)
@@ -33,6 +33,7 @@
 #include "sendf.h"
 #include "curl_base64.h"
 #include "curl_memory.h"
+#include "rawstr.h"
 
 /* include memdebug.h last */
 #include "memdebug.h"
@@ -87,17 +88,26 @@ static ssize_t send_callback(nghttp2_session *h2,
                              void *userp)
 {
   struct connectdata *conn = (struct connectdata *)userp;
+  struct http_conn *httpc = &conn->proto.httpc;
   ssize_t written;
-  CURLcode rc =
-    Curl_write(conn, conn->sock[FIRSTSOCKET], data, length, &written);
+  CURLcode rc;
   (void)h2;
   (void)flags;
 
-  if(rc) {
+  rc = 0;
+  written = ((Curl_send*)httpc->send_underlying)(conn, FIRSTSOCKET,
+                                                 data, length, &rc);
+
+  if(rc == CURLE_AGAIN) {
+    return NGHTTP2_ERR_WOULDBLOCK;
+  }
+
+  if(written == -1) {
     failf(conn->data, "Failed sending HTTP2 data");
     return NGHTTP2_ERR_CALLBACK_FAILURE;
   }
-  else if(!written)
+
+  if(!written)
     return NGHTTP2_ERR_WOULDBLOCK;
 
   return written;
@@ -111,6 +121,10 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
   (void)frame;
   infof(conn->data, "on_frame_recv() was called with header %x\n",
         frame->hd.type);
+  if((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) &&
+     frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
+    infof(conn->data, "stream_id=%d closed\n", frame->hd.stream_id);
+  }
   return 0;
 }
 
@@ -193,10 +207,14 @@ static int on_stream_close(nghttp2_session *session, int32_t stream_id,
                            nghttp2_error_code error_code, void *userp)
 {
   struct connectdata *conn = (struct connectdata *)userp;
+  struct http_conn *c = &conn->proto.httpc;
   (void)session;
   (void)stream_id;
   infof(conn->data, "on_stream_close() was called, error_code = %d\n",
         error_code);
+
+  c->closed = TRUE;
+
   return 0;
 }
 
@@ -367,6 +385,7 @@ static ssize_t http2_recv(struct connectdata *conn, int sockindex,
   ssize_t rv;
   ssize_t nread;
   char inbuf[H2_BUFSIZE];
+  struct http_conn *httpc = &conn->proto.httpc;
 
   (void)sockindex; /* we always do HTTP2 on sockindex 0 */
 
@@ -376,72 +395,207 @@ static ssize_t http2_recv(struct connectdata *conn, int sockindex,
   infof(conn->data, "http2_recv: %d bytes buffer\n",
         conn->proto.httpc.len);
 
-  for(;;) {
-    rc = Curl_read_plain(conn->sock[FIRSTSOCKET], inbuf, H2_BUFSIZE, &nread);
+  rc = 0;
+  nread = ((Curl_recv*)httpc->recv_underlying)(conn, FIRSTSOCKET,
+                                               inbuf, H2_BUFSIZE, &rc);
 
-    if(rc == CURLE_AGAIN) {
-      if(len == conn->proto.httpc.len) {
-        *err = rc;
-        return 0;
-      }
-      return len - conn->proto.httpc.len;
-    }
-    if(rc) {
-      failf(conn->data, "Failed receiving HTTP2 data");
-      *err = CURLE_RECV_ERROR;
-      return 0;
-    }
+  if(rc == CURLE_AGAIN) {
+    *err = rc;
+    return -1;
+  }
 
-    if(!nread) {
-      *err = CURLE_RECV_ERROR;
-      return 0; /* TODO EOF? */
-    }
-    rv = nghttp2_session_mem_recv(conn->proto.httpc.h2,
-                                  (const uint8_t *)inbuf, nread);
-
-    if(nghttp2_is_fatal((int)rv)) {
-      failf(conn->data, "nghttp2_session_mem_recv() returned %d:%s\n",
-            rv, nghttp2_strerror((int)rv));
-      *err = CURLE_RECV_ERROR;
-      return 0;
-    }
-    if(rv < nread) {
-      /* Happens when NGHTTP2_ERR_PAUSE is returned from user callback */
-      break;
-    }
-    break;
+  if(nread == -1) {
+    failf(conn->data, "Failed receiving HTTP2 data");
+    *err = rc;
+    return 0;
+  }
+
+  infof(conn->data, "nread=%zd\n", nread);
+  rv = nghttp2_session_mem_recv(httpc->h2, (const uint8_t *)inbuf, nread);
+
+  if(nghttp2_is_fatal((int)rv)) {
+    failf(conn->data, "nghttp2_session_mem_recv() returned %d:%s\n",
+          rv, nghttp2_strerror((int)rv));
+    *err = CURLE_RECV_ERROR;
+    return 0;
+  }
+  infof(conn->data, "nghttp2_session_mem_recv() returns %zd\n", rv);
+  /* Always send pending frames in nghttp2 session, because
+     nghttp2_session_mem_recv() may queue new frame */
+  rv = nghttp2_session_send(httpc->h2);
+  if(rv != 0) {
+    *err = CURLE_SEND_ERROR;
+    return 0;
+  }
+  if(len != httpc->len) {
+    return len - conn->proto.httpc.len;
+  }
+  /* If stream is closed, return 0 to signal the http routine to close
+     the connection */
+  if(httpc->closed) {
+    return 0;
   }
-  return len - conn->proto.httpc.len;
+  *err = CURLE_AGAIN;
+  return -1;
 }
 
+#define MAKE_NV(k, v)                                           \
+  { (uint8_t*)k, (uint8_t*)v, sizeof(k) - 1, sizeof(v) - 1 }
+
+#define MAKE_NV2(k, v, vlen)                            \
+  { (uint8_t*)k, (uint8_t*)v, sizeof(k) - 1, vlen }
+
 /* return number of received (decrypted) bytes */
 static ssize_t http2_send(struct connectdata *conn, int sockindex,
                           const void *mem, size_t len, CURLcode *err)
 {
-  /* TODO: proper implementation */
-  (void)conn;
+  /*
+   * BIG TODO: Currently, we send request in this function, but this
+   * function is also used to send request body. It would be nice to
+   * add dedicated function for request.
+   */
+  int rv;
+  struct http_conn *httpc = &conn->proto.httpc;
+  nghttp2_nv *nva;
+  size_t nheader;
+  size_t i;
+  char *hdbuf = (char*)mem;
+  char *end;
   (void)sockindex;
-  (void)mem;
-  (void)len;
-  (void)err;
-  return 0;
+
+  infof(conn->data, "http2_send len=%zu\n", len);
+
+  /* Calculate number of headers contained in [mem, mem + len) */
+  /* Here, we assume the curl http code generate *correct* HTTP header
+     field block */
+  nheader = 0;
+  for(i = 0; i < len; ++i) {
+    if(hdbuf[i] == 0x0a) {
+      ++nheader;
+    }
+  }
+  /* We counted additional 2 \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(nghttp2_nv) * nheader);
+  if(nva == NULL) {
+    *err = CURLE_OUT_OF_MEMORY;
+    return -1;
+  }
+  /* Extract :method, :path from request line */
+  end = strchr(hdbuf, ' ');
+  nva[0].name = (unsigned char *)":method";
+  nva[0].namelen = (uint16_t)strlen((char *)nva[0].name);
+  nva[0].value = (unsigned char *)hdbuf;
+  nva[0].valuelen = (uint16_t)(end - hdbuf);
+
+  hdbuf = end + 1;
+
+  end = strchr(hdbuf, ' ');
+  nva[1].name = (unsigned char *)":path";
+  nva[1].namelen = (uint16_t)strlen((char *)nva[1].name);
+  nva[1].value = (unsigned char *)hdbuf;
+  nva[1].valuelen = (uint16_t)(end - hdbuf);
+
+  nva[2].name = (unsigned char *)":scheme";
+  nva[2].namelen = (uint16_t)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].valuelen = (uint16_t)strlen((char *)nva[2].value);
+
+  hdbuf = strchr(hdbuf, 0x0a);
+  ++hdbuf;
+
+  for(i = 3; i < nheader; ++i) {
+    end = strchr(hdbuf, ':');
+    assert(end);
+    if(end - hdbuf == 4 && Curl_raw_nequal("host", hdbuf, 4)) {
+      nva[i].name = (unsigned char *)":authority";
+      nva[i].namelen = (uint16_t)strlen((char *)nva[i].name);
+    }
+    else {
+      nva[i].name = (unsigned char *)hdbuf;
+      nva[i].namelen = (uint16_t)(end - hdbuf);
+    }
+    hdbuf = end + 1;
+    for(; *hdbuf == ' '; ++hdbuf);
+    end = strchr(hdbuf, 0x0d);
+    assert(end);
+    nva[i].value = (unsigned char *)hdbuf;
+    nva[i].valuelen = (uint16_t)(end - hdbuf);
+
+    hdbuf = end + 2;
+  }
+
+  rv = nghttp2_submit_request(httpc->h2, 0, nva, nheader, NULL, NULL);
+
+  free(nva);
+
+  if(rv != 0) {
+    *err = CURLE_SEND_ERROR;
+    return -1;
+  }
+
+  rv = nghttp2_session_send(httpc->h2);
+
+  if(rv != 0) {
+    *err = CURLE_SEND_ERROR;
+    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. */
+
+  return len;
 }
 
 int Curl_http2_switched(struct connectdata *conn)
 {
-  int rc;
+  int rv;
+  CURLcode rc;
   struct http_conn *httpc = &conn->proto.httpc;
   /* we are switched! */
-  conn->handler = &Curl_handler_http2;
+  /* Don't know this is needed here at this moment. Original
+     handler->flags is still useful. */
+  /* conn->handler = &Curl_handler_http2; */
+  httpc->recv_underlying = (recving)conn->recv[FIRSTSOCKET];
+  httpc->send_underlying = (sending)conn->send[FIRSTSOCKET];
   conn->recv[FIRSTSOCKET] = http2_recv;
   conn->send[FIRSTSOCKET] = http2_send;
   infof(conn->data, "We have switched to HTTP2\n");
   httpc->bodystarted = FALSE;
-
-  /* send the SETTINGS frame (again) */
-  rc = nghttp2_session_upgrade(httpc->h2, httpc->binsettings, httpc->binlen,
-                               conn);
-  return rc;
+  httpc->closed = FALSE;
+
+  /* TODO: May get CURLE_AGAIN */
+  rv = (int) ((Curl_send*)httpc->send_underlying)
+    (conn, FIRSTSOCKET,
+     NGHTTP2_CLIENT_CONNECTION_HEADER,
+     NGHTTP2_CLIENT_CONNECTION_HEADER_LEN,
+     &rc);
+  assert(rv == 24);
+  if(conn->data->req.upgr101 == UPGR101_RECEIVED) {
+    /* queue SETTINGS frame (again) */
+    rv = nghttp2_session_upgrade(httpc->h2, httpc->binsettings,
+                                 httpc->binlen, NULL);
+    if(rv != 0) {
+      failf(conn->data, "nghttp2_session_upgrade() failed: %s(%d)",
+            nghttp2_strerror(rv), rv);
+      return -1;
+    }
+  }
+  else {
+    rv = nghttp2_submit_settings(httpc->h2, NGHTTP2_FLAG_NONE, NULL, 0);
+    if(rv != 0) {
+      failf(conn->data, "nghttp2_submit_settings() failed: %s(%d)",
+            nghttp2_strerror(rv), rv);
+      return -1;
+    }
+  }
+  return 0;
 }
 
 #endif