dirmngr: Detect dead keyservers and try another one.
[gnupg.git] / dirmngr / ks-engine-hkp.c
index 0c13185..28b05e9 100644 (file)
@@ -1,5 +1,6 @@
 /* ks-engine-hkp.c - HKP keyserver engine
  * Copyright (C) 2011, 2012 Free Software Foundation, Inc.
+ * Copyright (C) 2011, 2012, 2014 Werner Koch
  *
  * This file is part of GnuPG.
  *
@@ -17,7 +18,6 @@
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
-#warning fixme Windows part not yet done
 #include <config.h>
 
 #include <stdio.h>
 #include "userids.h"
 #include "ks-engine.h"
 
+/* Substitute a missing Mingw macro.  */
+#ifndef EAI_OVERFLOW
+# define EAI_OVERFLOW EAI_FAIL
+#endif
+
+
 /* To match the behaviour of our old gpgkeys helper code we escape
    more characters than actually needed. */
 #define EXTRA_ESCAPE_CHARS "@!\"#$%&'()*+,-./:;<=>?[\\]^_{|}~"
@@ -47,6 +53,9 @@
 /* How many redirections do we allow.  */
 #define MAX_REDIRECTS 2
 
+/* Number of retries done for a dead host etc.  */
+#define SEND_REQUEST_RETRIES 3
+
 /* Objects used to maintain information about hosts.  */
 struct hostinfo_s;
 typedef struct hostinfo_s *hostinfo_t;
@@ -94,6 +103,7 @@ create_new_hostinfo (const char *name)
   hi->lastfail = (time_t)(-1);
   hi->v4 = 0;
   hi->v6 = 0;
+  hi->dead = 0;
 
   /* Add it to the hosttable. */
   for (idx=0; idx < hosttable_size; idx++)
@@ -151,6 +161,19 @@ sort_hostpool (const void *xa, const void *xb)
 }
 
 
+/* Return true if the host with the hosttable index TBLIDX is in POOL.  */
+static int
+host_in_pool_p (int *pool, int tblidx)
+{
+  int i, pidx;
+
+  for (i=0; (pidx = pool[i]) != -1; i++)
+    if (pidx == tblidx && hosttable[pidx])
+      return 1;
+  return 0;
+}
+
+
 /* Select a random host.  Consult TABLE which indices into the global
    hosttable.  Returns index into TABLE or -1 if no host could be
    selected.  */
@@ -161,8 +184,8 @@ select_random_host (int *table)
   size_t tblsize;
   int pidx, idx;
 
-  /* We create a new table so that we select only from currently alive
-     hosts.  */
+  /* We create a new table so that we randomly select only from
+     currently alive hosts.  */
   for (idx=0, tblsize=0; (pidx = table[idx]) != -1; idx++)
     if (hosttable[pidx] && !hosttable[pidx]->dead)
       tblsize++;
@@ -179,20 +202,50 @@ select_random_host (int *table)
   if (tblsize == 1)  /* Save a get_uint_nonce.  */
     pidx = tbl[0];
   else
-    pidx = get_uint_nonce () % tblsize;
+    pidx = tbl[get_uint_nonce () % tblsize];
 
   xfree (tbl);
   return pidx;
 }
 
 
+/* Simplified version of getnameinfo which also returns a numeric
+   hostname inside of brackets.  The caller should provide a buffer
+   for TMPHOST which is 2 bytes larger than the the largest hostname.
+   returns 0 on success or an EAI error code.  */
+static int
+my_getnameinfo (const struct sockaddr *sa, socklen_t salen,
+                char *host, size_t hostlen)
+{
+  int ec;
+
+  if (hostlen < 5)
+    return EAI_OVERFLOW;
+
+  ec = getnameinfo (sa, salen, host, hostlen, NULL, 0, NI_NAMEREQD);
+  if (!ec && *host == '[')
+    ec = EAI_FAIL;  /* A name may never start with a bracket.  */
+  else if (ec == EAI_NONAME)
+    {
+      *host = '[';
+      ec = getnameinfo (sa, salen, host + 1, hostlen - 2,
+                        NULL, 0, NI_NUMERICHOST);
+      if (!ec)
+        strcat (host, "]");
+    }
+
+  return ec;
+}
+
+
 /* Map the host name NAME to the actual to be used host name.  This
    allows us to manage round robin DNS names.  We use our own strategy
    to choose one of the hosts.  For example we skip those hosts which
    failed for some time and we stick to one host for a time
-   independent of DNS retry times.  */
+   independent of DNS retry times.  If FORCE_RESELECT is true a new
+   host is always selected. */
 static char *
