/* call-dirmngr.c - communication with the dromngr 
 *	Copyright (C) 2002, 2003 Free Software Foundation, Inc.
 *
 * This file is part of GnuPG.
 *
 * GnuPG 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 2 of the License, or
 * (at your option) any later version.
 *
 * GnuPG 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
 */

#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h> 
#include <time.h>
#include <assert.h>
#include <ctype.h>

#include "gpgsm.h"
#include <gcrypt.h>
#include <assuan.h>

#include "i18n.h"

struct membuf {
  size_t len;
  size_t size;
  char *buf;
  int out_of_core;
};



static ASSUAN_CONTEXT dirmngr_ctx = NULL;
static int force_pipe_server = 0;

struct inq_certificate_parm_s {
  ASSUAN_CONTEXT ctx;
  KsbaCert cert;
};

struct lookup_parm_s {
  CTRL ctrl;
  ASSUAN_CONTEXT ctx;
  void (*cb)(void *, KsbaCert);
  void *cb_value;
  struct membuf data;
  int error;
};

struct run_command_parm_s {
  ASSUAN_CONTEXT ctx;
};


/* A simple implementation of a dynamic buffer.  Use init_membuf() to
   create a buffer, put_membuf to append bytes and get_membuf to
   release and return the buffer.  Allocation errors are detected but
   only returned at the final get_membuf(), this helps not to clutter
   the code with out of core checks.  */

static void
init_membuf (struct membuf *mb, int initiallen)
{
  mb->len = 0;
  mb->size = initiallen;
  mb->out_of_core = 0;
  mb->buf = xtrymalloc (initiallen);
  if (!mb->buf)
      mb->out_of_core = 1;
}

static void
put_membuf (struct membuf *mb, const void *buf, size_t len)
{
  if (mb->out_of_core)
    return;

  if (mb->len + len >= mb->size)
    {
      char *p;
      
      mb->size += len + 1024;
      p = xtryrealloc (mb->buf, mb->size);
      if (!p)
        {
          mb->out_of_core = 1;
          return;
        }
      mb->buf = p;
    }
  memcpy (mb->buf + mb->len, buf, len);
  mb->len += len;
}

static void *
get_membuf (struct membuf *mb, size_t *len)
{
  char *p;

  if (mb->out_of_core)
    {
      xfree (mb->buf);
      mb->buf = NULL;
      return NULL;
    }

  p = mb->buf;
  *len = mb->len;
  mb->buf = NULL;
  mb->out_of_core = 1; /* don't allow a reuse */
  return p;
}





/* Try to connect to the agent via socket or fork it off and work by
   pipes.  Handle the server's initial greeting */
