/* ========================================================================== */
/*! \file
 * \brief Minimalistic WWW client
 *
 * Copyright (c) 2015-2016 by the developers. See the LICENSE file for details.
 *
 * If nothing else is specified, functions return zero to indicate success
 * and a negative value to indicate an error.
 */


/* ========================================================================== */
/* Include headers */

#include "posix.h"  /* Include this first because of feature test macros */

#include <string.h>

#include "encoding.h"
#include "fileutils.h"
#include "http.h"
#include "inet.h"
#include "main.h"


/* ========================================================================== */
/*! \addtogroup HTTP
 *
* This module should behave RFC3986, RFC7230, RFC7231 and RFC7234 compliant.
 *
 * This module implements a minimalistic WWW client to download X.509 CRLs via
 * HTTP/1.1 protocol (for certificate revocation checks of the TLS module).
 */
/* @{ */


/* ========================================================================== */
/* Data types */

struct http_uri_data
{
   const char*  authority;  /* The DNS name or IP address of the HTTP server */
   const char*  path;       /* The path to the target file on the server */
};


struct http_status
{
   char  digit[3];  /* The 3 single digits of the status code */
   int  code;       /* The whole status code as single number */
};


/* ========================================================================== */
/* Constants */

/*! \brief Permissions for downloaded files that are created by this module */
#define HTTP_PERM  (posix_mode_t) (POSIX_S_IRUSR | POSIX_S_IWUSR)


/* ========================================================================== */
/* Create, open and lock local target file */

static int  http_prepare_local_file(int*  fd, const char*  lpn)
{
   int  res = -1;

   res = fu_open_file(lpn, fd, POSIX_O_RDWR | POSIX_O_CREAT| POSIX_O_TRUNC,
                      HTTP_PERM);
   if(!res)  { res = fu_lock_file(*fd); }

   /* Check for error */
   if(res)  { print_error("HTTP: Unable to create and lock target file"); }

   return(res);
}


/* ========================================================================== */
/* URI data destructor
 *
 * \param[in,out] uri_data  Pointer to structure with URI components
 *
 * \note
 * After all memory is released, \c NULL is written to the pointer pointed to
 * by \e uri_data to indicate that there is no longer data present.
 */

static void  http_uri_data_destructor(struct http_uri_data**  uri_data)
{
   if(NULL != *uri_data)
   {
      posix_free((void*) (*uri_data)->authority);
      posix_free((void*) (*uri_data)->path);
      posix_free((void*) *uri_data);
      *uri_data = NULL;
   }
}


/* ========================================================================== */
/* URI decoder
 *
 * \param[out] uri_data  Pointer to structure with extracted URI components
 * \param[in]  uri       URI of file on WWW server to download
 *
 * \note
 * On success the caller is responsible to release the memory allocated for the
 * URI object. Use the function \ref http_uri_data_destructor() for this
 * purpose.
 *
 * \return
 * - Zero on success
 * - -1 on unspecified error
 * - -2 if URI scheme is not supported
 */

static int  http_uri_data_constructor(struct http_uri_data**  uri_data,
                                      const char*  uri)
{
   int  res = -1;
   const char*  p;
   char*  q;
   char*  t;
   size_t  len;

   *uri_data = (struct http_uri_data*)
               posix_malloc(sizeof(struct http_uri_data));
   if(NULL != *uri_data)
   {
      (*uri_data)->authority = NULL;
      (*uri_data)->path = NULL;
      /* Check scheme */
      if(strncmp(uri, "http://", 7))  { res = -2; }
      else
      {
         /* Extract authority */
         p = &uri[7];
         q = strchr(p, (int) '/');
         if(NULL != q)
         {
            len = (size_t) (q - p);
            t = (char*) posix_malloc(len + (size_t) 1);
            if(NULL != t)
            {
               strncpy(t, p, len);
               t[len] = 0;
               (*uri_data)->authority = t;
               /* Extract path */
               len = strlen(q);
               t = (char*) posix_malloc(len + (size_t) 1);
               if(NULL != t)
               {
                  strncpy(t, q, len);
                  t[len] = 0;
                  (*uri_data)->path = t;
                  res = 0;
               }
            }
         }
      }
   }

   /* Check for error */
   if(res)
   {
      print_error("HTTP: Decoding of URI failed");
      http_uri_data_destructor(uri_data);
   }

   return(res);
}


