/* crlfetch.c - LDAP access
 *      Copyright (C) 2002 Klarälvdalens Datakonsult AB
 *      Copyright (C) 2003, 2004, 2005, 2006, 2007 g10 Code GmbH
 *
 * This file is part of DirMngr.
 *
 * DirMngr 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.
 *
 * DirMngr 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/>.
 */

#include <config.h>

#include <stdio.h>
#include <errno.h>
#include <npth.h>

#include "crlfetch.h"
#include "dirmngr.h"
#include "misc.h"
#include "http.h"

#if USE_LDAP
# include "ldap-wrapper.h"
#endif

/* For detecting armored CRLs received via HTTP (yes, such CRLS really
   exits, e.g. http://grid.fzk.de/ca/gridka-crl.pem at least in June
   2008) we need a context in the reader callback.  */
struct reader_cb_context_s
{
  estream_t fp;             /* The stream used with the ksba reader.  */
  int checked:1;            /* PEM/binary detection ahs been done.    */
  int is_pem:1;             /* The file stream is PEM encoded.        */
  struct b64state b64state; /* The state used for Base64 decoding.    */
};


/* We need to associate a reader object with the reader callback
   context.  This table is used for it. */
struct file_reader_map_s
{
  ksba_reader_t reader;
  struct reader_cb_context_s *cb_ctx;
};
#define MAX_FILE_READER 50
static struct file_reader_map_s file_reader_map[MAX_FILE_READER];

/* Associate FP with READER.  If the table is full wait until another
   thread has removed an entry.  */
static void
register_file_reader (ksba_reader_t reader, struct reader_cb_context_s *cb_ctx)
{
  int i;

  for (;;)
    {
      for (i=0; i < MAX_FILE_READER; i++)
        if (!file_reader_map[i].reader)
          {
            file_reader_map[i].reader = reader;
            file_reader_map[i].cb_ctx = cb_ctx;
            return;
          }
      log_info (_("reader to file mapping table full - waiting\n"));
      npth_sleep (2);
    }
}

/* Scan the table for an entry matching READER, remove that entry and
   return the associated file pointer. */
static struct reader_cb_context_s *
get_file_reader (ksba_reader_t reader)
{
  struct reader_cb_context_s *cb_ctx = NULL;
  int i;

  for (i=0; i < MAX_FILE_READER; i++)
    if (file_reader_map[i].reader == reader)
      {
        cb_ctx = file_reader_map[i].cb_ctx;
        file_reader_map[i].reader = NULL;
        file_reader_map[i].cb_ctx = NULL;
        break;
      }
  return cb_ctx;
}



static int
my_es_read (void *opaque, char *buffer, size_t nbytes, size_t *nread)
{
  struct reader_cb_context_s *cb_ctx = opaque;
  int result;

  result = es_read (cb_ctx->fp, buffer, nbytes, nread);
  if (result)
    return result;
  /* Fixme we should check whether the semantics of es_read are okay
     and well defined.  I have some doubts.  */
  if (nbytes && !*nread && es_feof (cb_ctx->fp))
    return gpg_error (GPG_ERR_EOF);
  if (!nread && es_ferror (cb_ctx->fp))
    return gpg_error (GPG_ERR_EIO);

  if (!cb_ctx->checked && *nread)
    {
      int c = *(unsigned char *)buffer;

      cb_ctx->checked = 1;
      if ( ((c & 0xc0) >> 6) == 0 /* class: universal */
           && (c & 0x1f) == 16    /* sequence */
           && (c & 0x20)          /* is constructed */ )
        ; /* Binary data.  */
      else
        {
          cb_ctx->is_pem = 1;
          b64dec_start (&cb_ctx->b64state, "");
        }
    }
  if (cb_ctx->is_pem && *nread)
    {
      size_t nread2;

      if (b64dec_proc (&cb_ctx->b64state, buffer, *nread, &nread2))
        {
          /* EOF from decoder. */
          *nread = 0;
          result = gpg_error (GPG_ERR_EOF);
        }
      else
        *nread = nread2;
    }

  return result;
}


/* Fetch CRL from URL and return the entire CRL using new ksba reader
   object in READER.  Note that this reader object should be closed
   only using ldap_close_reader. */