static int
start_dirmngr (void)
{
  int rc;
  char *infostr, *p;
  ASSUAN_CONTEXT ctx;

  if (dirmngr_ctx)
    return 0; /* fixme: We need a context for each thread or serialize
                 the access to the dirmngr */

  infostr = force_pipe_server? NULL : getenv ("DIRMNGR_INFO");
  if (!infostr)
    {
      const char *pgmname;
      const char *argv[3];
      int no_close_list[3];
      int i;

      if (opt.verbose)
        log_info (_("no running dirmngr - starting one\n"));
      
      if (fflush (NULL))
        {
          gpg_error_t tmperr = gpg_error (gpg_err_code_from_errno (errno));
          log_error ("error flushing pending output: %s\n", strerror (errno));
          return tmperr;
        }

      if (!opt.dirmngr_program || !*opt.dirmngr_program)
        opt.dirmngr_program = GNUPG_DEFAULT_DIRMNGR;
      if ( !(pgmname = strrchr (opt.dirmngr_program, '/')))
        pgmname = opt.dirmngr_program;
      else
        pgmname++;

      argv[0] = pgmname;
      argv[1] = "--server";
      argv[2] = NULL;

      i=0;
      if (log_get_fd () != -1)
        no_close_list[i++] = log_get_fd ();
      no_close_list[i++] = fileno (stderr);
      no_close_list[i] = -1;

      /* connect to the agent and perform initial handshaking */
      rc = assuan_pipe_connect (&ctx, opt.dirmngr_program, (char**)argv,
                                no_close_list);
    }
  else
    {
      int prot;
      int pid;

      infostr = xstrdup (infostr);
      if ( !(p = strchr (infostr, ':')) || p == infostr)
        {
          log_error (_("malformed DIRMNGR_INFO environment variable\n"));
          xfree (infostr);
          force_pipe_server = 1;
          return start_dirmngr ();
        }
      *p++ = 0;
      pid = atoi (p);
      while (*p && *p != ':')
        p++;
      prot = *p? atoi (p+1) : 0;
      if (prot != 1)
        {
          log_error (_("dirmngr protocol version %d is not supported\n"),
                     prot);
          xfree (infostr);
          force_pipe_server = 1;
          return start_dirmngr ();
        }

      rc = assuan_socket_connect (&ctx, infostr, pid);
      xfree (infostr);
      if (rc == ASSUAN_Connect_Failed)
        {
          log_error (_("can't connect to the dirmngr - trying fall back\n"));
          force_pipe_server = 1;
          return start_dirmngr ();
        }
    }

  if (rc)
    {
      log_error ("can't connect to the dirmngr: %s\n", assuan_strerror (rc));
      return gpg_error (GPG_ERR_NO_DIRMNGR);
    }
  dirmngr_ctx = ctx;

  if (DBG_ASSUAN)
    log_debug ("connection to dirmngr established\n");
  return 0;
}



/* Handle a SENDCERT inquiry. */
static AssuanError
inq_certificate (void *opaque, const char *line)
{
  struct inq_certificate_parm_s *parm = opaque;
  AssuanError rc;
  const unsigned char *der;
  size_t derlen;

  if (!(!strncmp (line, "SENDCERT", 8) && (line[8] == ' ' || !line[8])))
    {
      log_error ("unsupported inquiry `%s'\n", line);
      return ASSUAN_Inquire_Unknown;
    }
  line += 8;

  if (!*line)
    { /* send the current certificate */
      der = ksba_cert_get_image (parm->cert, &derlen);
      if (!der)
        rc = ASSUAN_Inquire_Error;
      else
        rc = assuan_send_data (parm->ctx, der, derlen);
    }
  else 
    { /* send the given certificate */
      int err;
      KsbaCert cert;

      err = gpgsm_find_cert (line, &cert);
      if (err)
        {
          log_error ("certificate not found: %s\n", gpg_strerror (err));
          rc = ASSUAN_Inquire_Error;
        }
      else
        {
          der = ksba_cert_get_image (cert, &derlen);
          if (!der)
            rc = ASSUAN_Inquire_Error;
          else
            rc = assuan_send_data (parm->ctx, der, derlen);
          ksba_cert_release (cert);
        }
    }

  return rc; 
}



/* Call the directory manager to check whether the certificate is valid
   Returns 0 for valid or usually one of the errors:

  GPG_ERR_CERTIFICATE_REVOKED
  GPG_ERR_NO_CRL_KNOWN
  GPG_ERR_CRL_TOO_OLD
 */
int
gpgsm_dirmngr_isvalid (KsbaCert cert)
{
  int rc;
  char *certid;
  char line[ASSUAN_LINELENGTH];
  struct inq_certificate_parm_s parm;

  rc = start_dirmngr ();
  if (rc)
    return rc;

  certid = gpgsm_get_certid (cert);
  if (!certid)
    {
      log_error ("error getting the certificate ID\n");
      return gpg_error (GPG_ERR_GENERAL);
    }

  if (opt.verbose > 1)
    {
      char *fpr = gpgsm_get_fingerprint_string (cert, GCRY_MD_SHA1);
      log_info ("asking dirmngr about %s\n", fpr);
      xfree (fpr);
    }

  parm.ctx = dirmngr_ctx;
  parm.cert = cert;

  snprintf (line, DIM(line)-1, "ISVALID %s", certid);
  line[DIM(line)-1] = 0;
  xfree (certid);

  rc = assuan_transact (dirmngr_ctx, line, NULL, NULL,
                        inq_certificate, &parm, NULL, NULL);
  if (opt.verbose > 1)
    log_info ("response of dirmngr: %s\n", rc? assuan_strerror (rc): "okay");
  return map_assuan_err (rc);
}



