#include <unistd.h>

#include "types.h"
#include "mpool.h"
#include "tktree.h"
#include "clstree.h"
#include "sha1.h"
#include "listen.h"
#include "ssl_conf.h"
#include "rovm.h"

#include "thread.h"
#include "thread_mutex.h"
#include "thread_cond.h"

#include "mpm_worker_fdqueue.h"
#include "mpm_worker_pod.h"
#include "mpm_worker.h"

#include "connection.h"
#include "request.h"
#include "proc_rc.h"

#include "log.h"
#include "common.h"
#include "ticket.h"
#include "unixd.h"

static void ssl_scache_dbm_expire (struct rovm *r)
{
  SSLModConfigRec *mc = myModConfig(r);
  SSLSrvConfigRec *sc = mySrvConfig(r);
  static time_t tLast = 0;
  apr_dbm_t *dbm;
  apr_datum_t dbmkey;
  apr_datum_t dbmval;
  apr_pool_t *p;
  time_t tExpiresAt;
  int nElements = 0;
  int nDeleted = 0;
  int bDelete;
  apr_datum_t *keylist;
  int keyidx;
  int i;
  time_t tNow;
  apr_status_t rv;
  
  /*
   * make sure the expiration for still not-accessed session
   * cache entries is done only from time to time
   */
  tNow = time(NULL);
  if (tNow < tLast+sc->session_cache_timeout)
    return;
  tLast = tNow;
  
  /*
   * Here we have to be very carefully: Not all DBM libraries are
   * smart enough to allow one to iterate over the elements and at the
   * same time delete expired ones. Some of them get totally crazy
   * while others have no problems. So we have to do it the slower but
   * more safe way: we first iterate over all elements and remember
   * those which have to be expired. Then in a second pass we delete
   * all those expired elements. Additionally we reopen the DBM file
   * to be really safe in state.
   */
  
#define KEYMAX 1024
  
  ssl_mutex_on(r);
  for (;;) 
    {
      /* allocate the key array in a memory sub pool */
      apr_pool_create_ex (&p, mc->pPool, NULL, NULL);
      if (p == NULL)
        break;
      if ((keylist = apr_palloc(p, sizeof(dbmkey)*KEYMAX)) == NULL) 
        {
          apr_pool_destroy (p);
          break;
        }
      
      /* pass 1: scan DBM database */
      keyidx = 0;
      if ((rv = apr_dbm_open(&dbm, mc->szSessionCacheDataFile,
                             APR_DBM_RWCREATE,SSL_DBM_FILE_MODE,
                             p)) != APR_SUCCESS) 
        {
          rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                    "Cannot open SSLSessionCache DBM file `%s' for "
                    "scanning",
                    mc->szSessionCacheDataFile);
          apr_pool_destroy (p);
          break;
        }
      apr_dbm_firstkey(dbm, &dbmkey);
      while (dbmkey.dptr != NULL) 
        {
          nElements++;
          bDelete = FALSE;
          apr_dbm_fetch(dbm, dbmkey, &dbmval);
          if (dbmval.dsize <= sizeof(time_t) || dbmval.dptr == NULL)
            bDelete = TRUE;
          else 
            {
              memcpy(&tExpiresAt, dbmval.dptr, sizeof(time_t));
              if (tExpiresAt <= tNow)
                bDelete = TRUE;
            }
          if (bDelete) 
            {
              if ((keylist[keyidx].dptr = apr_palloc(p, dbmkey.dsize)) != NULL) 
                {
                  memcpy(keylist[keyidx].dptr, dbmkey.dptr, dbmkey.dsize);
                  keylist[keyidx].dsize = dbmkey.dsize;
                  keyidx++;
                  if (keyidx == KEYMAX)
                    break;
                }
            }
          apr_dbm_nextkey (dbm, &dbmkey);
        }
      apr_dbm_close (dbm);
      
      /* pass 2: delete expired elements */
      if (apr_dbm_open(&dbm, mc->szSessionCacheDataFile,
                       APR_DBM_RWCREATE,SSL_DBM_FILE_MODE, p) != APR_SUCCESS) 
        {
          rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                    "Cannot re-open SSLSessionCache DBM file `%s' for "
                    "expiring",
                    mc->szSessionCacheDataFile);
          apr_pool_destroy(p);
          break;
        }
      for (i = 0; i < keyidx; i++) 
        {
          apr_dbm_delete(dbm, keylist[i]);
          nDeleted++;
        }
      apr_dbm_close(dbm);
      
      /* destroy temporary pool */
      apr_pool_destroy(p);
      
      if (keyidx < KEYMAX)
        break;
    }
  ssl_mutex_off(r);
  
  rovm_log (NULL, ROVMLOG_DEBUG, ROVMLOG_MARK, 
            "Inter-Process Session Cache (DBM) Expiry: "
            "old: %d, new: %d, removed: %d",
            nElements, nElements-nDeleted, nDeleted);
  return;
}

