gpgconf: Add command --query-swdb.
authorWerner Koch <wk@gnupg.org>
Wed, 2 Nov 2016 16:54:32 +0000 (17:54 +0100)
committerWerner Koch <wk@gnupg.org>
Wed, 2 Nov 2016 16:58:11 +0000 (17:58 +0100)
* tools/gpgconf.c (aQuerySWDB): New.
(opts): Add --query-swdb.
(valid_swdb_name_p): New.
(query_swdb): New.
(main): Implement command --query-swdb.
--

Right now this command is not very useful because dimngr has not yet
been changed to create the swdb.lst.  For manual tests the swdb.lst
file from the Net can be used with these additional lines:

  .filedate 20161102T130337
  .verified 20161102T150000

doc/tools.texi
tools/gpgconf.c

index 199b11e..2d2ccef 100644 (file)
@@ -250,6 +250,7 @@ throughout this section.
 * Listing options::        List all options of a component.
 * Changing options::       Changing options of a component.
 * Listing global options:: List all global options.
+* Querying versions::      Get and compare software versions.
 * Files used by gpgconf::  What files are used by gpgconf.
 @end menu
 
@@ -302,6 +303,13 @@ List the global configuration file in a colon separated format.  If
 Run a syntax check on the global configuration file.  If @var{filename}
 is given, check that file instead.
 
+
+@item --query-swdb @var{package_name} [@var{version_string}]
+Returns the current version for @var{package_name} and if
+@var{version_string} is given also an indicator on whether an update
+is available.
+
+
 @item --reload [@var{component}]
 @opindex reload
 Reload all or the given component. This is basically the same as sending
@@ -953,6 +961,80 @@ Unknown record types should be ignored.  Note that there is intentionally
 no feature to change the global option file through @command{gpgconf}.
 
 