-map_host (const char *name)
+map_host (ctrl_t ctrl, const char *name, int force_reselect)
 {
   hostinfo_t hi;
   int idx;
@@ -241,12 +294,13 @@ map_host (const char *name)
               if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
                 continue;
 
-              log_printhex ("getaddrinfo returned", ai->ai_addr,ai->ai_addrlen);
-              if ((ec=getnameinfo (ai->ai_addr, ai->ai_addrlen,
-                                   tmphost, sizeof tmphost,
-                                   NULL, 0, NI_NAMEREQD)))
-                log_info ("getnameinfo failed while checking '%s': %s\n",
-                          name, gai_strerror (ec));
+              dirmngr_tick (ctrl);
+              if ((ec = my_getnameinfo (ai->ai_addr, ai->ai_addrlen,
+                                        tmphost, sizeof tmphost)))
+                {
+                  log_info ("getnameinfo failed while checking '%s': %s\n",
+                            name, gai_strerror (ec));
+                }
               else if (refidx+1 >= reftblsize)
                 {
                   log_error ("getnameinfo returned for '%s': '%s'"
@@ -266,7 +320,7 @@ map_host (const char *name)
 
                       for (i=0; i < refidx; i++)
                         if (reftbl[i] == tmpidx)
-                      break;
+                          break;
                       if (!(i < refidx) && tmpidx != idx)
                         reftbl[refidx++] = tmpidx;
                     }
@@ -319,8 +373,10 @@ map_host (const char *name)
     {
       /* If the currently selected host is now marked dead, force a
          re-selection .  */
-      if (hi->poolidx >= 0 && hi->poolidx < hosttable_size
-          && hosttable[hi->poolidx] && hosttable[hi->poolidx]->dead)
+      if (force_reselect)
+        hi->poolidx = -1;
+      else if (hi->poolidx >= 0 && hi->poolidx < hosttable_size
+               && hosttable[hi->poolidx] && hosttable[hi->poolidx]->dead)
         hi->poolidx = -1;
 
       /* Select a host if needed.  */
@@ -349,53 +405,171 @@ map_host (const char *name)
 }
 
 
-/* Mark the host NAME as dead.  */
-static void
+/* Mark the host NAME as dead.  NAME may be given as an URL.  Returns
+   true if a host was really marked as dead or was already marked dead
+   (e.g. by a concurrent session).  */
+static int
 mark_host_dead (const char *name)
 {
-  hostinfo_t hi;
-  int idx;
+  const char *host;
+  char *host_buffer = NULL;
+  parsed_uri_t parsed_uri = NULL;
+  int done = 0;
+
+  if (name && *name && !http_parse_uri (&parsed_uri, name, 1))
+    {
+      if (parsed_uri->v6lit)
+        {
+          host_buffer = strconcat ("[", parsed_uri->host, "]", NULL);
+          if (!host_buffer)
+            log_error ("out of core in mark_host_dead");
+          host = host_buffer;
+        }
+      else
+        host = parsed_uri->host;
+    }
+  else
+    host = name;
+
+  if (host && *host && strcmp (host, "localhost"))
+    {
+      hostinfo_t hi;
+      int idx;
+
+      idx = find_hostinfo (host);
+      if (idx != -1)
+        {
+          hi = hosttable[idx];
+          log_info ("marking host '%s' as dead%s\n",
+                    hi->name, hi->dead? " (again)":"");
+          hi->dead = 1;
+          done = 1;
+        }
+    }
+
+  http_release_parsed_uri (parsed_uri);
+  xfree (host_buffer);
+  return done;
+}
+
+
+/* Mark a host in the hosttable as dead or - if ALIVE is true - as
+   alive.  If the host NAME does not exist a warning status message is
+   printed.  */
+gpg_error_t
+ks_hkp_mark_host (ctrl_t ctrl, const char *name, int alive)
+{
+  gpg_error_t err = 0;
+  hostinfo_t hi, hi2;
+  int idx, idx2, idx3, n;
 
   if (!name || !*name || !strcmp (name, "localhost"))
-    return;
+    return 0;
 
   idx = find_hostinfo (name);
   if (idx == -1)
-    return;
+    return gpg_error (GPG_ERR_NOT_FOUND);
+
   hi = hosttable[idx];
-  log_info ("marking host '%s' as dead%s\n", hi->name, hi->dead? " (again)":"");
-  hi->dead = 1;
+  if (alive && hi->dead)
+    {
+      hi->dead = 0;
+      err = ks_printf_help (ctrl, "marking '%s' as alive", name);
+    }
+  else if (!alive && !hi->dead)
+    {
+      hi->dead = 1;
+      err = ks_printf_help (ctrl, "marking '%s' as dead", name);
+    }
+
+  /* If the host is a pool mark all member hosts. */
+  if (!err && hi->pool)
+    {
+      for (idx2=0; !err && (n=hi->pool[idx2]) != -1; idx2++)
+        {
+          assert (n >= 0 && n < hosttable_size);
+
+          if (!alive)
+            {
+              /* Do not mark a host from a pool dead if it is also a
+                 member in another pool.  */
+              for (idx3=0; idx3 < hosttable_size; idx3++)
+                {
+                  if (hosttable[idx3] && hosttable[idx3]
+                      && hosttable[idx3]->pool
+                      && idx3 != idx
+                      && host_in_pool_p (hosttable[idx3]->pool, n))
+                    break;
+                }
+              if (idx3 < hosttable_size)
+                continue;  /* Host is also a member of another pool.  */
+            }
+
+          hi2 = hosttable[n];
+          if (!hi2)
+            ;
+          else if (alive && hi2->dead)
+            {
+              hi2->dead = 0;
+              err = ks_printf_help (ctrl, "marking '%s' as alive",
+                                    hi2->name);
+            }
+          else if (!alive && !hi2->dead)
+            {
+              hi2->dead = 1;
+              err = ks_printf_help (ctrl, "marking '%s' as dead",
+                                    hi2->name);
+            }
+        }
+    }
+
+  return err;
 }
 
 
 /* Debug function to print the entire hosttable.  */
-void
-ks_hkp_print_hosttable (void)
+gpg_error_t
+ks_hkp_print_hosttable (ctrl_t ctrl)
 {
+  gpg_error_t err;
   int idx, idx2;
   hostinfo_t hi;
+  membuf_t mb;
+  char *p;
+
+  err = ks_print_help (ctrl, "hosttable (idx, ipv4, ipv6, dead, name):");
+  if (err)
+    return err;
 
   for (idx=0; idx < hosttable_size; idx++)
     if ((hi=hosttable[idx]))
       {
-        log_info ("hosttable %3d %s %s %s %s\n",
-                  idx, hi->v4? "4":" ", hi->v6? "6":" ",
-                  hi->dead? "d":" ", hi->name);
+        err = ks_printf_help (ctrl, "%3d %s %s %s %s\n",
+                              idx, hi->v4? "4":" ", hi->v6? "6":" ",
+                              hi->dead? "d":" ", hi->name);
+        if (err)
+          return err;
         if (hi->pool)
           {
-            log_info ("          -->");
+            init_membuf (&mb, 256);
+            put_membuf_printf (&mb, "  .   -->");
             for (idx2=0; hi->pool[idx2] != -1; idx2++)
               {
-                log_printf (" %d", hi->pool[idx2]);
-                if (hi->poolidx == idx2)
-                  log_printf ("*");
+                put_membuf_printf (&mb, " %d", hi->pool[idx2]);
+                if (hi->poolidx == hi->pool[idx2])
+                  put_membuf_printf (&mb, "*");
               }
-            log_printf ("\n");
-            /* for (idx2=0; hi->pool[idx2] != -1; idx2++) */
-            /*   log_info ("              (%s)\n", */
-            /*              hosttable[hi->pool[idx2]]->name); */
+            put_membuf( &mb, "", 1);
+            p = get_membuf (&mb, NULL);
+            if (!p)
+              return gpg_error_from_syserror ();
+            err = ks_print_help (ctrl, p);
+            xfree (p);
+            if (err)
+              return err;
           }
       }
+  return 0;
 }
 
 
@@ -425,7 +599,9 @@ ks_hkp_help (ctrl_t ctrl, parsed_uri_t uri)
    PORT.  Returns an allocated string or NULL on failure and sets
    ERRNO.  */
 static char *
-make_host_part (const char *scheme, const char *host, unsigned short port)
+make_host_part (ctrl_t ctrl,
+                const char *scheme, const char *host, unsigned short port,
+                int force_reselect)
 {
   char portstr[10];
   char *hostname;
@@ -449,7 +625,7 @@ make_host_part (const char *scheme, const char *host, unsigned short port)
       /*fixme_do_srv_lookup ()*/
     }
 
-  hostname = map_host (host);
+  hostname = map_host (ctrl, host, force_reselect);
   if (!hostname)
     return NULL;
 
@@ -459,6 +635,32 @@ make_host_part (const char *scheme, const char *host, unsigned short port)
 }
 
 
+/* Resolve all known keyserver names and update the hosttable.  This
+   is mainly useful for debugging because the resolving is anyway done
+   on demand.  */
+gpg_error_t
+ks_hkp_resolve (ctrl_t ctrl, parsed_uri_t uri)
+{
+  gpg_error_t err;
+  char *hostport = NULL;
+
+  hostport = make_host_part (ctrl, uri->scheme, uri->host, uri->port, 1);
+  if (!hostport)
+    {
+      err = gpg_error_from_syserror ();
+      err = ks_printf_help (ctrl, "%s://%s:%hu: resolve failed: %s",
+                            uri->scheme, uri->host, uri->port,
+                            gpg_strerror (err));
+    }
+  else
+    {
+      err = ks_printf_help (ctrl, "%s", hostport);
+      xfree (hostport);
+    }
+  return err;
+}
+
+
 /* Send an HTTP request.  On success returns an estream object at
    R_FP.  HOSTPORTSTR is only used for diagnostics.  If POST_CB is not
    NULL a post request is used and that callback is called to allow
@@ -578,6 +780,42 @@ send_request (ctrl_t ctrl, const char *request, const char *hostportstr,
 }
 
 
+/* Helper to evaluate the error code ERR form a send_request() call
+   with REQUEST.  The function returns true if the caller shall try
+   again.  TRIES_LEFT points to a variable to track the number of
+   retries; this function decrements it and won't return true if it is
+   down to zero. */
+static int
+handle_send_request_error (gpg_error_t err, const char *request,
+                           unsigned int *tries_left)
+{
+  int retry = 0;
+
+  switch (gpg_err_code (err))
+    {
+    case GPG_ERR_ECONNREFUSED:
+    case GPG_ERR_ENETUNREACH:
+      if (mark_host_dead (request) && *tries_left)
+        retry = 1;
+      break;
+
+    case GPG_ERR_ETIMEDOUT:
+      if (*tries_left)
+        {
+          log_info ("selecting a different host due to a timeout\n");
+          retry = 1;
+        }
+
+    default:
+      break;
+    }
+
+  if (*tries_left)
+    --*tries_left;
+
+  return retry;
+}
+
 static gpg_error_t
 armor_data (char **r_string, const void *data, size_t datalen)
 {
@@ -636,7 +874,6 @@ armor_data (char **r_string, const void *data, size_t datalen)
 }
 
 
-
 \f
 /* Search the keyserver identified by URI for keys matching PATTERN.
    On success R_FP has an open stream to read the data.  */
@@ -650,6 +887,8 @@ ks_hkp_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern,
   char *hostport = NULL;
   char *request = NULL;
   estream_t fp = NULL;
+  int reselect;
+  unsigned int tries = SEND_REQUEST_RETRIES;
 
   *r_fp = NULL;
 
@@ -691,10 +930,14 @@ ks_hkp_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern,
     }
 
   /* Build the request string.  */