gpg_error_t
crl_fetch (ctrl_t ctrl, const char *url, ksba_reader_t *reader)
{
  gpg_error_t err;
  parsed_uri_t uri;
  char *free_this = NULL;
  int redirects_left = 2; /* We allow for 2 redirect levels.  */

  *reader = NULL;

  if (!url)
    return gpg_error (GPG_ERR_INV_ARG);

 once_more:
  err = http_parse_uri (&uri, url, 0);
  http_release_parsed_uri (uri);
  if (err && !strncmp (url, "https:", 6))
    {
      /* Our HTTP code does not support TLS, thus we can't use this
         scheme and it is frankly not useful for CRL retrieval anyway.
         We resort to using http, assuming that the server also
         provides plain http access. */
      free_this = xtrymalloc (strlen (url) + 1);
      if (free_this)
        {
          strcpy (stpcpy (free_this,"http:"), url+6);
          err = http_parse_uri (&uri, free_this, 0);
          http_release_parsed_uri (uri);
          if (!err)
            {
              log_info (_("using \"http\" instead of \"https\"\n"));
              url = free_this;
            }
        }
    }
  if (!err) /* Yes, our HTTP code groks that. */
    {
      http_t hd;

      if (opt.disable_http)
        {
          log_error (_("CRL access not possible due to disabled %s\n"),
                     "HTTP");
          err = gpg_error (GPG_ERR_NOT_SUPPORTED);
        }
      else
        err = http_open_document (&hd, url, NULL,
                                  (opt.honor_http_proxy? HTTP_FLAG_TRY_PROXY:0)
                                  |(DBG_LOOKUP? HTTP_FLAG_LOG_RESP:0),
                                  ctrl->http_proxy, NULL, NULL, NULL);

      switch ( err? 99999 : http_get_status_code (hd) )
        {
        case 200:
          {
            estream_t fp = http_get_read_ptr (hd);
            struct reader_cb_context_s *cb_ctx;

            cb_ctx = xtrycalloc (1, sizeof *cb_ctx);
            if (!cb_ctx)
              err = gpg_error_from_syserror ();
            if (!err)
              err = ksba_reader_new (reader);
            if (!err)
              {
                cb_ctx->fp = fp;
                err = ksba_reader_set_cb (*reader, &my_es_read, cb_ctx);
              }
            if (err)
              {
                log_error (_("error initializing reader object: %s\n"),
                           gpg_strerror (err));
                ksba_reader_release (*reader);
                *reader = NULL;
                http_close (hd, 0);
              }
            else
              {
                /* The ksba reader misses a user pointer thus we need
                   to come up with our own way of associating a file
                   pointer (or well the callback context) with the
                   reader.  It is only required when closing the
                   reader thus there is no performance issue doing it
                   this way.  FIXME: We now have a close notification
                   which might be used here. */
                register_file_reader (*reader, cb_ctx);
                http_close (hd, 1);
              }
          }
          break;

        case 301: /* Redirection (perm.). */
        case 302: /* Redirection (temp.). */
          {
            const char *s = http_get_header (hd, "Location");

            log_info (_("URL '%s' redirected to '%s' (%u)\n"),
                      url, s?s:"[none]", http_get_status_code (hd));
            if (s && *s && redirects_left-- )
              {
                xfree (free_this); url = NULL;
                free_this = xtrystrdup (s);
                if (!free_this)
                  err = gpg_error_from_errno (errno);
                else
                  {
                    url = free_this;
                    http_close (hd, 0);
                    /* Note, that our implementation of redirection
                       actually handles a redirect to LDAP.  */
                    goto once_more;
                  }
              }
            else
              err = gpg_error (GPG_ERR_NO_DATA);
            log_error (_("too many redirections\n")); /* Or no "Location". */
            http_close (hd, 0);
          }
          break;

        case 99999: /* Made up status code for error reporting.  */
          log_error (_("error retrieving '%s': %s\n"),
                     url, gpg_strerror (err));
          break;

        default:
          log_error (_("error retrieving '%s': http status %u\n"),
                     url, http_get_status_code (hd));
          err = gpg_error (GPG_ERR_NO_DATA);
          http_close (hd, 0);
        }
    }
  else /* Let the LDAP code try other schemes. */
    {
      if (opt.disable_ldap)
        {
          log_error (_("CRL access not possible due to disabled %s\n"),
                     "LDAP");
          err = gpg_error (GPG_ERR_NOT_SUPPORTED);
        }
      else
        {
#       if USE_LDAP
          err = url_fetch_ldap (ctrl, url, NULL, 0, reader);
#       else /*!USE_LDAP*/
          err = gpg_error (GPG_ERR_NOT_IMPLEMENTED);
#       endif /*!USE_LDAP*/
        }
    }

  xfree (free_this);
  return err;
}


/* Fetch CRL for ISSUER using a default server. Return the entire CRL
   as a newly opened stream returned in R_FP. */
gpg_error_t
crl_fetch_default (ctrl_t ctrl, const char *issuer, ksba_reader_t *reader)
{
  if (opt.disable_ldap)
    {
      log_error (_("CRL access not possible due to disabled %s\n"),
                 "LDAP");
      return gpg_error (GPG_ERR_NOT_SUPPORTED);
    }
#if USE_LDAP
  return attr_fetch_ldap (ctrl, issuer, "certificateRevocationList",
                          reader);
#else
  (void)ctrl;
  (void)issuer;
  (void)reader;
  return gpg_error (GPG_ERR_NOT_IMPLEMENTED);
#endif
}


/* Fetch a CA certificate for DN using the default server. This
   function only initiates the fetch; fetch_next_cert must be used to
   actually read the certificate; end_cert_fetch to end the
   operation. */
gpg_error_t
ca_cert_fetch (ctrl_t ctrl, cert_fetch_context_t *context, const char *dn)
{
  if (opt.disable_ldap)
    {
      log_error (_("CRL access not possible due to disabled %s\n"),
                 "LDAP");
      return gpg_error (GPG_ERR_NOT_SUPPORTED);
    }
#if USE_LDAP
  return start_default_fetch_ldap (ctrl, context, dn, "cACertificate");
#else
  (void)ctrl;
  (void)context;
  (void)dn;
  return gpg_error (GPG_ERR_NOT_IMPLEMENTED);
#endif
}