/* ========================================================================== */
/* Connect to HTTP service of host
 *
 * \param[out] sd         Pointer to socket descriptor
 * \param[in]  authority  URI of file on WWW server to download
 *
 * \note
 * The value \c -1 is written to \e sd on error.
 *
 * \return
 * - Zero on success (the socket descriptor was written to \e sd )
 * - -1 on unspecified error
 * - -3 if authority of URI is not reachable
 */

static int  http_connect(int*  sd, const char*  authority)
{
   int  res = 0;
   int  af = POSIX_AF_UNSPEC;
   const char*  host = authority;
   const char*  service = "www";
   const char*  p;
   char*  q = NULL;
   size_t  len;
   size_t  i;
   int  connection_failed = 0;

   /* Prepare for error */
   *sd = -1;

   /* Extract host and service from authority */
   p = strchr(authority, (int) ':');
   if(NULL != p)
   {
      i = (size_t) (p - authority);
      len = strlen(authority);
      q = (char*) posix_malloc(len + (size_t) 1);
      if(NULL == q)  { res = -1; }
      else
      {
         strncpy(q, authority, len);
         q[len] = 0;
         host = q;
         q[i] = 0;
         service = &q[++i];
      }
   }

   /* Open connection */
   if(!res)
   {
#if 0
      /* For debugging */
      printf("----------------------\n");
      printf("Host   : %s\n", host);
      printf("Service: %s\n", service);
      printf("----------------------\n");
#endif
      if(!res)
      {
         res = inet_connect(sd, &af, host, service);
         if(INET_ERR_CONN == res)  { connection_failed = 1; }
      }
   }

   /* Check for error */
   if(res)
   {
      print_error("HTTP: Cannot connect to server");
      inet_close(sd);
      if(connection_failed)  { res = -3; }
      else  { res = -1; }
   }

   if(NULL != q)  { posix_free((void*) q); }

   return(res);
}


/* ========================================================================== */
/* Check for transfer encoding in header of response
 *
 * \param[in] rh  HTTP response header
 *
 * \return
 * - Zero if no transfer encoding was detected
 * - 1 if unsupported transfer encoding was detected
 * - 2 if \c chunked transfer encoding was detected
 * - -1 on error
 */

static int  http_te_check(char*  resp_header)
{
   int  res = 0;
   posix_locale_t  lcp = 0;
   const char*  te_hfn = "Transfer-Encoding:";
   size_t  te_hfn_len = strlen(te_hfn);
   const char*  te_chunked = "chunked";
   size_t  te_chunked_len = strlen(te_chunked);
   char*  rh = resp_header;
   char*  p;
   size_t  i;

   /* Create a locale object with LC_CTYPE == POSIX */
   lcp = posix_newlocale(POSIX_LC_CTYPE_MASK, "POSIX", (posix_locale_t) 0);
   if((posix_locale_t) 0 == lcp)
   {
      print_error("HTTP: Cannot create locale object");
      res = -1;
   }

   /* Search for "Transfer-Encoding" header field */
   if(!res)
   {
      do
      {
         p = strchr(rh, 0x0A);
         if(NULL != p)
         {
            rh = p + 1;
            if(!posix_strncasecmp_l(rh, te_hfn, te_hfn_len, lcp))
            {
               /* "Transfer-Encoding" header field found */
               i = te_hfn_len;
               while(0x09 == (int) rh[i] || 0x20 == (int) rh[i])  { ++i; }
               if(!posix_strncasecmp_l(&rh[i], te_chunked, te_chunked_len, lcp))
               {
                  /*
                   * The "chunked" transfer encoding is defined as mandatory
                   * for HTTP/1.1 by RFC7230
                   */
                  printf("%s: HTTP: Response has chunked transfer encoding\n",
                         CFG_NAME);
                  res = 2;
               }
               else
               {
                  print_error("HTTP: Transfer encoding not supported");
                  res = 1;
               }
            }
         }
      }
      while(NULL != p);
   }

   /* Release memory allocated for locale object */
   if((posix_locale_t) 0 != lcp)  { posix_freelocale(lcp); }

   return(res);
}


