#include <sys/types.h>
#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"

static char *
ssl_var_lookup_ssl_version(apr_pool_t *p, char *var)
{
  char *result;
  char *cp, *cp2;
  
  result = NULL;
  
  if (strEQ(var, "PRODUCT")) 
    {
#if defined(SSL_PRODUCT_NAME) && defined(SSL_PRODUCT_VERSION)
      result = apr_psprintf(p, "%s/%s", SSL_PRODUCT_NAME, SSL_PRODUCT_VERSION);
#else
      result = NULL;
#endif
    }
  else if (strEQ(var, "INTERFACE"))
    result = apr_psprintf (p, "mod_ssl/%s", MOD_SSL_VERSION);
  else if (strEQ(var, "LIBRARY")) 
    {
      result = apr_pstrdup(p, SSLeay_version(SSLEAY_VERSION));
      if ((cp = strchr(result, ' ')) != NULL) 
        {
          *cp = '/';
          if ((cp2 = strchr(cp, ' ')) != NULL)
            *cp2 = NUL;
        }
    }
  return result;
}

static void 
ssl_var_lookup_ssl_cipher_bits(SSL *ssl, int *usekeysize, int *algkeysize)
{
  SSL_CIPHER *cipher;
  
  *usekeysize = 0;
  *algkeysize = 0;
  if (ssl != NULL)
    if ((cipher = SSL_get_current_cipher(ssl)) != NULL)
      *usekeysize = SSL_CIPHER_get_bits(cipher, algkeysize);
  return;
}

static char *
ssl_var_lookup_ssl_cipher(apr_pool_t *p, conn_rec *c, char *var)
{
  SSLConnRec *sslconn = myConnConfig(c);
  char *result;
  BOOL resdup;
  int usekeysize, algkeysize;
  SSL *ssl;
  
  result = NULL;
  resdup = TRUE;
  
  ssl = sslconn->ssl;
  ssl_var_lookup_ssl_cipher_bits(ssl, &usekeysize, &algkeysize);
  
  if (ssl && strEQ(var, "")) 
    {
      SSL_CIPHER *cipher = SSL_get_current_cipher(ssl);
      result = (cipher != NULL ? (char *)SSL_CIPHER_get_name(cipher) : NULL);
    }
  else if (strcEQ(var, "_EXPORT"))
    result = (usekeysize < 56 ? "true" : "false");
  else if (strcEQ(var, "_USEKEYSIZE")) 
    {
      result = apr_itoa(p, usekeysize);
      resdup = FALSE;
    }
  else if (strcEQ(var, "_ALGKEYSIZE")) 
    {
      result = apr_itoa(p, algkeysize);
      resdup = FALSE;
    }
  
  if (result != NULL && resdup)
    result = apr_pstrdup(p, result);
  return result;
}

static char *
ssl_var_lookup_ssl_cert_PEM(apr_pool_t *p, X509 *xs)
{
  char *result;
  BIO *bio;
  int n;
  
  if ((bio = BIO_new(BIO_s_mem())) == NULL)
    return NULL;
  PEM_write_bio_X509(bio, xs);
  n = BIO_pending(bio);
  result = apr_pcalloc(p, n+1);
  n = BIO_read(bio, result, n);
  result[n] = NUL;
  BIO_free(bio);
  return result;
}

static char *
ssl_var_lookup_ssl_cert_chain(apr_pool_t *p, STACK_OF(X509) *sk, char *var)
{
  char *result;
  X509 *xs;
  int n;
  
  result = NULL;
  
  if (strspn(var, "0123456789") == strlen(var)) 
    {
      n = atoi(var);
      if (n < sk_X509_num(sk)) 
        {
          xs = sk_X509_value(sk, n);
          result = ssl_var_lookup_ssl_cert_PEM(p, xs);
        }
    }
  
  return result;
}