gpg_error_t
start_cert_fetch (ctrl_t ctrl, cert_fetch_context_t *context,
                  strlist_t patterns, const ldap_server_t server)
{
  if (opt.disable_ldap)
    {
      log_error (_("certificate search not possible due to disabled %s\n"),
                 "LDAP");
      return gpg_error (GPG_ERR_NOT_SUPPORTED);
    }
#if USE_LDAP
  return start_cert_fetch_ldap (ctrl, context, patterns, server);
#else
  (void)ctrl;
  (void)context;
  (void)patterns;
  (void)server;
  return gpg_error (GPG_ERR_NOT_IMPLEMENTED);
#endif
}


gpg_error_t
fetch_next_cert (cert_fetch_context_t context,
                 unsigned char **value, size_t * valuelen)
{
#if USE_LDAP
  return fetch_next_cert_ldap (context, value, valuelen);
#else
  (void)context;
  (void)value;
  (void)valuelen;
  return gpg_error (GPG_ERR_NOT_IMPLEMENTED);
#endif
}


/* Fetch the next data from CONTEXT, assuming it is a certificate and return
   it as a cert object in R_CERT.  */
gpg_error_t
fetch_next_ksba_cert (cert_fetch_context_t context, ksba_cert_t *r_cert)
{
  gpg_error_t err;
  unsigned char *value;
  size_t valuelen;
  ksba_cert_t cert;

  *r_cert = NULL;

#if USE_LDAP
  err = fetch_next_cert_ldap (context, &value, &valuelen);
  if (!err && !value)
    err = gpg_error (GPG_ERR_BUG);
#else
  (void)context;
  err = gpg_error (GPG_ERR_NOT_IMPLEMENTED);
#endif
  if (err)
    return err;

  err = ksba_cert_new (&cert);
  if (err)
    {
      xfree (value);
      return err;
    }

  err = ksba_cert_init_from_mem (cert, value, valuelen);
  xfree (value);
  if (err)
    {
      ksba_cert_release (cert);
      return err;
    }
  *r_cert = cert;
  return 0;
}


void
end_cert_fetch (cert_fetch_context_t context)
{
#if USE_LDAP
  end_cert_fetch_ldap (context);
#else
  (void)context;
#endif
}


/* Lookup a cert by it's URL.  */
gpg_error_t
fetch_cert_by_url (ctrl_t ctrl, const char *url,
		   unsigned char **value, size_t *valuelen)
{
  const unsigned char *cert_image;
  size_t cert_image_n;
  ksba_reader_t reader;
  ksba_cert_t cert;
  gpg_error_t err;

  *value = NULL;
  *valuelen = 0;
  cert_image = NULL;
  reader = NULL;
  cert = NULL;

#if USE_LDAP
  err = url_fetch_ldap (ctrl, url, NULL, 0, &reader);
#else
  (void)ctrl;
  (void)url;
  err = gpg_error (GPG_ERR_NOT_IMPLEMENTED);
#endif /*USE_LDAP*/
  if (err)
    goto leave;

  err = ksba_cert_new (&cert);
  if (err)
    goto leave;

  err = ksba_cert_read_der (cert, reader);
  if (err)
    goto leave;

  cert_image = ksba_cert_get_image (cert, &cert_image_n);
  if (!cert_image || !cert_image_n)
    {
      err = gpg_error (GPG_ERR_INV_CERT_OBJ);
      goto leave;
    }

  *value = xtrymalloc (cert_image_n);
  if (!*value)
    {
      err = gpg_error_from_syserror ();
      goto leave;
    }

  memcpy (*value, cert_image, cert_image_n);
  *valuelen = cert_image_n;

 leave:

  ksba_cert_release (cert);
#if USE_LDAP
  ldap_wrapper_release_context (reader);
#endif /*USE_LDAP*/

  return err;
}

/* This function is to be used to close the reader object.  In
   addition to running ksba_reader_release it also releases the LDAP
   or HTTP contexts associated with that reader.  */
void
crl_close_reader (ksba_reader_t reader)
{
  struct reader_cb_context_s *cb_ctx;

  if (!reader)
    return;

  /* Check whether this is a HTTP one. */
  cb_ctx = get_file_reader (reader);
  if (cb_ctx)
    {
      /* This is an HTTP context. */
      if (cb_ctx->fp)
        es_fclose (cb_ctx->fp);
      /* Release the base64 decoder state.  */
      if (cb_ctx->is_pem)
        b64dec_finish (&cb_ctx->b64state);
      /* Release the callback context.  */
      xfree (cb_ctx);
    }
  else /* This is an ldap wrapper context (Currently not used). */
    {
#if USE_LDAP
      ldap_wrapper_release_context (reader);
#endif /*USE_LDAP*/
    }

  /* Now get rid of the reader object. */
  ksba_reader_release (reader);
}