/* ========================================================================== */
/* Add line to memory buffer
 *
 * \param[in.out] buf   Data buffer
 * \param[in,out] l     Pointer to size of \e buf
 * \param[in]     line  Line that should be added to \e buf
 *
 * \return
 * - Zero on success (data was written to \e buf )
 * - -1 if memory allocation failed
 */

static int  http_add_to_buffer(char**  buf, size_t*  l, const char*  line)
{
   int  res = -1;
   size_t  len;

   len = strlen(line);
   *buf = posix_realloc(*buf, *l + len);
   if(NULL == buf)
   {
      print_error("HTTP: Out of memory while adding data to buffer");
   }
   else
   {
      strcpy(&(*buf)[*l - (size_t) 1], line);
      *l += len;
      res = 0;
   }

   return(res);
}


/* ========================================================================== */
/* Send GET request
 *
 * \param[in] sd        Network socket descriptor
 * \param[in] uri_data  Data extracted from URI
 *
 * \return
 * - Zero on success
 * - -1 on unspecified error
 */

static int  http_get_tx(int  sd, struct http_uri_data*  uri_data)
{
   int  res = -1;
   int  rv;
   const size_t  ll = 8192;  /* At least 8000 octets recommended by RFC7230 */
   char  line[ll];
   size_t  l = 1;  /* One byte for string termination */
   char*  buf = NULL;
   size_t  len;
   size_t  i;
   ssize_t  rv2;

   /* Prepare GET request */
   rv = posix_snprintf(line, ll, "GET %s HTTP/1.1\r\n", uri_data->path);
   if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
   {
      res = http_add_to_buffer(&buf, &l, line);
   }
   if(!res)
   {
      res = -1;
      rv = posix_snprintf(line, ll, "Host: %s\r\n", uri_data->authority);
      if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
      {
         res = http_add_to_buffer(&buf, &l, line);
      }
   }
   if(!res)
   {
      res = -1;
      rv = posix_snprintf(line, ll, "Connection: close\r\n");
      if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
      {
         res = http_add_to_buffer(&buf, &l, line);
      }
   }
   if(!res)
   {
      res = -1;
      /* Accept cached results if they are not too old */
      rv = posix_snprintf(line, ll, "Cache-Control: max-age=1800\r\n");
      if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
      {
         res = http_add_to_buffer(&buf, &l, line);
      }
   }
#if 1  /* Disable this field for general use (download of non-CRL content) */
   if(!res)
   {
      res = -1;
      /*
       * Request for X.509 CRL content only
       * The IANA registered MIME type 'application/pkix-crl' (specified by
       * RFC2585 chapter 4) is preferred for the response.
       * Legacy Netscape MIME types are accepted too (for backward
       * compatibility) but associated with lower quality factors.
       */
      rv = posix_snprintf(line, ll, "Accept: application/pkix-crl, "
                                    "application/pkcs7-crl;q=0.8, "
                                    "application/x-pkcs7-crl;q=0.2\r\n");
      if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
      {
         res = http_add_to_buffer(&buf, &l, line);
      }
   }
#endif
   if(!res)
   {
      res = -1;
      rv = posix_snprintf(line, ll, "Accept-Encoding: identity\r\n");
      if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
      {
         res = http_add_to_buffer(&buf, &l, line);
      }
   }
   if(!res)
   {
      res = -1;
      rv = posix_snprintf(line, ll, "User-Agent: %s/%s\r\n", CFG_NAME,
                          CFG_VERSION);
      if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
      {
         res = http_add_to_buffer(&buf, &l, line);
      }
   }
   if(!res)
   {
      res = -1;
      rv = posix_snprintf(line, ll, "\r\n");
      if(0 <= rv && ll - (size_t) 1 > (size_t) rv)
      {
         res = http_add_to_buffer(&buf, &l, line);
      }
   }
#if 0
   /* For debugging (print header of request) */
   printf("----------------------\n");
   if(!res)  { printf("%s", buf); }
   printf("----------------------\n");
#endif

   /* Send GET request */
   if(!res)
   {
      len = l - (size_t) 1;
      i = 0;
      while(i < len)
      {
         do  { rv2 = posix_send(sd, &buf[i], len - i, 0); }
         while((ssize_t) -1 == rv2 && POSIX_EINTR == posix_errno);
         if((ssize_t) -1 == rv2)
         {
            print_error("HTTP: Failed to send GET request");
            res = -1;
            break;
         }
         i += (size_t) rv2;
      }
   }

   /* Release memory for request buffer */
   posix_free((void*) buf);

   return(res);
}