static char *
ssl_var_lookup_ssl_cert_verify(apr_pool_t *p, conn_rec *c)
{
  SSLConnRec *sslconn = myConnConfig(c);
  char *result;
  long vrc;
  const char *verr;
  const char *vinfo;
  SSL *ssl;
  X509 *xs;
  
  result = NULL;
  ssl   = sslconn->ssl;
  verr  = sslconn->verify_error;
  vinfo = sslconn->verify_info;
  vrc   = SSL_get_verify_result(ssl);
  xs    = SSL_get_peer_certificate(ssl);
  
  if (vrc == X509_V_OK && verr == NULL && vinfo == NULL && xs == NULL)
    /* no client verification done at all */
    result = "NONE";
  else if (vrc == X509_V_OK && verr == NULL && vinfo == NULL && xs != NULL)
    /* client verification done successful */
    result = "SUCCESS";
  else if (vrc == X509_V_OK && vinfo != NULL && strEQ(vinfo, "GENEROUS"))
    /* client verification done in generous way */
    result = "GENEROUS";
  else
    /* client verification failed */
    result = apr_psprintf(p, "FAILED:%s", verr);
  
  if (xs)
    X509_free(xs);
  return result;
}

static char *
ssl_var_lookup_ssl_cert_serial(apr_pool_t *p, X509 *xs)
{
  char *result;
  BIO *bio;
  int n;
  
  if ((bio = BIO_new(BIO_s_mem())) == NULL)
    return NULL;
  i2a_ASN1_INTEGER(bio, X509_get_serialNumber(xs));
  n = BIO_pending(bio);
  result = apr_pcalloc(p, n+1);
  n = BIO_read(bio, result, n);
  result[n] = NUL;
  BIO_free(bio);
  return result;
}

static char *
ssl_var_lookup_ssl_cert_valid(apr_pool_t *p, ASN1_UTCTIME *tm)
{
  char *result;
  BIO* bio;
  int n;
  
  if ((bio = BIO_new(BIO_s_mem())) == NULL)
    return NULL;
  ASN1_UTCTIME_print(bio, tm);
  n = BIO_pending(bio);
  result = apr_pcalloc(p, n+1);
  n = BIO_read(bio, result, n);
  result[n] = NUL;
  BIO_free(bio);
  return result;
}

#define DIGIT2NUM(x) (((x)[0] - '0') * 10 + (x)[1] - '0')

/* Return a string giving the number of days remaining until 'tm', or
 * "0" if this can't be determined. */
static char *
ssl_var_lookup_ssl_cert_remain(apr_pool_t *p, ASN1_UTCTIME *tm)
{
  apr_time_t then, now = apr_time_now();
  apr_time_exp_t exp = {0};
  long diff;
  
  /* Fail if the time isn't a valid ASN.1 UTCTIME; RFC3280 mandates
   * that the seconds digits are present even though ASN.1
   * doesn't. */
  if (tm->length < 11 || !ASN1_UTCTIME_check(tm))
    return apr_pstrdup(p, "0");
  
  exp.tm_year = DIGIT2NUM(tm->data);
  exp.tm_mon = DIGIT2NUM(tm->data + 2) - 1;
  exp.tm_mday = DIGIT2NUM(tm->data + 4) + 1;
  exp.tm_hour = DIGIT2NUM(tm->data + 6);
  exp.tm_min = DIGIT2NUM(tm->data + 8);
  exp.tm_sec = DIGIT2NUM(tm->data + 10);
  
  if (exp.tm_year <= 50) exp.tm_year += 100;
  
  if (apr_time_exp_gmt_get(&then, &exp) != APR_SUCCESS)
    return apr_pstrdup(p, "0");
  
  diff = (long)((apr_time_sec(then) - apr_time_sec(now)) / (60*60*24));
  
  return diff > 0 ? apr_ltoa(p, diff) : apr_pstrdup(p, "0");
}

static const struct
{
  char *name;
  int   nid;
} ssl_var_lookup_ssl_cert_dn_rec[] = 
  {
    { "C",     NID_countryName            },
    { "ST",    NID_stateOrProvinceName    }, /* officially    (RFC2156) */
    { "SP",    NID_stateOrProvinceName    }, /* compatibility (SSLeay)  */
    { "L",     NID_localityName           },
    { "O",     NID_organizationName       },
    { "OU",    NID_organizationalUnitName },
    { "CN",    NID_commonName             },
    { "T",     NID_title                  },
    { "I",     NID_initials               },
    { "G",     NID_givenName              },
    { "S",     NID_surname                },
    { "D",     NID_description            },
#ifdef NID_x500UniqueIdentifier /* new name as of Openssl 0.9.7 */
    { "UID",   NID_x500UniqueIdentifier   },
#else /* old name, OpenSSL < 0.9.7 */
    { "UID",   NID_uniqueIdentifier       },
#endif
    { "Email", NID_pkcs9_emailAddress     },
    { NULL,    0                          }
  };