+@node Querying versions
+@subsection Get and compare software versions.
+
+The GnuPG Project operates a server to query the current versions of
+software packages related to GnuPG.  @command{gpgconf} can be used to
+access this online database.  To allow for offline operations, this
+feature works by having @command{dirmngr} download a file from
+@code{https://versions.gnupg.org}, checking the signature of that file
+and storing the file in the GnuPG home directory.  If
+@command{gpgconf} is used and @command{dirmngr} is running, it may ask
+@command{dirmngr} to refresh that file before itself uses the file.
+
+The command @option{--query-swdb} returns information for the given
+package in a colon delimited format:
+
+@table @var
+
+@item name
+This is the name of the package as requested.  Note that "gnupg" is a
+special name which is replaced by the actual package implementing this
+version of GnuPG.  For this name it is also not required to specify a
+version because @command{gpgconf} takes its own version in this case.
+
+@item status
+The status of the software package according to this table:
+@table @code
+@item -
+No information available.  This is either because no current version
+has been specified or due to an error.
+@item ?
+The given name is not known in the online database.
+@item u
+An update of the software is available.
+@item c
+The specified version of the software is current.
+@item n
+The specified version is already newer than the released version.
+@end table
+
+@item urgency
+If the value (the empty string should be considered as zero) is
+greater than zero an important update is available.
+
+@item error
+This returns an @command{gpg-error} error code to distinguish between
+various failure modes.
+
+@item filedate
+This gives the date of the file with the version numbers in standard
+ISO format (@code{yyyymmddThhmmss}).  The date has been extracted by
+@command{dirmngr} from the signature of the file.
+
+@item verified
+This gives the date in ISO format the file was downloaded.  This value
+can be used to evaluate the freshness of the information.
+
+@item version
+This returns the version string for the requested software from the
+file.
+
+@item reldate
+This returns the release date in ISO format.
+
+@item size
+This returns the size of the package as decimal number of bytes.
+
+@item hash
+This returns a hexified SHA-2 hash of the package.
+
+@end table
+
+@noindent
+More fields may be added in future to the output.
+
 
 @mansect files
 @node Files used by gpgconf
@@ -965,6 +1047,12 @@ no feature to change the global option file through @command{gpgconf}.
   If this file exists, it is processed as a global configuration file.
   A commented example can be found in the @file{examples} directory of
   the distribution.
+
+@item @var{GNUPGHOME}/swdb.lst
+@cindex swdb.lst
+  A file with current software versions.  @command{dirmngr} creates
+  this file on demand from an online resource.
+
 @end table
 
 
index a1ca79f..1f00418 100644 (file)
@@ -1,5 +1,6 @@
 /* gpgconf.c - Configuration utility for GnuPG
  * Copyright (C) 2003, 2007, 2009, 2011 Free Software Foundation, Inc.
+ * Copyright (C) 2016 g10 Code GmbH.
  *
  * This file is part of GnuPG.
  *
@@ -52,6 +53,7 @@ enum cmd_and_opt_values
     aApplyDefaults,
     aListConfig,
     aCheckConfig,
+    aQuerySWDB,
     aListDirs,
     aLaunch,
     aKill,
@@ -79,6 +81,8 @@ static ARGPARSE_OPTS opts[] =
       N_("list global configuration file") },
     { aCheckConfig,   "check-config", 256,
       N_("check global configuration file") },
+    { aQuerySWDB,     "query-swdb", 256,
+      N_("query the software version database") },
     { aReload,        "reload", 256, N_("reload all or a given component")},
     { aLaunch,        "launch", 256, N_("launch a given component")},
     { aKill,          "kill", 256,   N_("kill a given component")},
@@ -203,6 +207,235 @@ list_dirs (estream_t fp, char **names)
 }
 
 
+
+/* Check whether NAME is valid argument for query_swdb().  Valid names
+ * start with a letter and contain only alphanumeric characters or an
+ * underscore.  */
+static int
+valid_swdb_name_p (const char *name)
+{
+  if (!name || !*name || !alphap (name))
+    return 0;
+
+  for (name++; *name; name++)
+    if (!alnump (name) && *name != '_')
+      return 0;
+
+  return 1;
+}
+
+
+/* Query the SWDB file.  If necessary and possible this functions asks
+ * the dirmngr to load an updated version of that file.  The caller
+ * needs to provide the NAME to query (e.g. "gnupg", "libgcrypt") and
+ * optional the currently installed version in CURRENT_VERSION.  The
+ * output written to OUT is a colon delimited line with these fields:
+ *
+ * name   :: The name of the package
+ * status :: This value tells the status of the software package
+ *           '-' :: No information available
+ *                  (error or CURRENT_VERSION not given)
+ *           '?' :: Unknown NAME
+ *           'u' :: Update available
+ *           'c' :: The version is Current
+ *           'n' :: The current version is already Newer than the
+ *                  available one.
+ * urgency :: If the value is greater than zero an urgent update is required.
+ * error   :: 0 on success or an gpg_err_code_t
+ *            Common codes seen:
+ *            GPG_ERR_TOO_OLD :: The SWDB file is to old to be used.
+ *            GPG_ERR_ENOENT  :: The SWDB file is not available.
+ *            GPG_ERR_BAD_SIGNATURE :: Currupted SWDB file.
+ * filedate:: Date of the swdb file (yyyymmddThhmmss)
+ * verified:: Date we checked the validity of the file (yyyyymmddThhmmss)
+ * version :: The version string from the swdb.
+ * reldate :: Release date of that version (yyyymmddThhmmss)
+ * size    :: Size of the package in bytes.
+ * hash    :: SHA-2 hash of the package.
+ *
+ */
+static void
+query_swdb (estream_t out, const char *name, const char *current_version)
+{
+  gpg_error_t err;
+  const char *search_name;
+  char *fname = NULL;
+  estream_t fp = NULL;
+  char *line = NULL;
+  char *self_version = NULL;
+  size_t length_of_line = 0;
+  size_t  maxlen;
+  ssize_t len;
+  char *fields[2];
+  char *p;
+  gnupg_isotime_t filedate = {0};
+  gnupg_isotime_t verified = {0};
+  char *value_ver = NULL;
+  gnupg_isotime_t value_date = {0};
+  char *value_size = NULL;
+  char *value_sha2 = NULL;
+  unsigned long value_size_ul;
+  int status, i;
+
+
+  if (!valid_swdb_name_p (name))
+    {
+      log_error ("error in package name '%s': %s\n",
+                 name, gpg_strerror (GPG_ERR_INV_NAME));
+      goto leave;
+    }
+  if (!strcmp (name, "gnupg"))
+    search_name = "gnupg21";
+  else if (!strcmp (name, "gnupg1"))
+    search_name = "gnupg1";
+  else
+    search_name = name;
+
+  if (!current_version && !strcmp (name, "gnupg"))
+    {
+      /* Use our own version but string a possible beta string.  */
+      self_version = xstrdup (PACKAGE_VERSION);
+      p = strchr (self_version, '-');
+      if (p)
+        *p = 0;
+      current_version = self_version;
+    }
+
+  if (current_version && compare_version_strings (current_version, NULL))
+    {
+      log_error ("error in version string '%s': %s\n",
+                 current_version, gpg_strerror (GPG_ERR_INV_ARG));
+      goto leave;
+    }
+
+  fname = make_filename (gnupg_homedir (), "swdb.lst", NULL);
+  fp = es_fopen (fname, "r");
+  if (!fp)
+    {
+      err = gpg_error_from_syserror ();
+      es_fprintf (out, "%s:-::%u:::::::\n", name, gpg_err_code (err));
+      if (gpg_err_code (err) != GPG_ERR_ENOENT)
+        log_error (_("error opening '%s': %s\n"), fname, gpg_strerror (err));
+      goto leave;
+    }
+
+  /* Note that the parser uses the first occurance of a matching
+   * values and ignores possible duplicated values.  */
+
+  maxlen = 2048; /* Set limit.  */
+  while ((len = es_read_line (fp, &line, &length_of_line, &maxlen)) > 0)
+    {
+      if (!maxlen)
+        {
+          err = gpg_error (GPG_ERR_LINE_TOO_LONG);
+          log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err));
+          goto leave;
+        }
+      /* Strip newline and carriage return, if present.  */
+      while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
+       line[--len] = '\0';
+
+      if (split_fields (line, fields, DIM (fields)) < DIM(fields))
+        continue; /* Skip empty lines and names w/o a value.  */
+      if (*fields[0] == '#')
+        continue; /* Skip comments.  */
+
+      /* Record the meta data.  */
+      if (!*filedate && !strcmp (fields[0], ".filedate"))
+        {
+          string2isotime (filedate, fields[1]);
+          continue;
+        }
+      if (!*verified && !strcmp (fields[0], ".verified"))
+        {
+          string2isotime (verified, fields[1]);
+          continue;
+        }
+
+      /* Tokenize the name.  */
+      p = strrchr (fields[0], '_');
+      if (!p)
+        continue; /* Name w/o an underscore.  */
+      *p++ = 0;
+
+      /* Wait for the requested name.  */
+      if (!strcmp (fields[0], search_name))
+        {
+          if (!strcmp (p, "ver") && !value_ver)
+            value_ver = xstrdup (fields[1]);
+          else if (!strcmp (p, "date") && !*value_date)
+            string2isotime (value_date, fields[1]);
+          else if (!strcmp (p, "size") && !value_size)
+            value_size = xstrdup (fields[1]);
+          else if (!strcmp (p, "sha2") && !value_sha2)
+            value_sha2 = xstrdup (fields[1]);
+        }
+    }
+  if (len < 0 || es_ferror (fp))
+    {
+      err = gpg_error_from_syserror ();
+      log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err));
+      goto leave;
+    }
+
+  if (!*filedate || !*verified)
+    {
+      err = gpg_error (GPG_ERR_INV_TIME);
+      es_fprintf (out, "%s:-::%u:::::::\n", name, gpg_err_code (err));
+      goto leave;
+    }
+
+  if (!value_ver)
+    {
+      es_fprintf (out, "%s:?:::::::::\n", name);
+      goto leave;
+    }
+
+  if (value_size)
+    {
+      gpg_err_set_errno (0);
+      value_size_ul = strtoul (value_size, &p, 10);
+      if (errno)
+        value_size_ul = 0;
+      else if (*p == 'k')
+        value_size_ul *= 1024;
+    }
+
+  err = 0;
+  status = '-';
+  if (compare_version_strings (value_ver, NULL))
+    err = gpg_error (GPG_ERR_INV_VALUE);
+  else if (!current_version)
+    ;
+  else if (!(i = compare_version_strings (value_ver, current_version)))
+    status = 'c';
+  else if (i > 0)
+    status = 'u';
+  else
+    status = 'n';
+
+  es_fprintf (out, "%s:%c::%d:%s:%s:%s:%s:%lu:%s:\n",
+              name,
+              status,
+              err,
+              filedate,
+              verified,
+              value_ver,
+              value_date,
+              value_size_ul,
+              value_sha2? value_sha2 : "");
+
+ leave:
+  xfree (value_ver);
+  xfree (value_size);
+  xfree (value_sha2);
+  xfree (line);
+  es_fclose (fp);
+  xfree (fname);
+  xfree (self_version);
+}
+
+
 /* gpgconf main. */
 int
 main (int argc, char **argv)
@@ -250,6 +483,7 @@ main (int argc, char **argv)
         case aApplyDefaults:
         case aListConfig:
         case aCheckConfig:
+        case aQuerySWDB:
         case aReload:
         case aLaunch:
         case aKill:
@@ -417,6 +651,18 @@ main (int argc, char **argv)
       list_dirs (outfp, argc? argv : NULL);
       break;
 
+    case aQuerySWDB:
+      /* Query the software version database.  */
+      if (!fname || argc > 2)
+       {
+         es_fprintf (es_stderr, "usage: %s --query-swdb NAME [VERSION]\n",
+                      GPGCONF_NAME);
+         exit (2);
+       }
+      get_outfp (&outfp);
+      query_swdb (outfp, fname, argc > 1? argv[1] : NULL);
+      break;
+
     case aCreateSocketDir:
       {
         char *socketdir;