/* ========================================================================== */
/* Receive header of response of GET request
 *
 * \param[out] sc   Status information
 * \param[in]  sd   Network socket descriptor
 * \param[out] cte  Pointer to flag indicating "chunked" transfer encoding
 *
 * On success, the value 1 is written to \e cte if \c chunked transfer encoding
 * was detected.
 *
 * \return
 * - Zero on success
 * - -1 on unspecified error
 */

static int  http_get_rx_header(struct http_status*  sc, int  sd, int*  cte)
{
   int  res = 0;
   int  eoh = 0;  /* End of header flag */
   char*  resp = NULL;  /* Response data buffer */
   size_t  len = 512;  /* Length of buffer at address 'resp' */
   size_t  ri = 0;  /* Index for received data */
   size_t  pi;  /* Index for peek (look ahead) data */
   posix_ssize_t  rv;
   size_t  i;
   char*  p;
   size_t  lines = 0;
   int  df[3] = { 100, 10, 1 };  /* Decimal digit factors */

   /*
    * Note:
    * The line lengths inside the header may be arbitrary.
    * We allocate 512Byte at the beginning and expontially increase the size
    * if required.
    */
   resp = (char*) posix_malloc(len);
   if(NULL == resp)
   {
      print_error("HTTP: Out of memory while receiving response");
      res = -1;
   }

   /* Receive response and write it to local file */
   while(!res && !eoh)
   {
      while(!res && !eoh && len > ri)
      {
         /* Peek into next block of input */
         pi = ri;
         do  { rv = posix_recv(sd, &resp[ri], len - ri, POSIX_MSG_PEEK); }
         while(-1 == rv && POSIX_EINTR == posix_errno);
         if(-1 == rv)
         {
            print_error("HTTP: Reading from socket failed");
            res = -1;
            break;
         }
         pi += (size_t) rv;
         /* 'pi' now points behind the last character */
         /* Check for EOH (End Of Header) */
         i = ri;
         while(pi > i)
         {
            /* Check for EOL */
            if('\n' == resp[i])
            {
               ++lines;
               if(1 == i && '\r' == resp[i - 1])
               {
                  /* Empty header */
                  eoh = 1;
                  res = -1;
                  break;
               }
               else if(3 <= i)
               {
                  if('\r' == resp[i - 1]
                     && '\n' == resp[i - 2] && '\r' == resp[i - 3])
                  {
                     eoh = 1;
                     break;
                  }
               }
            }
            ++i;
         }
         if(eoh)
         {
            ++i;
            --lines;
         }
         /* 'i' now points behind last character of empty separation line */
         /* Read next block of input (up to but not including index 'i') */
         while(i > ri)
         {
            do  { rv = posix_recv(sd, &resp[ri], i - ri, 0); }
            while(-1 == rv && POSIX_EINTR == posix_errno);
            if(-1 == rv)
            {
               print_error("HTTP: Reading from socket failed");
               res = -1;
               break;
            }
            ri += (size_t) rv;
         }
         /* 'ri' now points behind the last character of ESL */
      }
      /* Allocate more memory if required */
      if(!res && !eoh)
      {
         len *= 2;
         p = (char*) posix_realloc(resp, len);
         if(NULL == p)
         {
            print_error("HTTP: Out of memory while receiving response");
            res = -1;
         }
         else  { resp = p; }
      }
   }
   if(!res && !eoh)  { res = -1; }
   if(!res)
   {
      /* Write zero termination to header buffer */
      len = ri;
      resp[len] = 0;
#if 0
      /* For debugging (print header of received response) */
      printf("----------------------\n");
      if(!res)  { printf("%s", resp); }
      printf("----------------------\n");
#endif
      /* Check for transfer encoding */
      *cte = 0;
      res = http_te_check(resp);
      if(res)
      {
         if(2 == res)
         {
            /* Support for chunked transfer encoding is mandatory */
            *cte = 1;
            res = 0;
         }
         else  { res = -1; }
      }
      if(!res)
      {
         /* Extract status code */
         p = strchr(resp, (int) ' ');
         if(NULL == p)  { res = -1; }
         else
         {
            if((size_t) 3 > strlen(++p))  { res = -1; }
            else
            {
               sc->code = 0;
               for(i = 0; i < 3; ++i)
               {
                  res = enc_ascii_check_digit(&p[i]);
                  if(res)  { break; }
                  sc->digit[i] = p[i] - (char) 48;
                  sc->code += df[i] * (int) (sc->digit[i]);
               }
            }
         }
      }
   }
   posix_free((void*) resp);

   return(res);
}


