Initial commit.
authorcrh <crh@ubiqx.org>
Wed, 6 Jun 2012 02:40:38 +0000 (21:40 -0500)
committercrh <crh@ubiqx.org>
Wed, 6 Jun 2012 02:40:38 +0000 (21:40 -0500)
 * PrequelD does not actually do anything yet.  It is missing the ability to parse incomming requests and respond to them.
 * The framework is all there, however.

src/daemon/PrequelD.c [new file with mode: 0644]

diff --git a/src/daemon/PrequelD.c b/src/daemon/PrequelD.c
new file mode 100644 (file)
index 0000000..2254403
--- /dev/null
@@ -0,0 +1,1019 @@
+/* ========================================================================== **
+ *                                 PrequelD.c
+ *
+ * Copyright:
+ *  Copyright (C) 2012 by Christopher R. Hertel
+ *
+ * Email: crh@ubiqx.mn.org
+ *
+ * $Id: PrequelD.c 2012-06-05 21:40:38 -0500 crh$
+ *
+ * -------------------------------------------------------------------------- **
+ *
+ * Description:
+ *  PeerDist protocol server daemon.
+ *
+ * -------------------------------------------------------------------------- **
+ *
+ * License:
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * -------------------------------------------------------------------------- **
+ *               This code was developed in participation with
+ *                the Protocol Freedom Information Foundation.
+ *         See http://www.protocolfreedom.org/ for more information.
+ * -------------------------------------------------------------------------- **
+ *
+ * Notes:
+ *
+ *  The Prequel Project website is http://www.ubiqx.org/proj/Prequel/
+ *
+ * Terminology:
+ *
+ *  PeerDist  - The protocol and/or encoding format used by Microsoft's
+ *              BranchCacheTM distributed caching system.
+ *
+ *  Prequel   - The name of an Open Source project aimed at implementing
+ *              PeerDist for use with Linux, *BSD, and other Unix-like OSes.
+ *
+ * References:
+ *  [BCOVIEW]:    MS W2K8R2 BranchCache Overview
+ *                http://technet.microsoft.com/en-us/library/dd637832.aspx
+ *
+ *  [MS-CCRSOD]:  Content Caching and Retrieval System Overview
+ *                http://msdn.microsoft.com/en-us/library/ff632262.aspx
+ *
+ *  [MS-PCCRC]:   Peer Content Caching and Retrieval: Content Identification
+ *                http://msdn.microsoft.com/en-us/library/dd303704.aspx
+ *
+ * To Compile:
+ *  cc -I.. -o PrequelD PrequelD.c PD_peerdist1.c PD_read_config.c \
+ *    PD_sha2_oSSL.c PD_utils.c ../ubi_sLinkList.c -lcrypto -lpthread
+ *
+ * ========================================================================== **
+ */
+
+#include <time.h>           /* For nanosleep(2).                        */
+#include <stdlib.h>         /* Standard Library.                        */
+#include <stdbool.h>        /* Standard boolean type.                   */
+#include <stdint.h>         /* Standard extended integer types.         */
+#include <unistd.h>         /* For getopt(3), close(2), unlink(2), etc. */
+#include <string.h>         /* For strerror(3), strchr(3).              */
+#include <ctype.h>          /* For isspace(3).                          */
+#include <sys/types.h>      /* For the pid_t and DIR* types.            */
+#include <dirent.h>         /* For reading directory entries.           */
+#include <pthread.h>        /* For POSIX Threads.                       */
+#include <poll.h>           /* For ppoll(2) and struct pollfd.          */
+#include <sys/socket.h>     /* Socket(7) interface header.              */
+#include <sys/un.h>         /* Unix(7) domain sockets.                  */
+
+#include "PD_peerdist1.h"   /* PeerDist v1 protocol support.  */
+#include "PD_read_config.h" /* Read the configuration file.   */
+#include "PD_utils.h"       /* General PrequelD utilities.    */
+#include "ubi_sLinkList.h"  /* Linked List module.            */
+
+
+/* -------------------------------------------------------------------------- **
+ * Defined Constants:
+ *
+ *  DEFAULT_CONFIG_FILE - Default pathname of the configuration file to read
+ *                        on startup.  This value can be overridden at
+ *                        compile time.
+ */
+
+#ifndef DEFAULT_CONFIG_FILE
+#define DEFAULT_CONFIG_FILE "/etc/prequeld.conf"
+#endif
+
+
+/* -------------------------------------------------------------------------- **
+ * Macros:
+ *
+ *  forEach( V, L ) - A macro for traversing a linked list of <pd_ConfigRec>
+ *                    records.
+ *                    V - The variable that will point to each node in the
+ *                        list.  V must be substituted by a pointer to a
+ *                        <pd_ConfigRec>.
+ *                    L - The list to be traversed.
+ */
+
+#define forEach( V, L ) \
+        for( (V) = (pd_ConfigRec *)ubi_slFirst( (L) ); \
+             NULL != (V); \
+             (V) = (pd_ConfigRec *)ubi_slNext( (V) ) )
+
+
+/* -------------------------------------------------------------------------- **
+ * Typedefs:
+ *
+ *  WorkItem  - A queue entry with a pointer to a string that is the path to
+ *              a source file that is in need of hashing.
+ */
+
+typedef struct
+  {
+  ubi_slNode  node;
+  char       *srcPath;
+  } WorkItem;
+
+
+/* -------------------------------------------------------------------------- **
+ * Global Constants:
+ *
+ *  Copyright   - Copyright information.
+ *  License     - License information string.
+ *  Revision    - Revision string, generated by revision control.
+ *  Id          - Longer revision string.
+ *
+ *  HelpMsg     - The program help message.
+ *  VerboseMsg  - Verbose help message.
+ */
+
+static const char *Copyright =
+                    "Copyright (c) 2012 by Christopher R. Hertel";
+static const char *License =
+                    "License: GNU General Public License version 3 (GPLv3)";
+static const char *Revision =
+                    "$Revision$";
+static const char *Id =
+                    "$Id: PrequelD.c 2012-06-05 21:40:38 -0500 crh$";
+
+static const char *HelpMsg[] =
+  {
+  "  Available Options:",
+  "  -c <file> Specify the configuration file to use.",
+  "  -f        Run in the foreground, not as a daemon.",
+  "  -h        Produce this useful help message, then exit (-hv == more help).",
+  "  -l <file> Send log messages to <file>.",
+  "  -q        Be quiet.  Set the verbosity level to zero.",
+  "  -t        Test-parse the configuration file, then exit.",
+  "  -v        Be verbose.  Add more -v's for more verbosity.",
+  "  -V        Output version information.",
+  NULL
+  };
+
+static const char *VerboseMsg[] =
+  {
+  "The Options Available to You:",
+  "  -c <file> Specify the configuration file to use.",
+  "            Default: \"" DEFAULT_CONFIG_FILE "\".",
+  "  -f        Run in the foreground, not as a daemon.",
+  "            Default: Run as a daemon.",
+  "  -h        Produce a useful help message, then exit.",
+  "            Default: Don't produce a useful help message.  Run normally.",
+  "  -l <file> Send log messages to <file>.",
+  "            Default:  When running in the foreground, log messages are sent",
+  "                      to <stderr>.  When running in the background, log",
+  "                      messages are sent to syslog.  All initial messages",
+  "                      (as the program starts up) are sent to <stdout> or",
+  "                      <stderr>.",
+  "  -q        Be quiet.  Set the verbosity level to zero (0) and produce only",
+  "            the minimum set of diagnostic messages.  Default verbosity: 1.",
+  "  -t        Parse the configuration file and report any errors to <stderr>.",
+  "            The program will exit once parsing is complete.",
+  "            Use -tv to dump the contents of a successfully parsed",
+  "            configuration file.",
+  "  -v        Be verbose.  Add more -v's for more verbosity.",
+  "            Use of -q or -v overrides the config file verbosity settings.",
+  "            Default verbosity: 1.",
+  "  -V        Output version information.",
+  "            Increased verbosity provides copyright and license information.",
+  NULL
+  };
+
+
+/* -------------------------------------------------------------------------- **
+ * Global Variables:
+ *
+ *  Verbosity     - Controls the level of diagnostic messages that are
+ *                  produced.  The minimum value is 0.  Anything above 10
+ *                  is ridiculous.  Default: 1.  A maximum value of 255
+ *                  is enforced both in the PD_read_config module and in
+ *                  <ReadOpts()> function, below.
+ *
+ *  Daemonize     - By default PrequelD will run as a daemon, but this
+ *                  default behavior can be overridden with a command-line
+ *                  option.  There is no configuration file option for this,
+ *                  so we use a global variable to represent this attribute.
+ *
+ *  Cfg           - Pointer used to retrieve the configuration information
+ *                  from the configuration file.
+ *
+ *  WorkerThread  - The thread ID of the background thread that is doing all
+ *                  of the real work.
+ *
+ *  GlobalMutex   - A global mutex attribute.  The worker thread needs brief
+ *                  access to the <Cfg> list and other state attribues.
+ *                  This allows the foreground and background to share.
+ *
+ *  Shutdown      - Used to let the worker process know when it is time to
+ *                  shut down and return.
+ *
+ *  WorkQueue     - A queue of source file names that are waiting to be
+ *                  hashed.  The queue is filled when a request is made
+ *                  and no hash cache file is found.  It is emptied by
+ *                  the worker thread.  Queued entries have priority over
+ *                  source files found during directory traversal.
+ */
+
+static unsigned int     Verbosity     = 1;
+static bool             Daemonize     = true;
+static pd_Config       *Cfg           = NULL;
+static pthread_t        WorkerThread;
+static pthread_mutex_t  GlobalMutex   = PTHREAD_MUTEX_INITIALIZER;
+static bool             Shutdown      = false;
+static ubi_slList       WorkQueue[1];
+
+/* -------------------------------------------------------------------------- **
+ * Static Functions:
+ */
+
+static void Usage( const char *progname, int status )
+  /* ------------------------------------------------------------------------ **
+   * Provide usage information, then exit.
+   *
+   *  Input:  progname  - Pointer to a string containing the program name.
+   *          status    - Exit status to return.
+   *
+   *  Output: <none>
+   *
+   *  Notes:  Output is sent to <stdout> so that it can be piped through
+   *          'more' or 'less' or something else.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  int          i;
+  const char **msg = (Verbosity > 1) ? VerboseMsg : HelpMsg;
+
+  Say( "Usage: %s [options]\n", progname );
+  for( i = 0; NULL != msg[i]; i++ )
+    Say( "%s\n", msg[i] );
+
+  exit( status );
+  } /* Usage */
+
+
+static void *Responder( void *thingy )
+  /* ------------------------------------------------------------------------ **
+   * Respond to a request from a client.
+   *
+   *  Input:  thingy  - A pointer to an integer, which is the open
+   *                    communications socket.
+   *
+   *  Output: <none>
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  int client_sock = *((int *)thingy);
+
+  (void)pthread_detach( pthread_self() );
+
+  /* FIX:  This is for testing... */
+  (void)write( client_sock, "Response.\n", 10 );
+
+  (void)close( client_sock );
+  return( NULL );
+  } /* Responder */
+
+
+static void Listener( const int lsock )
+  /* ------------------------------------------------------------------------ **
+   * Listen for incomming connection requests and spawn responder threads.
+   *
+   *  Input:  lsock - Listening socket.
+   *
+   *  Output: <none>
+   *
+   *  Notes:  This function is run within the primary thread, but it is
+   *          started after the worker thread has been spun off so it
+   *          must lock the global mutex before accessing any commonly
+   *          used objects.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  int       newSock;
+  int       result;
+  pthread_t threadhandle;
+
+  while( (newSock = accept( lsock, NULL, NULL )) <= 0 )
+    {
+    int tmpInt = newSock;
+
+    result = pthread_create( &threadhandle, NULL, &Responder,
+                             (void *)&tmpInt );
+    if( result )
+      {
+      if( 0 == pthread_mutex_lock( &GlobalMutex ) )
+        {
+        Log( "Warning: Error creating responder subthread; %s.\n",
+             strerror( result ) );
+        (void)pthread_mutex_unlock( &GlobalMutex );
+        }
+      (void)close( newSock );
+      }
+    }
+
+  if( 0 == pthread_mutex_lock( &GlobalMutex ) )
+    {
+    Log( "Failure: Error accepting socket connection; %s.\n", ErrStr );
+    (void)pthread_mutex_unlock( &GlobalMutex );
+    }
+  close( lsock );
+  } /* Listener */
+
+
+static void WorkHash( const char *srcPathName )
+  /* ------------------------------------------------------------------------ **
+   * Create a hash cache file given a source file name.
+   *
+   *  Input:  srcPathName - The pathname of the source file to be hashed.
+   *
+   *  Output: <none>
+   *
+   *  Notes:  Dancing with mutex.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  pd_ConfigRec  *entry;
+  char          *cacheDir;
+  int            minBlocks;
+  pd_v1HashType  hashType;
+  unsigned char  key[PD_V1_KEY_SIZE];
+  int            result;
+  int            syserr;
+
+  /* Try to grab the mutex.
+   *  If we can't grab the mutex, then something's wrong but we cannot log it
+   *  because we need the mutex in order to (safely) write to the log.
+   */
+  if( (result = pthread_mutex_lock( &GlobalMutex )) )
+    return;
+
+  /* We have mutex.
+   *  Find the config record that points to the source directory tree in
+   *  which the source file resides.
+   */
+  entry = pd_FindSrcDir( Cfg, srcPathName );
+  if( NULL == entry )
+    {
+    if( Verbosity > 2 )
+      Log( "Requested file is out of scope: %s\n", srcPathName );
+    (void)pthread_mutex_unlock( &GlobalMutex );
+    return;
+    }
+
+  /* We have mutex and a configuration entry.
+   *  Copy all of the config values so that we can release the mutex.
+   */
+  if( entry->cacheDir )
+    {
+    cacheDir  = strdup( entry->cacheDir );
+    minBlocks = entry->minBlocks;
+    hashType  = entry->v1HashType;
+    syserr    = 0;
+    (void)memcpy( key, entry->userParam, PD_V1_KEY_SIZE );
+    (void)pthread_mutex_unlock( &GlobalMutex );
+    }
+  else
+    {
+    /* This should be impossible.  Check PD_read_config if this happens.  */
+    Log( "Bad configuration entry; cacheDir is NULL.\n" );
+    (void)pthread_mutex_unlock( &GlobalMutex );
+    return;
+    }
+
+  /* Check the <cacheDir> copy. */
+  if( NULL == cacheDir )
+    {
+    if( Verbosity )
+      Log( "Warning: Memory allocation failure in WorkHash().\n" );
+    return;
+    }
+
+  /* This step may take a while.
+   *  Which is why we don't want to be holding the mutex now.
+   */
+  result = pd_v1CreateHashCache( srcPathName,
+                                 cacheDir,
+                                 minBlocks,
+                                 hashType,
+                                 key,
+                                 &syserr );
+  /* Clean up.  */
+  free( cacheDir );
+
+  /* Report the results.
+   *  It is not a fatal error to fail to create the cache file, but it should
+   *  probably be logged.  Logging requires the mutex.
+   */
+  if( Verbosity > 0 )
+    {
+    if( pthread_mutex_lock( &GlobalMutex ) )
+      return;   /* Error getting the mutex... can't log without it. */
+
+    if( pd_Success == result )
+      {
+      if( Verbosity > 2 )
+        Log( "File \"%s\" hashed.\n", srcPathName );
+      }
+    else
+      {
+      if( syserr )
+        Log( "Failure hashing \"%s\"; %s; %s.\n", srcPathName,
+             pd_v1StrResult( result ), strerror( syserr ) );
+      else
+        Log( "Failure hashing \"%s\"; %s.\n", srcPathName,
+             pd_v1StrResult( result ) );
+      }
+
+    (void)pthread_mutex_unlock( &GlobalMutex );
+    }
+  } /* WorkHash */
+
+
+static void *WorkerMain( void *thingy )
+  /* ------------------------------------------------------------------------ **
+   * Worker thread mainline.
+   *
+   *  Input:  thingy  - Pointer to an opaque blob that currently isn't used.
+   *
+   *  Output: Currently always returns NULL.
+   *
+   *  Notes:  This code runs as a background thread.  It's job is to generate
+   *          the hash cache files.
+   *
+   *  FIX:    The wDir state is designed to allow the worker thread to
+   *          traverse configured source directories and hash files that
+   *          have not been requested yet.  The requested files, listed
+   *          in the <WorkQueue> have higher priority.  Unfortunately,
+   *          directory traversal has not been implemented yet.
+   *
+   *          In addition, we should traverse the cache directories and
+   *          purge cache files that are out of date or for which the
+   *          source file no longer exists.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  enum { wNone, wStop, wQueue, wDir } doState;          /* What shall we do?  */
+  struct timespec ts = { 0, 500000000 };                /* Half a second.     */
+  char           *srcPathName = NULL;
+  WorkItem       *srcEntry;
+
+  do
+    {
+    /* Start by waiting a half second, just to pace ourselves.  */
+    (void)nanosleep( &ts, NULL );
+
+    /* Determine which action to take, and collect any required data.
+     *  This requires locking/unlocking the global mutex.
+     */
+    doState = wNone;      /* Default action.  */
+    if( 0 == pthread_mutex_lock( &GlobalMutex ) )
+      {
+      if( Shutdown )
+        doState = wStop;
+      else
+        {
+        if( 0 == ubi_slCount( WorkQueue ) )
+          doState = wDir;
+        else
+          {
+          srcEntry    = (WorkItem *)ubi_slDequeue( WorkQueue );
+          if( NULL != srcEntry )
+            {
+            srcPathName = srcEntry->srcPath;
+            free( srcEntry );           /* Free the node, but not the string. */
+            doState = wQueue;
+            }
+          }
+        }
+      (void)pthread_mutex_unlock( &GlobalMutex );
+      }
+
+    /* Now do the thing.  */
+    switch( doState )
+      {
+      case wQueue:
+        /* Hash an entry from the queue.  */
+        if( NULL != srcPathName )
+          {
+          WorkHash( srcPathName );
+          free( srcPathName );
+          }
+        break;
+      case wDir:
+        /* Hash an entry from a source directory. */
+        /* FIX: Need directory traversal. */
+        break;
+      default:
+        break;
+      }
+
+    } while( wStop != doState );
+
+  return( NULL );
+  } /* WorkerMain */
+
+
+static void StartWorker( void )
+  /* ------------------------------------------------------------------------ **
+   * Start a background thread to generate hash cache files.
+   *
+   *  Input:  <none>
+   *  Output: <none>
+   *
+   *  Notes:  The worker thread does the work of creating hash cache files
+   *          from source content.  It runs separately so that it does not
+   *          interfere with communications with our clients.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  int            result;
+  pthread_attr_t attr[1];
+
+  /* Set up the attributes to make the thread joinable, then start it.  */
+  if( (result = pthread_attr_init( attr )) )
+    LogX( EXIT_FAILURE, "Failure initializing worker thread attributes; %s.\n",
+          strerror( result ) );
+
+  if( (result = pthread_attr_setdetachstate( attr, PTHREAD_CREATE_JOINABLE )) )
+    LogX( EXIT_FAILURE,
+          "Failure setting \"joinable\" attribute for worker thread; %s.\n",
+          strerror( result ) );
+
+  if( (result = pthread_create( &WorkerThread, attr, WorkerMain, NULL )) )
+    LogX( EXIT_FAILURE, "Failure spawning worker thread; %s.\n",
+          strerror( result )  );
+  } /* StartWorker */
+
+
+static int OpenListenSock( const char *sockpath )
+  /* ------------------------------------------------------------------------ **
+   * Open a Unix Domain socket and set it to listen for connection requests.
+   *
+   *  Input:  sockpath  - The pathname of the Unix Domain socket to be
+   *                      opened for listening.
+   *
+   *  Output: The opened and listening socket.
+   *          Only a valid value will be returned.  This function will exit
+   *          the program on error.
+   *
+   *  Notes:  We call this prior to spinning off any threads, so we can avoid
+   *          locking the global mutex.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  struct sockaddr_un laddr[1];
+  int                lsock;
+
+  /* Sanity check.
+   * We must be able to fit the socket path into a Unix Domain socket address.
+   */
+  if( strlen( sockpath ) >= sizeof( laddr->sun_path ) )
+    {
+    LogX( EXIT_FAILURE,
+          "Annoyance: Path '%s' exceeds maximum unix domain address length.\n",
+          sockpath );
+    }
+
+  /* Create a unix domain socket. */
+  lsock = socket( AF_UNIX, SOCK_STREAM, 0 );
+  if( lsock < 0 )
+    {
+    LogX( EXIT_FAILURE,
+          "Failure: Unable to create unix domain socket; %s.\n", ErrStr );
+    }
+
+  /* Bind the socket to the name within the namespace. */
+  (void)unlink( sockpath );
+  (void)memset( laddr, 0, sizeof( struct sockaddr_un ) );
+  laddr->sun_family = AF_UNIX;
+  (void)strcpy( laddr->sun_path, sockpath );
+  if( bind( lsock, (struct sockaddr *)laddr, sizeof( laddr ) ) < 0 )
+    {
+    (void)close( lsock );
+    LogX( EXIT_FAILURE,
+          "Failure: Socket bind() to '%s' failed; %s.\n", sockpath, ErrStr );
+    }
+
+  /* Set the socket to listen. */
+  if( listen( lsock, 32 ) < 0 )
+    {
+    (void)close( lsock );
+    (void)unlink( sockpath );
+    LogX( EXIT_FAILURE,
+          "Failure: Cannot listen on unix domain socket '%s'; %s.\n",
+          sockpath, ErrStr );
+    }
+
+  return( lsock );
+  } /* OpenListenSock */
+
+
+static void Spawn( void )
+  /* ------------------------------------------------------------------------ **
+   * Spawn a daemon process, which will take over control for us.
+   *
+   *  Input:  <none>
+   *  Output: <none>
+   *
+   *  Notes:  If the child process is created successfully, the parent
+   *          process is terminated using _exit(2).  This bypasses executing
+   *          any functions registered with atexit(3) or on_exit(3).  We
+   *          don't have any such functions but, even so, this is the normal
+   *          way to exit the parent process.
+   *          See: http://www.steve.org.uk/Reference/Unix/faq_2.html#SEC6
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  pid_t pid = fork();
+
+  if( pid < 0 )   /* Error. */
+    LogX( EXIT_FAILURE, "Cannot spawn child process; %s\n", ErrStr );
+
+  if( pid > 0 )   /* Parent process. */
+    _exit( EXIT_SUCCESS );
+
+  /* Child process.
+   */
+  if( setsid() < 0 )
+    LogX( EXIT_FAILURE, "Failure: setsid(2) failed in Spawn(); %s.\n", ErrStr );
+  if( Verbosity )
+    Log( "Process %d spawned.\n", getpid() );
+  /* Close standard file descriptors. */
+  close(  STDIN_FILENO );
+  close( STDOUT_FILENO );
+  close( STDERR_FILENO );
+  return;
+  } /* Spawn */
+
+
+static void ReadKeys( void )
+  /* ------------------------------------------------------------------------ **
+   * Read and store the server secret signing keys from the key files.
+   *
+   *  Input:  <none>
+   *  Output: <none>
+   *
+   *  Notes:  The signing key is officially known as the "Server Secret".
+   *          See the definition of Server Secret in [MS-PCCRC], section 1.1.
+   *
+   *          The "Server Secret" is the SHA-256 hash of "an arbitrary length
+   *          binary string stored on the server”.  [MS-PCCRC] does not give
+   *          a name to this arbitrary binary string.  It is referred to as
+   *          the "Server Passphrase" in Prequel documentation.
+   *
+   *          This function makes one or more passes through the sourcedir
+   *          configuration list.  Each time, it finds the first entry
+   *          that has a NULL <userParam> pointer.  It attempts to read the
+   *          signing key from the keyfile.  If it succeeds, it goes
+   *          through the rest of the list looking for matching keyfile
+   *          names.  If it finds a match, it points the <userParam> of
+   *          the matching entry to the already-copied key.
+   *
+   *          The userParam pointer could be used for storing a more complex
+   *          structure or other extended information.  Right now, it's only
+   *          used for storing the signing key.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  FILE          *keyF;
+  pd_ConfigRec  *entry;
+  char          *kfName;
+  unsigned char *ss = NULL;
+  size_t         result;
+
+  do
+    {
+    kfName = NULL;
+    forEach( entry, Cfg )   /* Read ConfigRec entries sequentially. */
+      {
+      if( NULL == entry->userParam )
+        {
+        if( NULL == kfName )
+          {
+          kfName = entry->keyFileName;
+          if( NULL == (ss = (unsigned char *)malloc( PD_V1_KEY_SIZE )) )
+            LogX( EXIT_FAILURE,
+                  "Failure: malloc(3) failed in ReadKeys(); %s.\n", ErrStr );
+          if( NULL == (keyF = fopen( kfName, "r" )) )
+            LogX( EXIT_FAILURE,
+                  "Failure: Could not open keyfile %s; %s.\n", kfName, ErrStr );
+          result = fread( ss, 1, PD_V1_KEY_SIZE, keyF );
+          if( PD_V1_KEY_SIZE != result )
+            LogX( EXIT_FAILURE,
+                  "Failure: Only %d bytes in keyfile %s.\n", result, kfName );
+          (void)fclose( keyF );
+          entry->userParam = ss;
+          }
+        else
+          {
+          if( 0 == strcmp( kfName, entry->keyFileName ) )
+            entry->userParam = ss;
+          }
+        }
+      }
+    } while( NULL != kfName );
+
+  } /* ReadKeys */
+
+
+static void ReadConfig( char *fname )
+  /* ------------------------------------------------------------------------ **
+   * Open, read, and interpret the configuration file.
+   *
+   *  Input:  fname - The name of the file to open, or NULL.
+   *                  If <fname> is NULL, then the default file name will
+   *                  be used.
+   *
+   *  Output: <none>
+   *
+   *  Notes:  The configuration file format was inspired, somewhat, by the
+   *          format of the ISC dhcpd.conf file.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  FILE *configF;
+
+  /* Ensure that we have a configurage file pathname,
+   * then attempt to open the file for reading.
+   */
+  if( NULL == fname )
+    fname = DEFAULT_CONFIG_FILE;
+  if( NULL == (configF = fopen( fname, "r" )) )
+    Fail( "Unable to open configuration file \"%s\" for input; %s.\n",
+          fname, ErrStr );
+
+  /* Parse it.
+   *  <Cfg> is a global pointer.
+   */
+  Cfg = pd_ParseCfg( configF );
+
+  /* Close it.
+   */
+  fclose( configF );
+  } /* ReadConfig */
+
+
+static void ReadOpts( int argc, char *argv[] )
+  /* ------------------------------------------------------------------------ **
+   * Interpret any command-line options, and the configuration file.
+   *
+   *  Input:  argc  - Count of arguments.
+   *          argv  - Array of pointers to argument strings.
+   *
+   *  Output: <none>
+   *
+   *  Notes:  This function also calls <ReadConfig()>, which reads and
+   *          interprets the configuration file.
+   *
+   *          This is quite a workhorse function.  It handles collecting
+   *          and validating the configuration of the daemon.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  int           c;
+  FILE         *LogF;
+  extern char  *optarg;
+  extern int    optind;
+  char         *ConfigFile  = NULL;
+  char         *LogFile     = NULL;
+  bool          VerbSet     = false;
+  bool          SpewHelp    = false;
+  bool          SpewVersion = false;
+  bool          TestConfig  = false;
+
+  /* Read the arguments.
+   */
+  while( (c = getopt( argc, argv, "c:fhl:qtvV" )) >= 0 )
+    {
+    switch( c )
+      {
+      case 'c':
+        ConfigFile  = optarg;
+        break;
+      case 'f':
+        Daemonize   = false;
+        break;
+      case 'h':
+        SpewHelp    = true;
+        break;
+      case 'l':
+        LogFile     = optarg;
+        break;
+      case 'q':
+        Verbosity   = 0;
+        VerbSet     = true;
+        break;
+      case 't':
+        TestConfig  = true;
+        break;
+      case 'v':
+        if( Verbosity < 0xFF )
+          Verbosity++;
+        VerbSet     = true;
+        break;
+      case 'V':
+        SpewVersion = true;
+        break;
+      default:
+        /* Unknown option.  Provide simple help, then exit.
+         */
+        Verbosity = 0;
+        Usage( argv[0], EXIT_FAILURE );
+        break;
+      }
+    }
+
+  /* Provide version information (and more) if it was requested.
+   */
+  if( SpewVersion )
+    {
+    switch( Verbosity )
+      {
+      case 0:
+        Say( "%s\n", Revision );
+        break;
+      case 1:
+        Say( "%s\n", Id );
+        break;
+      case 2:
+        Say( "%s\n", Id );
+        Say( "%s\n", License );
+        break;
+      default:
+        Say( "%s\n", Id );
+        Say( "%s\n", Copyright );
+        Say( "%s\n", License );
+        Say( "Developed in participation with the " );
+        Say( "Protocol Freedom Information Foundation.\n" );
+        break;
+      }
+    if( SpewHelp && Verbosity )
+      Say( "\n" );
+    }
+
+  /* If help was requested, send help.
+   */
+  if( SpewHelp )
+    {
+    Usage( argv[0], EXIT_SUCCESS );
+    }
+
+  /* If the user wants a configuration file test, provide it,
+   *  then exit the program.
+   */
+  if( TestConfig )
+    {
+    (void)setLogFile( stderr );
+    if( NULL == ConfigFile )
+      ConfigFile = DEFAULT_CONFIG_FILE;
+    ReadConfig( ConfigFile );
+    if( (1 == Verbosity) && (NULL != Cfg) )
+      Log( "%s: successfully parsed.\n", ConfigFile );
+    if( 1 < Verbosity )
+      pd_DumpCfg( Cfg );
+    exit( EXIT_SUCCESS );
+    }
+
+  /* Enable logging.
+   *  If a log file name was specified, then all messages from this point
+   *  on will be directed to the log file.
+   *  If no log file was specified, and we will run as a daemon, then we
+   *  start logging to syslog().
+   *  Otherwise, we log to <stderr> unless/until a log file name is given
+   *  in the configuration file.
+   */
+  if( NULL == LogFile )
+    {
+    if( Daemonize )
+      OpenSyslog( "prequeld" );
+    else
+      (void)setLogFile( stderr );
+    }
+  else
+    {
+    LogF = fopen( LogFile, "a" );
+    if( NULL == LogF )
+      Fail( "Unable to open log file \"%s\" for output; %s.\n",
+            LogFile, ErrStr );
+    (void)setLogFile( LogF );
+    }
+
+  /* Open and read the configuration file.
+   *  Command-line options override configfile options.  This is particularly
+   *  tricky when dealing with logging, which has already started.
+   */
+  ReadConfig( ConfigFile );
+  if( NULL == Cfg )         /* Parsing returned no configuration.  Exit.  */
+    LogX( EXIT_FAILURE, "Null configuration.  Exiting.\n" );
+
+  if( NULL == LogFile )
+    {
+    /* We have already started logging to syslog (background) or stderr
+     * (foreground).  If a log file was specified in the config file, and
+     * if we are not running in the foreground, use the specified config
+     * file.
+     */
+    if( (NULL != Cfg->logFileName) && Daemonize )
+      {
+      LogF = fopen( Cfg->logFileName, "a" );
+      if( NULL == LogF )
+        Fail( "Unable to open log file \"%s\" for output; %s.\n",
+              Cfg->logFileName, ErrStr );
+      (void)setLogFile( LogF );
+      }
+    }
+  else
+    {
+    /* If we have a command-line specified logfile, store it in the Cfg
+     *  structure for later use.
+     */
+    Cfg->logFileName = replaceStr( LogFile, Cfg->logFileName );
+    }
+
+  /* User-set verbosity overrides config file.  */
+  if( VerbSet )
+    {
+    pd_ConfigRec *entry;
+
+    forEach( entry, Cfg )
+      entry->verbosity = (0xFF & Verbosity);
+    }
+  } /* ReadOpts */
+
+
+/* -------------------------------------------------------------------------- **
+ * Mainline:
+ */
+
+int main( int argc, char *argv[] )
+  /* ------------------------------------------------------------------------ **
+   * Program mainline.
+   *
+   *  Input:  argc  - You know what this is.
+   *          argv  - You know what to do.
+   *
+   *  Output: EXIT_SUCCESS or, on failure, EXIT_FAILURE.
+   *
+   * ------------------------------------------------------------------------ **
+   */
+  {
+  int listenSock;
+
+  /* Gather working parameters.
+   *  Note that ReadOpts() takes care of reading the configuration file.
+   */
+  ReadOpts( argc, argv );         /* Get all set up to run. */
+  ReadKeys();                     /* Find the signing keys. */
+
+  /* Become a daemon, unless we were told otherwise.  */
+  if( Daemonize )
+    Spawn();
+
+  /* Before creating any subthreads, let's try to open the listening socket.  */
+  listenSock = OpenListenSock( Cfg->sockFileName );
+
+  /* Initialize the priority queue and start the worker thread. */
+  (void)ubi_slInitList( WorkQueue );
+  StartWorker();
+
+  /* Listen for incoming requests on the named Unix Domain socket.  */
+  Listener( listenSock );
+
+  /* Shut down the worker thread. */
+  (void)pthread_mutex_lock( &GlobalMutex );
+  Shutdown = true;
+  (void)pthread_mutex_unlock( &GlobalMutex );
+  (void)pthread_join( WorkerThread, NULL );
+
+  /* Clean up and exit. */
+  if( Verbosity )
+    Log( "Normal exit.\n" );
+  return( EXIT_SUCCESS );
+  } /* main */
+
+/* ========================================================================== */