Merge T3490-proposal1 into master
[gnupg.git] / dirmngr / http.c
index e74d051..8e778df 100644 (file)
@@ -31,7 +31,7 @@
  */
 
 /* Simple HTTP client implementation.  We try to keep the code as
-   self-contained as possible.  There are some contraints however:
+   self-contained as possible.  There are some constraints however:
 
   - estream is required.  We now require estream because it provides a
     very useful and portable asprintf implementation and the fopencookie
@@ -70,6 +70,7 @@
 # include <sys/socket.h>
 # include <sys/time.h>
 # include <time.h>
+# include <fcntl.h>
 # include <netinet/in.h>
 # include <arpa/inet.h>
 # include <netdb.h>
@@ -153,13 +154,14 @@ static int insert_escapes (char *buffer, const char *string,
 static uri_tuple_t parse_tuple (char *string);
 static gpg_error_t send_request (http_t hd, const char *httphost,
                                  const char *auth,const char *proxy,
-                                const char *srvtag,strlist_t headers);
+                                const char *srvtag, unsigned int timeout,
+                                 strlist_t headers);
 static char *build_rel_path (parsed_uri_t uri);
 static gpg_error_t parse_response (http_t hd);
 
 static gpg_error_t connect_server (const char *server, unsigned short port,
                                    unsigned int flags, const char *srvtag,
-                                   assuan_fd_t *r_sock);
+                                   unsigned int timeout, assuan_fd_t *r_sock);
 static gpgrt_ssize_t read_server (assuan_fd_t sock, void *buffer, size_t size);
 static gpg_error_t write_server (assuan_fd_t sock, const char *data, size_t length);
 
@@ -259,6 +261,9 @@ struct http_session_s
   /* A per-session TLS verification callback.  */
   http_verify_cb_t verify_cb;
   void *verify_cb_value;
+
+  /* The connect timeout */
+  unsigned int connect_timeout;
 };
 
 
@@ -695,6 +700,7 @@ http_session_new (http_session_t *r_session,
   sess->flags = flags;
   sess->verify_cb = verify_cb;
   sess->verify_cb_value = verify_cb_value;
+  sess->connect_timeout = 0;
 
 #if HTTP_USE_NTBTLS
   {
@@ -867,6 +873,15 @@ http_session_set_log_cb (http_session_t sess,
 }
 
 
+/* Set the TIMEOUT in milliseconds for the connection's connect
+ * calls.  Using 0 disables the timeout.  */
+void
+http_session_set_timeout (http_session_t sess, unsigned int timeout)
+{
+  sess->connect_timeout = timeout;
+}
+
+
 
 \f
 /* Start a HTTP retrieval and on success store at R_HD a context
@@ -898,7 +913,9 @@ http_open (http_t *r_hd, http_req_t reqtype, const char *url,
 
   err = parse_uri (&hd->uri, url, 0, !!(flags & HTTP_FLAG_FORCE_TLS));
   if (!err)
-    err = send_request (hd, httphost, auth, proxy, srvtag, headers);
+    err = send_request (hd, httphost, auth, proxy, srvtag,
+                        hd->session? hd->session->connect_timeout : 0,
+                        headers);
 
   if (err)
     {
@@ -918,10 +935,10 @@ http_open (http_t *r_hd, http_req_t reqtype, const char *url,
 
 /* This function is useful to connect to a generic TCP service using
    this http abstraction layer.  This has the advantage of providing
-   service tags and an estream interface.  */
+   service tags and an estream interface.  TIMEOUT is in milliseconds. */
 gpg_error_t
 http_raw_connect (http_t *r_hd, const char *server, unsigned short port,
-                  unsigned int flags, const char *srvtag)
+                  unsigned int flags, const char *srvtag, unsigned int timeout)
 {
   gpg_error_t err = 0;
   http_t hd;
@@ -938,6 +955,10 @@ http_raw_connect (http_t *r_hd, const char *server, unsigned short port,
           log_error ("Tor support is not available\n");
           return gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED);
         }
+      /* Non-blocking connects do not work with our Tor proxy because
+       * we can't continue the Socks protocol after the EINPROGRESS.
+       * Disable the timeout to use a blocking connect.  */
+      timeout = 0;
     }
 
   /* Create the handle. */