/* ========================================================================== */
/* Receive header of chunk and extract chunk size
 *
 * \param[out] cs  Chunk size
 * \param[in]  sd  Network socket descriptor
 *
 * \return
 * - Zero on success (chunk size was written to \e cs )
 * - -1 on unspecified error
 */

static int  http_get_chunk_size(size_t*  cs, int  sd)
{
   int  res = 0;
   const size_t  ll = 8192;  /* At least 8000 octets recommended by RFC7230 */
   char  line[ll];
   posix_ssize_t  rv;
   size_t  i = 0;
   unsigned long int  value;

   /* Read first line of chunk */
   do
   {
      if(ll <= i)  { res = -1;  break; }
      do  { rv = posix_recv(sd, &line[i], 1, 0); }
      while(-1 == rv && POSIX_EINTR == posix_errno);
      if(0 > rv)
      {
         res = -1;
         break;
      }
   }
   while(0x0A != (int) line[i++]);

   /* Extract chunk size */
   if(!res)
   {
      res = sscanf(line, "%lx", &value);
      if(1 != res)  { res = -1; }
      else
      {
         if((unsigned long int) POSIX_UINT_MAX < value)  { res = -1; }
         else
         {
            *cs = (size_t) value;
            res = 0;
         }
      }
   }

   /* Check for error */
   if(res)  { print_error("HTTP: Reading chunk size failed"); }

   return(res);
}


/* ========================================================================== */
/* Receive response of GET request
 *
 * \param[in] fd   Local target file descriptor
 * \param[in] sd   Network socket descriptor
 * \param[in] cte  Flag indicating "chunked" transfer encoding
 *
 * The data received from socket \e sd are written to the file \e fd .
 *
 * \return
 * - Zero on success (data was written to \e fs )
 * - -1 on unspecified error
 */