static char *
ssl_var_lookup_ssl_cert_dn(apr_pool_t *p, X509_NAME *xsname, char *var)
{
  char *result, *ptr;
  X509_NAME_ENTRY *xsne;
  int i, j, n, idx = 0;
  apr_size_t varlen;
  
  /* if an _N suffix is used, find the Nth attribute of given name */
  ptr = strchr(var, '_');
  if (ptr != NULL && strspn(ptr + 1, "0123456789") == strlen(ptr + 1)) 
    {
      idx = atoi(ptr + 1);
      varlen = ptr - var;
    }
  else
    varlen = strlen(var);
  
  result = NULL;
  
  for (i = 0; ssl_var_lookup_ssl_cert_dn_rec[i].name != NULL; i++) 
    {
      if (strEQn(var, ssl_var_lookup_ssl_cert_dn_rec[i].name, varlen)
          && strlen(ssl_var_lookup_ssl_cert_dn_rec[i].name) == varlen) 
        {
          for (j = 0; j < sk_X509_NAME_ENTRY_num((STACK_OF(X509_NAME_ENTRY) *)
                                                 X509_NAME_get_entries(xsname));
               j++) 
            {
              xsne = sk_X509_NAME_ENTRY_value((STACK_OF(X509_NAME_ENTRY) *)
                                              X509_NAME_get_entries(xsname), j);
              
              n =OBJ_obj2nid((ASN1_OBJECT *)X509_NAME_ENTRY_get_object(xsne));
              
              if (n == ssl_var_lookup_ssl_cert_dn_rec[i].nid && idx-- == 0) 
                {
                  unsigned char *data = X509_NAME_ENTRY_get_data_ptr(xsne);
                  /* cast needed from unsigned char to char */
                  result = apr_pstrmemdup(p, (char *)data,
                                          X509_NAME_ENTRY_get_data_len(xsne));
#if APR_CHARSET_EBCDIC
                  ap_xlate_proto_from_ascii(result, X509_NAME_ENTRY_get_data_len(xsne));
#endif /* APR_CHARSET_EBCDIC */
                  break;
                }
            }
          break;
        }
    }
  return result;
}

static char *
ssl_var_lookup_ssl_cert(apr_pool_t *p, X509 *xs, char *var)
{
  char *result;
  BOOL resdup;
  X509_NAME *xsname;
  int nid;
  char *cp;
  
  result = NULL;
  resdup = TRUE;
  
  if (strcEQ(var, "M_VERSION")) 
    {
      result = apr_psprintf(p, "%lu", X509_get_version(xs)+1);
      resdup = FALSE;
    }
  else if (strcEQ(var, "M_SERIAL")) {
    result = ssl_var_lookup_ssl_cert_serial(p, xs);
  }
  else if (strcEQ(var, "V_START")) {
    result = ssl_var_lookup_ssl_cert_valid(p, X509_get_notBefore(xs));
  }
  else if (strcEQ(var, "V_END")) {
    result = ssl_var_lookup_ssl_cert_valid(p, X509_get_notAfter(xs));
  }
  else if (strcEQ(var, "V_REMAIN")) 
    {
      result = ssl_var_lookup_ssl_cert_remain(p, X509_get_notAfter(xs));
      resdup = FALSE;
    }
  else if (strcEQ(var, "S_DN")) 
    {
      xsname = X509_get_subject_name(xs);
      cp = X509_NAME_oneline(xsname, NULL, 0);
      result = apr_pstrdup(p, cp);
      modssl_free(cp);
      resdup = FALSE;
    }
  else if (strlen(var) > 5 && strcEQn(var, "S_DN_", 5)) 
    {
      xsname = X509_get_subject_name(xs);
      result = ssl_var_lookup_ssl_cert_dn(p, xsname, var+5);
      resdup = FALSE;
    }
  else if (strcEQ(var, "I_DN")) 
    {
      xsname = X509_get_issuer_name(xs);
      cp = X509_NAME_oneline(xsname, NULL, 0);
      result = apr_pstrdup(p, cp);
      modssl_free(cp);
      resdup = FALSE;
    }
  else if (strlen(var) > 5 && strcEQn(var, "I_DN_", 5)) 
    {
      xsname = X509_get_issuer_name(xs);
      result = ssl_var_lookup_ssl_cert_dn(p, xsname, var+5);
      resdup = FALSE;
    }
  else if (strcEQ(var, "A_SIG")) 
    {
      nid = OBJ_obj2nid((ASN1_OBJECT *)X509_get_signature_algorithm(xs));
      result = apr_pstrdup(p,
                           (nid == NID_undef) ? "UNKNOWN" : OBJ_nid2ln(nid));
      resdup = FALSE;
    }
  else if (strcEQ(var, "A_KEY")) 
    {
      nid = OBJ_obj2nid((ASN1_OBJECT *)X509_get_key_algorithm(xs));
      result = apr_pstrdup(p,
                           (nid == NID_undef) ? "UNKNOWN" : OBJ_nid2ln(nid));
      resdup = FALSE;
    }
  else if (strcEQ(var, "CERT")) {
    result = ssl_var_lookup_ssl_cert_PEM(p, xs);
  }
  
  if (result != NULL && resdup)
    result = apr_pstrdup(p, result);

  return result;
}