@@ -952,7 +973,7 @@ http_raw_connect (http_t *r_hd, const char *server, unsigned short port,
   {
     assuan_fd_t sock;
 
-    err = connect_server (server, port, hd->flags, srvtag, &sock);
+    err = connect_server (server, port, hd->flags, srvtag, timeout, &sock);
     if (err)
       {
         xfree (hd);
@@ -1047,6 +1068,7 @@ http_wait_response (http_t hd)
 {
   gpg_error_t err;
   cookie_t cookie;
+  int use_tls;
 
   /* Make sure that we are in the data. */
   http_start_data (hd);
@@ -1057,6 +1079,7 @@ http_wait_response (http_t hd)
   if (!cookie)
     return gpg_err_make (default_errsource, GPG_ERR_INTERNAL);
 
+  use_tls = cookie->use_tls;
   es_fclose (hd->fp_write);
   hd->fp_write = NULL;
   /* The close has released the cookie and thus we better set it to NULL.  */
@@ -1075,7 +1098,7 @@ http_wait_response (http_t hd)
     return gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
   cookie->sock = my_socket_ref (hd->sock);
   cookie->session = http_session_ref (hd->session);
-  cookie->use_tls = hd->uri->use_tls;
+  cookie->use_tls = use_tls;
 
   hd->read_cookie = cookie;
   hd->fp_read = es_fopencookie (cookie, "r", cookie_functions);
@@ -1202,14 +1225,16 @@ parse_uri (parsed_uri_t *ret_uri, const char *uri,
 {
   gpg_err_code_t ec;
 
-  *ret_uri = xtrycalloc (1, sizeof **ret_uri + strlen (uri));
+  *ret_uri = xtrycalloc (1, sizeof **ret_uri + 2 * strlen (uri) + 1);
   if (!*ret_uri)
     return gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
   strcpy ((*ret_uri)->buffer, uri);
+  strcpy ((*ret_uri)->buffer + strlen (uri) + 1, uri);
+  (*ret_uri)->original = (*ret_uri)->buffer + strlen (uri) + 1;
   ec = do_parse_uri (*ret_uri, 0, no_scheme_check, force_tls);
   if (ec)
     {
-      xfree (*ret_uri);
+      http_release_parsed_uri (*ret_uri);
       *ret_uri = NULL;
     }
   return gpg_err_make (default_errsource, ec);
@@ -1238,6 +1263,11 @@ http_release_parsed_uri (parsed_uri_t uri)
     {
       uri_tuple_t r, r2;
 
+      for (r = uri->params; r; r = r2)
+       {
+         r2 = r->next;
+         xfree (r);
+       }
       for (r = uri->query; r; r = r2)
        {
          r2 = r->next;
@@ -1347,7 +1377,7 @@ do_parse_uri (parsed_uri_t uri, int only_local_part,
          if ((n = remove_escapes (uri->host)) < 0)
            return GPG_ERR_BAD_URI;
          if (n != strlen (uri->host))
-           return GPG_ERR_BAD_URI;     /* Hostname incudes a Nul. */
+           return GPG_ERR_BAD_URI;     /* Hostname includes a Nul. */
          p = p2 ? p2 : NULL;
        }
       else if (uri->is_http)
@@ -1635,7 +1665,8 @@ is_hostname_port (const char *string)
  */
 static gpg_error_t
 send_request (http_t hd, const char *httphost, const char *auth,
-             const char *proxy, const char *srvtag, strlist_t headers)
+             const char *proxy, const char *srvtag, unsigned int timeout,
+              strlist_t headers)
 {
   gpg_error_t err;
   const char *server;
@@ -1645,6 +1676,9 @@ send_request (http_t hd, const char *httphost, const char *auth,
   char *proxy_authstr = NULL;
   char *authstr = NULL;
   assuan_fd_t sock;
+#ifdef USE_TLS
+  int have_http_proxy = 0;
+#endif
 
   if (hd->uri->use_tls && !hd->session)
     {
@@ -1668,6 +1702,10 @@ send_request (http_t hd, const char *httphost, const char *auth,
           log_error ("Tor support is not available\n");
           return gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED);
         }
+      /* Non-blocking connects do not work with our Tor proxy because
+       * we can't continue the Socks protocol after the EINPROGRESS.
+       * Disable the timeout to use a blocking connect.  */
+      timeout = 0;
     }
 
   server = *hd->uri->host ? hd->uri->host : "localhost";
@@ -1731,9 +1769,12 @@ send_request (http_t hd, const char *httphost, const char *auth,
 
       if (err)
         ;
-      else if (!strcmp (uri->scheme, "http") || !strcmp (uri->scheme, "socks4"))
-        ;
-      else if (!strcmp (uri->scheme, "socks5h"))
+#ifdef USE_TLS
+      else if (!strcmp (uri->scheme, "http"))
+        have_http_proxy = 1;
+#endif
+      else if (!strcmp (uri->scheme, "socks4")
+               || !strcmp (uri->scheme, "socks5h"))
         err = gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED);
       else
         err = gpg_err_make (default_errsource, GPG_ERR_INV_URI);
@@ -1762,12 +1803,12 @@ send_request (http_t hd, const char *httphost, const char *auth,
 
       err = connect_server (*uri->host ? uri->host : "localhost",
                             uri->port ? uri->port : 80,
-                            hd->flags, srvtag, &sock);
+                            hd->flags, NULL, timeout, &sock);
       http_release_parsed_uri (uri);
     }
   else
     {
-      err = connect_server (server, port, hd->flags, srvtag, &sock);
+      err = connect_server (server, port, hd->flags, srvtag, timeout, &sock);
     }
 
   if (err)
@@ -1782,6 +1823,94 @@ send_request (http_t hd, const char *httphost, const char *auth,
       return gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
     }
 
+#if USE_TLS
+  if (have_http_proxy && hd->uri->use_tls)
+    {
+      int saved_flags;
+      cookie_t cookie;
+
+      /* Try to use the CONNECT method to proxy our TLS stream.  */
+      request = es_bsprintf
+        ("CONNECT %s:%hu HTTP/1.0\r\nHost: %s:%hu\r\n%s",
+         httphost ? httphost : server,
+         port,
+         httphost ? httphost : server,
+         port,
+         proxy_authstr ? proxy_authstr : "");
+      xfree (proxy_authstr);
+      proxy_authstr = NULL;
+
+      if (! request)
+        return gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+
+      if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP))
+        log_debug_with_string (request, "http.c:request:");
+
+      cookie = xtrycalloc (1, sizeof *cookie);
+      if (! cookie)
+        {
+          err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+          xfree (request);
+          return err;
+        }
+      cookie->sock = my_socket_ref (hd->sock);
+      hd->write_cookie = cookie;
+
+      hd->fp_write = es_fopencookie (cookie, "w", cookie_functions);
+      if (! hd->fp_write)
+        {
+          err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+          my_socket_unref (cookie->sock, NULL, NULL);
+          xfree (cookie);
+          xfree (request);
+          hd->write_cookie = NULL;
+          return err;
+        }
+      else if (es_fputs (request, hd->fp_write) || es_fflush (hd->fp_write))
+        err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+
+      xfree (request);
+      request = NULL;
+
+      /* Make sure http_wait_response doesn't close the stream.  */
+      saved_flags = hd->flags;
+      hd->flags &= ~HTTP_FLAG_SHUTDOWN;
+
+      /* Get the response.  */
+      err = http_wait_response (hd);
+
+      /* Restore flags, destroy stream.  */
+      hd->flags = saved_flags;
+      es_fclose (hd->fp_read);
+      hd->fp_read = NULL;
+      hd->read_cookie = NULL;
+
+      /* Reset state.  */
+      hd->in_data = 0;
+
+      if (err)
+        return err;
+
+      if (hd->status_code != 200)
+        {
+          request = es_bsprintf
+            ("CONNECT %s:%hu",
+             httphost ? httphost : server,
+             port);
+
+          log_error (_("error accessing '%s': http status %u\n"),
+                     request ? request : "out of core",
+                     http_get_status_code (hd));
+
+          xfree (request);
+          return gpg_error (GPG_ERR_NO_DATA);
+        }
+
+      /* We are done with the proxy, the code below will establish a
+       * TLS session and talk directly to the target server.  */
+      http_proxy = NULL;
+    }
+#endif /* USE_TLS */
 
 #if HTTP_USE_NTBTLS
   if (hd->uri->use_tls)
@@ -2185,7 +2314,7 @@ store_header (http_t hd, char *line)
   if (*line == ' ' || *line == '\t')
     {
       /* Continuation. This won't happen too often as it is not
-         recommended.  We use a straightforward implementaion. */
+         recommended.  We use a straightforward implementation. */
       if (!hd->headers)
         return GPG_ERR_PROTOCOL_VIOLATION;
       n += strlen (hd->headers->value);
@@ -2316,7 +2445,7 @@ parse_response (http_t hd)
       if (!len)
        return GPG_ERR_EOF;
 
-      if ((hd->flags & HTTP_FLAG_LOG_RESP))
+      if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP))
         log_debug_with_string (line, "http.c:response:\n");
     }
   while (!*line);
@@ -2361,7 +2490,7 @@ parse_response (http_t hd)
       /* Trim line endings of empty lines. */
       if ((*line == '\r' && line[1] == '\n') || *line == '\n')
        *line = 0;
-      if ((hd->flags & HTTP_FLAG_LOG_RESP))
+      if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP))
         log_info ("http.c:RESP: '%.*s'\n",
                   (int)strlen(line)-(*line&&line[1]?2:0),line);
       if (*line)
@@ -2468,7 +2597,7 @@ start_server ()
 
 /* Return true if SOCKS shall be used.  This is the case if tor_mode
  * is enabled and the desired address is not the loopback address.
- * This function is basically a copy of the same internal fucntion in
+ * This function is basically a copy of the same internal function in
  * Libassuan.  */
 static int
 use_socks (struct sockaddr_storage *addr)
@@ -2526,13 +2655,166 @@ my_sock_new_for_addr (struct sockaddr_storage *addr, int type, int proto)
 }
 
 