/* Lookup helpers*/
static AssuanError
lookup_cb (void *opaque, const void *buffer, size_t length)
{
  struct lookup_parm_s *parm = opaque;
  size_t len;
  char *buf;
  KsbaCert cert;
  int rc;

  if (parm->error)
    return 0;

  if (buffer)
    {
      put_membuf (&parm->data, buffer, length);
      return 0;
    }
  /* END encountered - process what we have */
  buf = get_membuf (&parm->data, &len);
  if (!buf)
    {
      parm->error = gpg_error (GPG_ERR_ENOMEM);
      return 0;
    }

  cert = ksba_cert_new ();
  if (!cert)
    {
      parm->error = gpg_error (GPG_ERR_ENOMEM);
      return 0;
    }
  rc = ksba_cert_init_from_mem (cert, buf, len);
  if (rc)
    {
      log_error ("failed to parse a certificate: %s\n", ksba_strerror (rc));
    }
  else
    {
      parm->cb (parm->cb_value, cert);
    }

  ksba_cert_release (cert);
  init_membuf (&parm->data, 4096);
  return 0;
}

/* Return a properly escaped pattern from NAMES.  The only error
   return is NULL to indicate a malloc failure. */
static char *
pattern_from_strlist (STRLIST names)
{
  STRLIST sl;
  int n;
  const char *s;
  char *pattern, *p;

  for (n=0, sl=names; sl; sl = sl->next)
    {
      for (s=sl->d; *s; s++, n++)
	{
          if (*s == '%' || *s == ' ' || *s == '+')
            n += 2;
	}
      n++;
    }

  p = pattern = xtrymalloc (n+1);
  if (!pattern)
    return NULL;

  for (n=0, sl=names; sl; sl = sl->next)
    {
      for (s=sl->d; *s; s++)
        {
          switch (*s)
            {
            case '%':
              *p++ = '%';
              *p++ = '2';
              *p++ = '5';
              break;
            case ' ':
              *p++ = '%';
              *p++ = '2';
              *p++ = '0';
              break;
            case '+':
              *p++ = '%';
              *p++ = '2';
              *p++ = 'B';
              break;
            default:
              *p++ = *s;
              break;
            }
        }
      *p++ = ' ';
    }
  if (p == pattern)
    *pattern = 0; /* is empty */
  else
    p[-1] = '\0'; /* remove trailing blank */
  
  return pattern;
}

static AssuanError
lookup_status_cb (void *opaque, const char *line)
{
  struct lookup_parm_s *parm = opaque;

  if (!strncmp (line, "TRUNCATED", 9) && (line[9]==' ' || !line[9]))
    {
      if (parm->ctrl)
        {
          for (line +=9; *line == ' '; line++)
            ;
          gpgsm_status (parm->ctrl, STATUS_TRUNCATED, line);
        }
    }
  return 0;
}


/* Run the Directroy Managers lookup command using the pattern
   compiled from the strings given in NAMES.  The caller must provide
   the callback CB which will be passed cert by cert.  Note that CTRL
   is optional. */
int 
gpgsm_dirmngr_lookup (CTRL ctrl, STRLIST names,
                      void (*cb)(void*, KsbaCert), void *cb_value)
{ 
  int rc;
  char *pattern;
  char line[ASSUAN_LINELENGTH];
  struct lookup_parm_s parm;
  size_t len;

  rc = start_dirmngr ();
  if (rc)
    return rc;

  pattern = pattern_from_strlist (names);
  if (!pattern)
    return OUT_OF_CORE (errno);
  snprintf (line, DIM(line)-1, "LOOKUP %s", pattern);
  line[DIM(line)-1] = 0;
  xfree (pattern);

  parm.ctrl = ctrl;
  parm.ctx = dirmngr_ctx;
  parm.cb = cb;
  parm.cb_value = cb_value;
  parm.error = 0;
  init_membuf (&parm.data, 4096);

  rc = assuan_transact (dirmngr_ctx, line, lookup_cb, &parm,
                        NULL, NULL, lookup_status_cb, &parm);
  xfree (get_membuf (&parm.data, &len));
  if (rc)
    return map_assuan_err (rc);
  return parm.error;
}