void 
ssl_scache_dbm_init (struct rovm *r, apr_pool_t *p)
{
  SSLModConfigRec *mc = myModConfig(r);
  apr_dbm_t *dbm;
  apr_status_t rv;
  
  /* for the DBM we need the data file */
  if (mc->szSessionCacheDataFile == NULL) 
    {
      rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                "SSLSessionCache required");
      ssl_die();
    }
  
  /* open it once to create it and to make sure it _can_ be created */
  ssl_mutex_on (r);
  if ((rv = apr_dbm_open(&dbm, mc->szSessionCacheDataFile,
                         APR_DBM_RWCREATE, SSL_DBM_FILE_MODE, mc->pPool)) != APR_SUCCESS) 
    {
      rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                "Cannot create SSLSessionCache DBM file `%s'",
                mc->szSessionCacheDataFile);
      ssl_mutex_off (r);
      return;
    }
  apr_dbm_close (dbm);

#if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE)
  /*
   * We have to make sure the Apache child processes have access to
   * the DBM file. But because there are brain-dead platforms where we
   * cannot exactly determine the suffixes we try all possibilities.
   */
  if (geteuid () == 0 /* is superuser */) 
    {
      chown(mc->szSessionCacheDataFile, unixd_config.user_id, -1 /* no gid change */);
      if (chown(apr_pstrcat(p, mc->szSessionCacheDataFile, SSL_DBM_FILE_SUFFIX_DIR, NULL),
                unixd_config.user_id, -1) == -1) {
        if (chown(apr_pstrcat(p, mc->szSessionCacheDataFile, ".db", NULL),
                  unixd_config.user_id, -1) == -1)
          chown(apr_pstrcat(p, mc->szSessionCacheDataFile, ".dir", NULL),
                unixd_config.user_id, -1);
      }
      if (chown(apr_pstrcat(p, mc->szSessionCacheDataFile, SSL_DBM_FILE_SUFFIX_PAG, NULL),
                unixd_config.user_id, -1) == -1) {
        if (chown(apr_pstrcat(p, mc->szSessionCacheDataFile, ".db", NULL),
                  unixd_config.user_id, -1) == -1)
          chown(apr_pstrcat(p, mc->szSessionCacheDataFile, ".pag", NULL),
                unixd_config.user_id, -1);
      }
    }
#endif
  ssl_mutex_off (r);
  ssl_scache_dbm_expire (r);
  return;
}

void 
ssl_scache_dbm_kill (struct rovm *r)
{
  SSLModConfigRec *mc = myModConfig (r);
  rc_pool_t *p;

  apr_pool_create_ex (&p, mc->pPool, NULL, NULL);
  if (p != NULL) 
    {
      /* the correct way */
      unlink (apr_pstrcat (p, mc->szSessionCacheDataFile, SSL_DBM_FILE_SUFFIX_DIR, NULL));
      unlink (apr_pstrcat (p, mc->szSessionCacheDataFile, SSL_DBM_FILE_SUFFIX_PAG, NULL));
      /* the additional ways to be sure */
      unlink (apr_pstrcat (p, mc->szSessionCacheDataFile, ".dir", NULL));
      unlink (apr_pstrcat (p, mc->szSessionCacheDataFile, ".pag", NULL));
      unlink (apr_pstrcat (p, mc->szSessionCacheDataFile, ".db", NULL));
      unlink (mc->szSessionCacheDataFile);
      mp_destroy (p);
    }
    return;
}