+/* Call WSAGetLastError and map it to a libgpg-error.  */
+#ifdef HAVE_W32_SYSTEM
+static gpg_error_t
+my_wsagetlasterror (void)
+{
+  int wsaerr;
+  gpg_err_code_t ec;
+
+  wsaerr = WSAGetLastError ();
+  switch (wsaerr)
+    {
+    case WSAENOTSOCK:        ec = GPG_ERR_EINVAL;       break;
+    case WSAEWOULDBLOCK:     ec = GPG_ERR_EAGAIN;       break;
+    case ERROR_BROKEN_PIPE:  ec = GPG_ERR_EPIPE;        break;
+    case WSANOTINITIALISED:  ec = GPG_ERR_ENOSYS;       break;
+    case WSAENOBUFS:         ec = GPG_ERR_ENOBUFS;      break;
+    case WSAEMSGSIZE:        ec = GPG_ERR_EMSGSIZE;     break;
+    case WSAECONNREFUSED:    ec = GPG_ERR_ECONNREFUSED; break;
+    case WSAEISCONN:         ec = GPG_ERR_EISCONN;      break;
+    case WSAEALREADY:        ec = GPG_ERR_EALREADY;     break;
+    case WSAETIMEDOUT:       ec = GPG_ERR_ETIMEDOUT;    break;
+    default:                 ec = GPG_ERR_EIO;          break;
+    }
+
+  return gpg_err_make (default_errsource, ec);
+}
+#endif /*HAVE_W32_SYSTEM*/
+
+
+/* Connect SOCK and return GPG_ERR_ETIMEOUT if a connection could not
+ * be established within TIMEOUT milliseconds.  0 indicates the
+ * system's default timeout.  The other args are the usual connect
+ * args.  On success 0 is returned, on timeout GPG_ERR_ETIMEDOUT, and
+ * another error code for other errors.  On timeout the caller needs
+ * to close the socket as soon as possible to stop an ongoing
+ * handshake.
+ *
+ * This implementation is for well-behaving systems; see Stevens,
+ * Network Programming, 2nd edition, Vol 1, 15.4.  */
+static gpg_error_t
+connect_with_timeout (assuan_fd_t sock,
+                      struct sockaddr *addr, int addrlen,
+                      unsigned int timeout)
+{
+  gpg_error_t err;
+  int syserr;
+  socklen_t slen;
+  fd_set rset, wset;
+  struct timeval tval;
+  int n;
+
+#ifndef HAVE_W32_SYSTEM
+  int oflags;
+# define RESTORE_BLOCKING()  do {  \
+    fcntl (sock, F_SETFL, oflags); \
+  } while (0)
+#else /*HAVE_W32_SYSTEM*/
+# define RESTORE_BLOCKING()  do {                       \
+    unsigned long along = 0;                            \
+    ioctlsocket (FD2INT (sock), FIONBIO, &along);       \
+  } while (0)
+#endif /*HAVE_W32_SYSTEM*/
+
+
+  if (!timeout)
+    {
+      /* Shortcut.  */
+      if (assuan_sock_connect (sock, addr, addrlen))
+        err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+      else
+        err = 0;
+      return err;
+    }
+
+  /* Switch the socket into non-blocking mode.  */
+#ifdef HAVE_W32_SYSTEM
+  {
+    unsigned long along = 1;
+    if (ioctlsocket (FD2INT (sock), FIONBIO, &along))
+      return my_wsagetlasterror ();
+  }
+#else
+  oflags = fcntl (sock, F_GETFL, 0);
+  if (fcntl (sock, F_SETFL, oflags | O_NONBLOCK))
+    return gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+#endif
+
+  /* Do the connect.  */
+  if (!assuan_sock_connect (sock, addr, addrlen))
+    {
+      /* Immediate connect.  Restore flags. */
+      RESTORE_BLOCKING ();
+      return 0; /* Success.  */
+    }
+  err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+  if (gpg_err_code (err) != GPG_ERR_EINPROGRESS
+#ifdef HAVE_W32_SYSTEM
+      && gpg_err_code (err) != GPG_ERR_EAGAIN
+#endif
+      )
+    {
+      RESTORE_BLOCKING ();
+      return err;
+    }
+
+  FD_ZERO (&rset);
+  FD_SET (FD2INT (sock), &rset);
+  wset = rset;
+  tval.tv_sec = timeout / 1000;
+  tval.tv_usec = (timeout % 1000) * 1000;
+
+  n = my_select (FD2INT(sock)+1, &rset, &wset, NULL, &tval);
+  if (n < 0)
+    {
+      err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+      RESTORE_BLOCKING ();
+      return err;
+    }
+  if (!n)
+    {
+      /* Timeout: We do not restore the socket flags on timeout
+       * because the caller is expected to close the socket.  */
+      return gpg_err_make (default_errsource, GPG_ERR_ETIMEDOUT);
+    }
+  if (!FD_ISSET (sock, &rset) && !FD_ISSET (sock, &wset))
+    {
+      /* select misbehaved.  */
+      return gpg_err_make (default_errsource, GPG_ERR_SYSTEM_BUG);
+    }
+
+  slen = sizeof (syserr);
+  if (getsockopt (FD2INT(sock), SOL_SOCKET, SO_ERROR,
+                  (void*)&syserr, &slen) < 0)
+    {
+      /* Assume that this is Solaris which returns the error in ERRNO.  */
+      err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ());
+    }
+  else if (syserr)
+    err = gpg_err_make (default_errsource, gpg_err_code_from_errno (syserr));
+  else
+    err = 0; /* Connected.  */
+
+  RESTORE_BLOCKING ();
+
+  return err;
+
+#undef RESTORE_BLOCKING
+}
+
+
 /* Actually connect to a server.  On success 0 is returned and the
  * file descriptor for the socket is stored at R_SOCK; on error an
- * error code is returned and ASSUAN_INVALID_FD is stored at
- * R_SOCK.  */
+ * error code is returned and ASSUAN_INVALID_FD is stored at R_SOCK.
+ * TIMEOUT is the connect timeout in milliseconds.  Note that the
+ * function tries to connect to all known addresses and the timeout is
+ * for each one. */
 static gpg_error_t
 connect_server (const char *server, unsigned short port,
-                unsigned int flags, const char *srvtag, assuan_fd_t *r_sock)
+                unsigned int flags, const char *srvtag, unsigned int timeout,
+                assuan_fd_t *r_sock)
 {
   gpg_error_t err;
   assuan_fd_t sock = ASSUAN_INVALID_FD;
@@ -2645,11 +2927,11 @@ connect_server (const char *server, unsigned short port,
             }
 
           anyhostaddr = 1;
-          if (assuan_sock_connect (sock, (struct sockaddr *)ai->addr,
-                                   ai->addrlen))
+          err = connect_with_timeout (sock, (struct sockaddr *)ai->addr,
+                                      ai->addrlen, timeout);
+          if (err)
             {
-              last_err = gpg_err_make (default_errsource,
-                                       gpg_err_code_from_syserror ());
+              last_err = err;
             }
           else
             {
@@ -3016,7 +3298,7 @@ gpg_error_t
 http_verify_server_credentials (http_session_t sess)
 {
 #if HTTP_USE_GNUTLS
-  static const char const errprefix[] = "TLS verification of peer failed";
+  static const char errprefix[] = "TLS verification of peer failed";
   int rc;
   unsigned int status;
   const char *hostname;