+  reselect = 0;
+ again:
   {
     char *searchkey;
 
-    hostport = make_host_part (uri->scheme, uri->host, uri->port);
+    xfree (hostport);
+    hostport = make_host_part (ctrl, uri->scheme, uri->host, uri->port,
+                               reselect);
     if (!hostport)
       {
         err = gpg_error_from_syserror ();
@@ -708,6 +951,7 @@ ks_hkp_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern,
         goto leave;
       }
 
+    xfree (request);
     request = strconcat (hostport,
                          "/pks/lookup?op=index&options=mr&search=",
                          searchkey,
@@ -722,10 +966,15 @@ ks_hkp_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern,
 
   /* Send the request.  */
   err = send_request (ctrl, request, hostport, NULL, NULL, &fp);
+  if (handle_send_request_error (err, request, &tries))
+    {
+      reselect = 1;
+      goto again;
+    }
   if (err)
     goto leave;
 
-  /* Start reading the response.  */
+  /* Peek at the response.  */
   {
     int c = es_getc (fp);
     if (c == -1)
@@ -736,8 +985,8 @@ ks_hkp_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern,
       }
     if (c == '<')
       {
-        /* The document begins with a '<', assume it's a HTML
-           response, which we don't support.  */
+        /* The document begins with a '<': Assume a HTML response,
+           which we don't support.  */
         err = gpg_error (GPG_ERR_UNSUPPORTED_ENCODING);
         goto leave;
       }
@@ -768,6 +1017,8 @@ ks_hkp_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, estream_t *r_fp)
   char *hostport = NULL;
   char *request = NULL;
   estream_t fp = NULL;
+  int reselect;
+  unsigned int tries = SEND_REQUEST_RETRIES;
 
   *r_fp = NULL;
 
@@ -799,14 +1050,18 @@ ks_hkp_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, estream_t *r_fp)
       return gpg_error (GPG_ERR_INV_USER_ID);
     }
 
