dirmngr: Allow a timeout for HTTP and other TCP connects.
authorWerner Koch <wk@gnupg.org>
Thu, 8 Jun 2017 06:23:06 +0000 (08:23 +0200)
committerWerner Koch <wk@gnupg.org>
Thu, 8 Jun 2017 07:37:36 +0000 (09:37 +0200)
* dirmngr/http.c: Include fcntl.h.
(http_session_s): Add field 'connect_timeout'.
(http_session_new): Clear that.
(http_session_set_timeout): New function.
(my_wsagetlasterror) [W32]: New.
(connect_with_timeout): New function.
(connect_server): Add arg 'timeout' and call connect_with_timeout.
(send_request): Add arg 'timeout' and pass it to connect_server.
(http_raw_connect): Add arg 'timeout'.
(http_open): Pass TIMEOUT from the session to connect_server.
--

Note that the non-blocking connect we implement is traditional a
pretty non-portable thing due to slighly different semantics.  The
code uses the strategy W. Richard Stevens suggested in 1998.
Hopefully current OS versions got it all right.

The code has not been tested on Windows.

Signed-off-by: Werner Koch <wk@gnupg.org>
dirmngr/http.c
dirmngr/http.h
dirmngr/t-http.c

index f461e5d..0544c9b 100644 (file)
@@ -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;
@@ -952,7 +969,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);
@@ -1635,7 +1652,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;
@@ -1762,12 +1780,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, srvtag, 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)
@@ -2526,13 +2544,162 @@ 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)
+    {
+      RESTORE_BLOCKING ();
+      return err;
+    }
+
+  FD_ZERO (&rset);
+  FD_SET (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 +2812,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
             {
index 2609b9e..448cd04 100644 (file)
@@ -124,6 +124,7 @@ void http_session_set_log_cb (http_session_t sess,
                               void (*cb)(http_session_t, gpg_error_t,
                                          const char *,
                                          const void **, size_t *));
+void http_session_set_timeout (http_session_t sess, unsigned int timeout);
 
 
 gpg_error_t http_parse_uri (parsed_uri_t *ret_uri, const char *uri,
@@ -133,7 +134,8 @@ void http_release_parsed_uri (parsed_uri_t uri);
 
 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 http_open (http_t *r_hd, http_req_t reqtype,
                        const char *url,
index a3a74dd..b0f2bcf 100644 (file)
@@ -203,6 +203,7 @@ main (int argc, char **argv)
   int no_crl = 0;
   const char *cafile = NULL;
   http_session_t session = NULL;
+  unsigned int timeout = 0;
 
   gpgrt_init ();
   log_set_prefix (PGM, GPGRT_LOG_WITH_PREFIX | GPGRT_LOG_WITH_PID);
@@ -224,6 +225,7 @@ main (int argc, char **argv)
                  "  --debug           flyswatter\n"
                  "  --tls-debug N     use TLS debug level N\n"
                  "  --cacert FNAME    expect CA certificate in file FNAME\n"
+                 "  --timeout MS      timeout for connect in MS\n"
                  "  --no-verify       do not verify the certificate\n"
                  "  --force-tls       use HTTP_FLAG_FORCE_TLS\n"
                  "  --force-tor       use HTTP_FLAG_FORCE_TOR\n"
@@ -261,6 +263,15 @@ main (int argc, char **argv)
               argc--; argv++;
             }
         }
+      else if (!strcmp (*argv, "--timeout"))
+        {
+          argc--; argv++;
+          if (argc)
+            {
+              timeout = strtoul (*argv, NULL, 10);
+              argc--; argv++;
+            }
+        }
       else if (!strcmp (*argv, "--no-verify"))
         {
           no_verify = 1;
@@ -407,6 +418,9 @@ main (int argc, char **argv)
   http_release_parsed_uri (uri);
   uri = NULL;
 
+  if (session)
+    http_session_set_timeout (session, timeout);
+
   rc = http_open_document (&hd, *argv, NULL, my_http_flags,
                            NULL, session, NULL, NULL);
   if (rc)