/* Run Command helpers*/

/* Fairly simple callback to write all output of dirmngr to stdout. */
static AssuanError
run_command_cb (void *opaque, const void *buffer, size_t length)
{
  if (buffer)
    {
      if ( fwrite (buffer, length, 1, stdout) != 1 )
        log_error ("error writing to stdout: %s\n", strerror (errno));
    }
  return 0;
}

/* Handle inquiries from the dirmngr COMMAND. */
static AssuanError
run_command_inq_cb (void *opaque, const char *line)
{
  struct run_command_parm_s *parm = opaque;
  AssuanError rc = 0;

  if ( !strncmp (line, "SENDCERT", 8) && (line[8] == ' ' || !line[8]) )
    { /* send the given certificate */
      int err;
      KsbaCert cert;
      const unsigned char *der;
      size_t derlen;

      line += 8;
      if (!*line)
        return ASSUAN_Inquire_Error;

      err = gpgsm_find_cert (line, &cert);
      if (err)
        {
          log_error ("certificate not found: %s\n", gpg_strerror (err));
          rc = ASSUAN_Inquire_Error;
        }
      else
        {
          der = ksba_cert_get_image (cert, &derlen);
          if (!der)
            rc = ASSUAN_Inquire_Error;
          else
            rc = assuan_send_data (parm->ctx, der, derlen);
          ksba_cert_release (cert);
        }
    }
  else if ( !strncmp (line, "PRINTINFO", 9) && (line[9] == ' ' || !line[9]) )
    { /* Simply show the message given in the argument. */
      line += 9;
      log_info ("dirmngr: %s\n", line);
    }
  else
    {
      log_error ("unsupported inquiry `%s'\n", line);
      rc = ASSUAN_Inquire_Unknown;
    }

  return rc; 
}

static AssuanError
run_command_status_cb (void *opaque, const char *line)
{
  if (opt.verbose)
    {
      log_info ("dirmngr status: %s\n", line);
    }
  return 0;
}



/* Pass COMMAND to dirmngr and print all output generated by Dirmngr
   to stdout.  A couple of inquiries are defined (see above).  ARGC
   arguments in ARGV are given to the Dirmngr.  Spaces, plus and
   percent characters within the argument strings are percent escaped
   so that blanks can act as delimiters. */
int
gpgsm_dirmngr_run_command (CTRL ctrl, const char *command,
                           int argc, char **argv)
{ 
  int rc;
  int i;
  const char *s;
  char *line, *p;
  size_t len;
  struct run_command_parm_s parm;

  rc = start_dirmngr ();
  if (rc)
    return rc;

  parm.ctx = dirmngr_ctx;

  len = strlen (command) + 1;
  for (i=0; i < argc; i++)
    len += 1 + 3*strlen (argv[i]); /* enough space for percent escaping */
  line = xtrymalloc (len);
  if (!line)
    return OUT_OF_CORE (errno);

  p = stpcpy (line, command);
  for (i=0; i < argc; i++)
    {
      *p++ = ' ';
      for (s=argv[i]; *s; s++)
        {
          if (!isascii (*s))
            *p++ = *s;
          else if (*s == ' ')
            *p++ = '+';
          else if (!isprint (*s) || *s == '+')
            {
              sprintf (p, "%%%02X", *s);
              p += 3;
            }
          else
            *p++ = *s;
        }
    }
  *p = 0;

  rc = assuan_transact (dirmngr_ctx, line,
                        run_command_cb, NULL,
                        run_command_inq_cb, &parm,
                        run_command_status_cb, NULL);
  xfree (line);
  log_info ("response of dirmngr: %s\n", rc? assuan_strerror (rc): "okay");
  return map_assuan_err (rc);
}