+  reselect = 0;
+ again:
   /* Build the request string.  */
-  hostport = make_host_part (uri->scheme, uri->host, uri->port);
+  xfree (hostport);
+  hostport = make_host_part (ctrl, uri->scheme, uri->host, uri->port, reselect);
   if (!hostport)
     {
       err = gpg_error_from_syserror ();
       goto leave;
     }
 
+  xfree (request);
   request = strconcat (hostport,
                        "/pks/lookup?op=get&options=mr&search=0x",
                        kidbuf,
@@ -819,6 +1074,11 @@ ks_hkp_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, estream_t *r_fp)
 
   /* Send the request.  */
   err = send_request (ctrl, request, hostport, NULL, NULL, &fp);
+  if (handle_send_request_error (err, request, &tries))
+    {
+      reselect = 1;
+      goto again;
+    }
   if (err)
     goto leave;
 
@@ -875,6 +1135,8 @@ ks_hkp_put (ctrl_t ctrl, parsed_uri_t uri, const void *data, size_t datalen)
   estream_t fp = NULL;
   struct put_post_parm_s parm;
   char *armored = NULL;
+  int reselect;
+  unsigned int tries = SEND_REQUEST_RETRIES;
 
   parm.datastring = NULL;
 
@@ -892,13 +1154,17 @@ ks_hkp_put (ctrl_t ctrl, parsed_uri_t uri, const void *data, size_t datalen)
   armored = NULL;
 
   /* Build the request string.  */
-  hostport = make_host_part (uri->scheme, uri->host, uri->port);
+  reselect = 0;
+ again:
+  xfree (hostport);
+  hostport = make_host_part (ctrl, uri->scheme, uri->host, uri->port, reselect);
   if (!hostport)
     {
       err = gpg_error_from_syserror ();
       goto leave;
     }
 
+  xfree (request);
   request = strconcat (hostport, "/pks/add", NULL);
   if (!request)
     {
@@ -908,6 +1174,11 @@ ks_hkp_put (ctrl_t ctrl, parsed_uri_t uri, const void *data, size_t datalen)
 
   /* Send the request.  */
   err = send_request (ctrl, request, hostport, put_post_cb, &parm, &fp);
+  if (handle_send_request_error (err, request, &tries))
+    {
+      reselect = 1;
+      goto again;
+    }
   if (err)
     goto leave;