static char *
ssl_var_lookup_ssl_compress_meth(SSL *ssl)
{
  char *result = "NULL";
#ifdef OPENSSL_VERSION_NUMBER
#if (OPENSSL_VERSION_NUMBER >= 0x00908000)
  SSL_SESSION *pSession = SSL_get_session(ssl);
  
  if (pSession) 
    {
      switch (pSession->compress_meth) 
        {
        case 0:
          /* default "NULL" already set */
          break;
          
          /* Defined by RFC 3749, deflate is coded by "1" */
        case 1:
          result = "DEFLATE";
          break;
          
          /* IANA assigned compression number for LZS */
        case 0x40:
          result = "LZS";
          break;
          
        default:
          result = "UNKNOWN";
          break;
        }
    }
#endif
#endif
  return result;
}

static char *ssl_var_lookup_ssl(apr_pool_t *p, conn_rec *c, char *var)
{
  SSLConnRec *sslconn = myConnConfig(c);
  char *result;
  X509 *xs;
  STACK_OF(X509) *sk;
  SSL *ssl;
  
  result = NULL;
  
  ssl = sslconn->ssl;
  if (strlen(var) > 8 && strcEQn(var, "VERSION_", 8)) {
    result = ssl_var_lookup_ssl_version(p, var+8);
  }
  else if (ssl != NULL && strcEQ(var, "PROTOCOL")) {
    result = (char *)SSL_get_version(ssl);
  }
  else if (ssl != NULL && strcEQ(var, "SESSION_ID")) 
    {
      char buf[SSL_SESSION_ID_STRING_LEN];
      SSL_SESSION *pSession = SSL_get_session(ssl);
      if (pSession) 
        {
          result = apr_pstrdup(p, SSL_SESSION_id2sz(
                                                    SSL_SESSION_get_session_id(pSession),
                                                    SSL_SESSION_get_session_id_length(pSession),
                                                    buf, sizeof(buf)));
        }
    }
  else if (ssl != NULL && strlen(var) >= 6 && strcEQn(var, "CIPHER", 6)) {
    result = ssl_var_lookup_ssl_cipher(p, c, var+6);
  }
  else if (ssl != NULL && strlen(var) > 18 && strcEQn(var, "CLIENT_CERT_CHAIN_", 18)) 
    {
      sk = SSL_get_peer_cert_chain(ssl);
      result = ssl_var_lookup_ssl_cert_chain(p, sk, var+18);
    }
  else if (ssl != NULL && strcEQ(var, "CLIENT_VERIFY")) {
    result = ssl_var_lookup_ssl_cert_verify(p, c);
  }
  else if (ssl != NULL && strlen(var) > 7 && strcEQn(var, "CLIENT_", 7)) 
    {
      if ((xs = SSL_get_peer_certificate(ssl)) != NULL) 
        {
          result = ssl_var_lookup_ssl_cert(p, xs, var+7);
          X509_free(xs);
        }
    }
  else if (ssl != NULL && strlen(var) > 7 && strcEQn(var, "SERVER_", 7)) 
    {
      if ((xs = SSL_get_certificate(ssl)) != NULL)
        result = ssl_var_lookup_ssl_cert(p, xs, var+7);
    }
  else if (ssl != NULL && strcEQ(var, "COMPRESS_METHOD")) {
    result = ssl_var_lookup_ssl_compress_meth(ssl);
  }
  return result;
}