BOOL 
ssl_scache_dbm_store (struct rovm *r, UCHAR *id, int idlen, time_t expiry, SSL_SESSION *sess)
{
  SSLModConfigRec *mc = myModConfig(r);
  apr_dbm_t *dbm;
  apr_datum_t dbmkey;
  apr_datum_t dbmval;
  UCHAR ucaData[SSL_SESSION_MAX_DER];
  int nData;
  UCHAR *ucp;
  apr_status_t rv;
  
  /* streamline session data */
  if ((nData = i2d_SSL_SESSION(sess, NULL)) > sizeof(ucaData)) 
    {
      rovm_log (NULL, ROVMLOG_DEBUG, ROVMLOG_MARK, 
                "streamline session data size too large: %d > "
                "%" APR_SIZE_T_FMT,
                nData, sizeof(ucaData));
      return FALSE;
    }
  ucp = ucaData;
  i2d_SSL_SESSION(sess, &ucp);
  
  /* be careful: do not try to store too much bytes in a DBM file! */
#ifdef PAIRMAX
  if ((idlen + nData) >= PAIRMAX) 
    {
      rovm_log (NULL, ROVMLOG_DEBUG, ROVMLOG_MARK, 
                "data size too large for DBM session cache: %d >= %d",
                (idlen + nData), PAIRMAX);
      return FALSE;
    }
#else
  if ((idlen + nData) >= 950 /* at least less than approx. 1KB */) 
    {
      rovm_log (NULL, ROVMLOG_DEBUG, ROVMLOG_MARK, 
                "data size too large for DBM session cache: %d >= %d",
                (idlen + nData), 950);
      return FALSE;
    }
#endif
  
  /* create DBM key */
  dbmkey.dptr  = (char *)id;
  dbmkey.dsize = idlen;
  
  /* create DBM value */
  dbmval.dsize = sizeof(time_t) + nData;
  dbmval.dptr  = (char *)malloc(dbmval.dsize);
  if (dbmval.dptr == NULL) 
    {
      rovm_log (NULL, ROVMLOG_DEBUG, ROVMLOG_MARK, 
                "malloc error creating DBM value");
      return FALSE;
    }
  memcpy((char *)dbmval.dptr, &expiry, sizeof(time_t));
  memcpy((char *)dbmval.dptr+sizeof(time_t), ucaData, nData);
  
  /* and store it to the DBM file */
  ssl_mutex_on (r);
  if ((rv = apr_dbm_open(&dbm, mc->szSessionCacheDataFile,
                         APR_DBM_RWCREATE, SSL_DBM_FILE_MODE, mc->pPool)) != APR_SUCCESS) 
    {
      rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                "Cannot open SSLSessionCache DBM file `%s' for writing "
                "(store)",
                mc->szSessionCacheDataFile);
      ssl_mutex_off(r);
      free(dbmval.dptr);
      return FALSE;
    }
  if ((rv = apr_dbm_store(dbm, dbmkey, dbmval)) != APR_SUCCESS) 
    {
      rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                "Cannot store SSL session to DBM file `%s'",
                mc->szSessionCacheDataFile);
      apr_dbm_close(dbm);
      ssl_mutex_off(r);
      free(dbmval.dptr);
      return FALSE;
    }
  apr_dbm_close(dbm);
  ssl_mutex_off(r);
  
  /* free temporary buffers */
  free(dbmval.dptr);
  
  /* allow the regular expiring to occur */
  ssl_scache_dbm_expire(r);
  
  return TRUE;
}