static int  http_get_rx_body(int  fd, int  sd, int  cte)
{
   int  res = 0;
   posix_ssize_t  rv;
   char*  chunk = NULL;
   size_t  chunk_size = 4096;
   size_t  len;
   char*  p;
   char  crlf[2];

   if(!cte)
   {
      /* No transfer encoding */
      chunk = (char*) posix_malloc(chunk_size);
      if(NULL == chunk)  { res = -1; }
      while(!res)
      {
         /* Read data from socket */
         do  { rv = posix_recv(sd, chunk, chunk_size, 0); }
         while(-1 == rv && POSIX_EINTR == posix_errno);
         if(0 > rv)
         {
            print_error("HTTP: Reading from socket failed");
            res = -1;
            break;
         }
         len = (size_t) rv;
         /* Check for EOD */
         if(!len)  { break; }
         /* Write data to local file */
         res = fu_write_to_filedesc(fd, chunk, len);
         if(0 > res)  { res = -1; }
      }
   }
   else
   {
      /* Decode "chunked" transfer encoding */
      do
      {
         /* Read header of next chunk */
         if(http_get_chunk_size(&chunk_size, sd))  { res = -1; }
         else if(chunk_size)
         {
            /* Reallocate buffer for next chunk */
            p = (char*) posix_realloc(chunk, chunk_size);
            if(NULL == p)  { res = -1; }
            else
            {
               /* Read data of next chunk */
               chunk = p;
               len = 0;
               while(chunk_size > len)
               {
                  /* Read data from socket */
                  do  { rv = posix_recv(sd, &chunk[len], chunk_size - len, 0); }
                  while(-1 == rv && POSIX_EINTR == posix_errno);
                  if(0 >= rv)
                  {
                     print_error("HTTP: Reading from socket failed");
                     res = -1;
                     break;
                  }
                  len += (size_t) rv;
               }
               /* Write data to local file */
               if(!res)
               {
                  res = fu_write_to_filedesc(fd, chunk, chunk_size);
                  if(0 > res)  { res = -1; }
               }
            }
            /* Read trailing CR+LF of chunk */
            if(!res)
            {
               len = 0;
               while(2 > len)
               {
                  do  { rv = posix_recv(sd, &crlf[len], 2 - len, 0); }
                  while(-1 == rv && POSIX_EINTR == posix_errno);
                  if(0 >= rv)
                  {
                     print_error("HTTP: Reading from socket failed");
                     res = -1;
                     break;
                  }
                  len += (size_t) rv;
               }
               if(!res && (0x0D != (int) crlf[0] || 0x0A != crlf[1]))
               {
                  print_error("HTTP: Invalid end of chunk");
                  res = -1;
               }
            }
         }
      }
      while(!res && chunk_size);
   }

   /* Release memory allocated for buffer */
   posix_free((void*) chunk);

   return(res);
}


/* ========================================================================== */
/*! \brief Download file from WWW via HTTP
 *
 * \param[in] uri  URI of file on WWW server to download
 * \param[in] lpn  Local pathname where the file should be stored
 *
 * \attention
 * The caller is responsible to verify that \e uri is a RFC3986 compliant URI.
 *
 * \attention
 * The caller is responsible that write permission to \e lpn is granted. If the
 * file already exist, it is overwritten and truncated.
 *
 * \return
 * - Zero on success (file is now stored at \e lpn )
 * - -1 on unspecified error
 * - -2 if URI scheme is not supported
 * - -3 if authority of URI is not reachable
 * - -4 if requested file not available via path of URI
 */

int  http_download_file(const char*  uri, const char*  lpn)
{
   int  res;
   int  fd = -1;
   struct http_uri_data*  uri_data = NULL;
   int  sd = -1;
   struct http_status  sc;
   int  fc = 0;  /* "file created" flag */
   int  cte = 0;  /* Flag indicating "chunked" transfer encoding */

   /* Init status code */
   sc.digit[0] = '5';
   sc.digit[1] = '0';
   sc.digit[2] = '0';
   sc.code = 500;

   /* Prepare local target file */
   res = http_prepare_local_file(&fd, lpn);
   if(!res)  { fc = 1; }

   /* Connect to server */
   if(!res)  { res = http_uri_data_constructor(&uri_data, uri); }
   if(!res)  { res = http_connect(&sd, uri_data->authority); }

   /* HTTP protocol handling */
   if(!res)  { res = http_get_tx(sd, uri_data); }
   if(!res)  { res = http_get_rx_header(&sc, sd, &cte); }
   if(!res)
   {
      /* Check header of response */
      if(2 != (int) sc.digit[0])
      {
         res = -1;
         if(5 == (int) sc.digit[0])
         {
            print_error("HTTP: Server reported error status");
         }
         if(404 == sc.code)  { res = -4; }
      }
   }
   if(!res)  { res = http_get_rx_body(fd, sd, cte); }

   /* Destroy socket (if present) */
   inet_close(&sd);

   /* Destroy URI data (if present) */
   http_uri_data_destructor(&uri_data);

   /* Close local target file (if present) */
   fu_close_file(&fd, NULL);

   /* Check for error */
   if(res)
   {
      print_error("HTTP: Download failed");
      if(fc)
      {
         /* Unlink local target file */
         if(fu_unlink_file(lpn))
         {
            print_error("HTTP: Unlinking target file failed");
         }
      }
   }
   else  { printf("%s: HTTP: File successfully downloaded\n", CFG_NAME); }

   return(res);
}


/* @} */

/* EOF */