/* This function must remain safe to use for a non-SSL connection. */
char *
ssl_var_lookup (apr_pool_t *p, struct rovm *s, conn_rec *c, request_rec *r, char *var)
{
  SSLModConfigRec *mc = myModConfig(s);
  const char *result;
  BOOL resdup;
  apr_time_exp_t tm;
  
  result = NULL;
  resdup = TRUE;
  
  /*
   * When no pool is given try to find one
   */
  if (p == NULL) 
    {
      if (r != NULL)
        p = r->pool;
      else if (c != NULL)
        p = c->pool;
      else
        p = mc->pPool;
    }

  if (r)
    {
      rovm_log (NULL, ROVMLOG_DEBUG, ROVMLOG_MARK, "ssl_engine_var: TODO");
      return NULL;
    }

  /*
   * Connection stuff
   */
  if (result == NULL && c != NULL)
    {
      SSLConnRec *sslconn = myConnConfig (c);
        if (strlen(var) > 4 && strcEQn(var, "SSL_", 4)
            && sslconn && sslconn->ssl)
            result = ssl_var_lookup_ssl(p, c, var+4);
        else if (strcEQ(var, "REMOTE_ADDR"))
            result = c->remote_ip;
        else if (strcEQ(var, "HTTPS")) {
            if (sslconn && sslconn->ssl)
                result = "on";
            else
                result = "off";
        }
    }
 
    /*
     * Totally independent stuff
     */
  if (result == NULL) 
    {
      if (strlen(var) > 12 && strcEQn(var, "SSL_VERSION_", 12))
        result = ssl_var_lookup_ssl_version(p, var+12);
      else if (strcEQ(var, "SERVER_SOFTWARE"))
        result = rv_get_server_version ();
      else if (strcEQ(var, "API_VERSION")) 
        {
          result = apr_itoa(p, 20060310);
          resdup = FALSE;
        }
      else if (strcEQ(var, "TIME_YEAR")) 
        {
          apr_time_exp_lt(&tm, apr_time_now());
          result = apr_psprintf(p, "%02d%02d",
                                (tm.tm_year / 100) + 19, tm.tm_year % 100);
          resdup = FALSE;
        }
#define MKTIMESTR(format, tmfield)                        \
      apr_time_exp_lt(&tm, apr_time_now());               \
      result = apr_psprintf(p, format, tm.tmfield);       \
      resdup = FALSE;
      else if (strcEQ(var, "TIME_MON")) {
        MKTIMESTR("%02d", tm_mon+1)
          }
      else if (strcEQ(var, "TIME_DAY")) {
        MKTIMESTR("%02d", tm_mday)
          }
      else if (strcEQ(var, "TIME_HOUR")) {
        MKTIMESTR("%02d", tm_hour)
          }
      else if (strcEQ(var, "TIME_MIN")) {
        MKTIMESTR("%02d", tm_min)
          }
      else if (strcEQ(var, "TIME_SEC")) {
        MKTIMESTR("%02d", tm_sec)
          }
      else if (strcEQ(var, "TIME_WDAY")) {
        MKTIMESTR("%d", tm_wday)
          }
      else if (strcEQ(var, "TIME")) 
        {
          apr_time_exp_lt(&tm, apr_time_now());
          result = apr_psprintf(p,
                                "%02d%02d%02d%02d%02d%02d%02d", (tm.tm_year / 100) + 19,
                                (tm.tm_year % 100), tm.tm_mon+1, tm.tm_mday,
                                tm.tm_hour, tm.tm_min, tm.tm_sec);
          resdup = FALSE;
        }
      /* all other env-variables from the parent Apache process */
      else if (strlen(var) > 4 && strcEQn(var, "ENV:", 4)) 
        {
          if (result == NULL)
            result = getenv(var+4);
        }
    }
  
  if (result != NULL && resdup)
    result = apr_pstrdup(p, result);
  if (result == NULL)
    result = "";
  return (char *)result;
}