void 
ssl_scache_dbm_remove (struct rovm *r, UCHAR *id, int idlen)
{
  SSLModConfigRec *mc = myModConfig(r);
  apr_dbm_t *dbm;
  apr_datum_t dbmkey;
  apr_status_t rv;
  
  /* create DBM key and values */
  dbmkey.dptr  = (char *)id;
  dbmkey.dsize = idlen;
  
  /* and delete it from the DBM file */
  ssl_mutex_on(r);
  if ((rv = apr_dbm_open(&dbm, mc->szSessionCacheDataFile,
                         APR_DBM_RWCREATE, SSL_DBM_FILE_MODE, mc->pPool)) != APR_SUCCESS) 
    {
      rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                "Cannot open SSLSessionCache DBM file `%s' for writing "
                "(delete)",
                mc->szSessionCacheDataFile);
      ssl_mutex_off(r);
      return;
    }
  apr_dbm_delete(dbm, dbmkey);
  apr_dbm_close(dbm);
  ssl_mutex_off(r);
  
  return;
}

SSL_SESSION *
ssl_scache_dbm_retrieve (struct rovm *r, UCHAR *id, int idlen)
{
  SSLModConfigRec *mc = myModConfig(r);
  apr_dbm_t *dbm;
  apr_datum_t dbmkey;
  apr_datum_t dbmval;
  SSL_SESSION *sess = NULL;
  MODSSL_D2I_SSL_SESSION_CONST unsigned char *ucpData;
  int nData;
  time_t expiry;
  time_t now;
  apr_status_t rc;
  
  /* allow the regular expiring to occur */
  ssl_scache_dbm_expire(r);
  
  /* create DBM key and values */
  dbmkey.dptr  = (char *)id;
  dbmkey.dsize = idlen;
  
  /* and fetch it from the DBM file
   * XXX: Should we open the dbm against r->pool so the cleanup will
   * do the apr_dbm_close? This would make the code a bit cleaner.
   */
  ssl_mutex_on(r);
  if ((rc = apr_dbm_open(&dbm, mc->szSessionCacheDataFile,
                         APR_DBM_RWCREATE, SSL_DBM_FILE_MODE, mc->pPool)) != APR_SUCCESS) 
    {
      rovm_log (NULL, ROVMLOG_ERR, ROVMLOG_MARK, 
                "Cannot open SSLSessionCache DBM file `%s' for reading "
                "(fetch)",
                mc->szSessionCacheDataFile);
      ssl_mutex_off(r);
      return NULL;
    }
  rc = apr_dbm_fetch(dbm, dbmkey, &dbmval);
  if (rc != APR_SUCCESS) 
    {
      apr_dbm_close(dbm);
      ssl_mutex_off(r);
      return NULL;
    }
  if (dbmval.dptr == NULL || dbmval.dsize <= sizeof(time_t)) 
    {
      apr_dbm_close(dbm);
      ssl_mutex_off(r);
      return NULL;
    }
  
  /* parse resulting data */
  nData = dbmval.dsize-sizeof(time_t);
  ucpData = malloc(nData);
  if (ucpData == NULL) 
    {
      apr_dbm_close(dbm);
      ssl_mutex_off(r);
      return NULL;
    }
  /* Cast needed, ucpData may be const */
  memcpy((unsigned char *)ucpData,
         (char *)dbmval.dptr + sizeof(time_t), nData);
  memcpy(&expiry, dbmval.dptr, sizeof(time_t));
  
  apr_dbm_close(dbm);
  ssl_mutex_off(r);
  
  /* make sure the stuff is still not expired */
  now = time(NULL);
  if (expiry <= now) 
    {
      ssl_scache_dbm_remove(r, id, idlen);
      return NULL;
    }
  
  /* unstreamed SSL_SESSION */
  sess = d2i_SSL_SESSION (NULL, &ucpData, nData);
  
  return sess;
}
