/*
 *  Copyright (C) 2009-2010 Howard Chu
 *
 *  This file is part of librtmp.
 *
 *  librtmp is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation; either version 2.1,
 *  or (at your option) any later version.
 *
 *  librtmp 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 Lesser General Public License
 *  along with librtmp see the file COPYING.  If not, write to
 *  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *  Boston, MA  02110-1301, USA.
 *  http://www.gnu.org/copyleft/lgpl.html
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

#include "rtmp_sys.h"
#include "log.h"
#include "http.h"

#ifdef CRYPTO
#ifdef USE_POLARSSL
#include <polarssl/sha2.h>
#ifndef SHA256_DIGEST_LENGTH
#define SHA256_DIGEST_LENGTH	32
#endif
#define HMAC_CTX	sha2_context
#define HMAC_setup(ctx, key, len)	sha2_hmac_starts(&ctx, (unsigned char *)key, len, 0)
#define HMAC_crunch(ctx, buf, len)	sha2_hmac_update(&ctx, buf, len)
#define HMAC_finish(ctx, dig, dlen)	dlen = SHA256_DIGEST_LENGTH; sha2_hmac_finish(&ctx, dig)
#define HMAC_close(ctx)
#elif defined(USE_GNUTLS)
#include <gnutls/gnutls.h>
#include <gcrypt.h>
#ifndef SHA256_DIGEST_LENGTH
#define SHA256_DIGEST_LENGTH	32
#endif
#define HMAC_CTX	gcry_md_hd_t
#define HMAC_setup(ctx, key, len)	gcry_md_open(&ctx, GCRY_MD_SHA256, GCRY_MD_FLAG_HMAC); gcry_md_setkey(ctx, key, len)
#define HMAC_crunch(ctx, buf, len)	gcry_md_write(ctx, buf, len)
#define HMAC_finish(ctx, dig, dlen)	dlen = SHA256_DIGEST_LENGTH; memcpy(dig, gcry_md_read(ctx, 0), dlen)
#define HMAC_close(ctx)	gcry_md_close(ctx)
#else	/* USE_OPENSSL */
#include <openssl/ssl.h>
#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <openssl/rc4.h>
#define HMAC_setup(ctx, key, len)	HMAC_CTX_init(&ctx); HMAC_Init_ex(&ctx, (unsigned char *)key, len, EVP_sha256(), 0)
#define HMAC_crunch(ctx, buf, len)	HMAC_Update(&ctx, (unsigned char *)buf, len)
#define HMAC_finish(ctx, dig, dlen)	HMAC_Final(&ctx, (unsigned char *)dig, &dlen);
#define HMAC_close(ctx)	HMAC_CTX_cleanup(&ctx)
#endif

extern void RTMP_TLS_Init();
extern TLS_CTX RTMP_TLS_ctx;

#include <zlib.h>

#endif /* CRYPTO */

#define	AGENT	"Mozilla/5.0 (Windows NT 5.1; rv:8.0) Gecko/20100101 Firefox/8.0"

HTTPResult
HTTP_get(struct HTTP_ctx *http, const char *url, HTTP_read_callback *cb)
{
  char *host, *path;
  char *p1, *p2;
  char hbuf[256];
  int port = 80;
#ifdef CRYPTO
  int ssl = 0;
#endif
  int hlen, flen = 0;
  int rc, i;
  int len_known;
  HTTPResult ret = HTTPRES_OK;
  struct sockaddr_in sa;
  RTMPSockBuf sb = {0};

  http->status = -1;

  memset(&sa, 0, sizeof(struct sockaddr_in));
  sa.sin_family = AF_INET;

  /* we only handle http here */
  if (strncasecmp(url, "http", 4))
    return HTTPRES_BAD_REQUEST;

  if (url[4] == 's')
    {
#ifdef CRYPTO
      ssl = 1;
      port = 443;
      if (!RTMP_TLS_ctx)
	RTMP_TLS_Init();
#else
      return HTTPRES_BAD_REQUEST;
#endif
    }

  p1 = strchr(url + 4, ':');
  if (!p1 || strncmp(p1, "://", 3))
    return HTTPRES_BAD_REQUEST;

  host = p1 + 3;
  path = strchr(host, '/');
  hlen = path - host;
  strncpy(hbuf, host, hlen);
  hbuf[hlen] = '\0';
  host = hbuf;
  p1 = strrchr(host, ':');
  if (p1)
    {
      *p1++ = '\0';
      port = atoi(p1);
    }

  sa.sin_addr.s_addr = inet_addr(host);
  if (sa.sin_addr.s_addr == INADDR_NONE)
    {
      struct hostent *hp = gethostbyname(host);
      if (!hp || !hp->h_addr)
	return HTTPRES_LOST_CONNECTION;
      sa.sin_addr = *(struct in_addr *)hp->h_addr;
    }
  sa.sin_port = htons(port);
  sb.sb_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (sb.sb_socket == -1)
    return HTTPRES_LOST_CONNECTION;
  i =
    sprintf(sb.sb_buf,
	    "GET %s HTTP/1.0\r\nUser-Agent: %s\r\nHost: %s\r\nReferer: %.*s\r\n",
	    path, AGENT, host, (int)(path - url + 1), url);
  if (http->date[0])
    i += sprintf(sb.sb_buf + i, "If-Modified-Since: %s\r\n", http->date);
  i += sprintf(sb.sb_buf + i, "\r\n");

  if (connect
      (sb.sb_socket, (struct sockaddr *)&sa, sizeof(struct sockaddr)) < 0)
    {
      ret = HTTPRES_LOST_CONNECTION;
      goto leave;
    }
#ifdef CRYPTO
  if (ssl)
    {
#ifdef NO_SSL
      RTMP_Log(RTMP_LOGERROR, "%s, No SSL/TLS support", __FUNCTION__);
      ret = HTTPRES_BAD_REQUEST;
      goto leave;
#else
      TLS_client(RTMP_TLS_ctx, sb.sb_ssl);
      TLS_setfd(sb.sb_ssl, sb.sb_socket);
      if (TLS_connect(sb.sb_ssl) < 0)
	{
	  RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed", __FUNCTION__);
	  ret = HTTPRES_LOST_CONNECTION;
	  goto leave;
	}
#endif
    }
#endif
  RTMPSockBuf_Send(&sb, sb.sb_buf, i);

  /* set timeout */
#define HTTP_TIMEOUT	5
  {
    SET_RCVTIMEO(tv, HTTP_TIMEOUT);
    if (setsockopt
        (sb.sb_socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)))
      {
        RTMP_Log(RTMP_LOGERROR, "%s, Setting socket timeout to %ds failed!",
	    __FUNCTION__, HTTP_TIMEOUT);
      }
  }

  sb.sb_size = 0;
  sb.sb_timedout = FALSE;
  if (RTMPSockBuf_Fill(&sb) < 1)
    {
      ret = HTTPRES_LOST_CONNECTION;
      goto leave;
    }
  if (strncmp(sb.sb_buf, "HTTP/1", 6))
    {
      ret = HTTPRES_BAD_REQUEST;
      goto leave;
    }

  p1 = strchr(sb.sb_buf, ' ');
  rc = atoi(p1 + 1);
  http->status = rc;

  if (rc >= 300)
    {
      if (rc == 304)
	{
	  ret = HTTPRES_OK_NOT_MODIFIED;
	  goto leave;
	}
      else if (rc == 404)
	ret = HTTPRES_NOT_FOUND;
      else if (rc >= 500)
	ret = HTTPRES_SERVER_ERROR;
      else if (rc >= 400)
	ret = HTTPRES_BAD_REQUEST;
      else
	ret = HTTPRES_REDIRECTED;
    }

  p1 = memchr(sb.sb_buf, '\n', sb.sb_size);
  if (!p1)
    {
      ret = HTTPRES_BAD_REQUEST;
      goto leave;
    }
  sb.sb_start = p1 + 1;
  sb.sb_size -= sb.sb_start - sb.sb_buf;

  while ((p2 = memchr(sb.sb_start, '\r', sb.sb_size)))
    {
      if (*sb.sb_start == '\r')
	{
	  sb.sb_start += 2;
	  sb.sb_size -= 2;
	  break;
	}
      else
	if (!strncasecmp
	    (sb.sb_start, "Content-Length: ", sizeof("Content-Length: ") - 1))
	{
	  flen = atoi(sb.sb_start + sizeof("Content-Length: ") - 1);
	}
      else
	if (!strncasecmp
	    (sb.sb_start, "Last-Modified: ", sizeof("Last-Modified: ") - 1))
	{
	  *p2 = '\0';
	  strcpy(http->date, sb.sb_start + sizeof("Last-Modified: ") - 1);
	}
      p2 += 2;
      sb.sb_size -= p2 - sb.sb_start;
      sb.sb_start = p2;
      if (sb.sb_size < 1)
	{
	  if (RTMPSockBuf_Fill(&sb) < 1)
	    {
	      ret = HTTPRES_LOST_CONNECTION;
	      goto leave;
	    }
	}
    }

  len_known = flen > 0;
  while ((!len_known || flen > 0) &&
	 (sb.sb_size > 0 || RTMPSockBuf_Fill(&sb) > 0))
    {
      cb(sb.sb_start, 1, sb.sb_size, http->data);
      if (len_known)
	flen -= sb.sb_size;
      http->size += sb.sb_size;
      sb.sb_size = 0;
    }

  if (flen > 0)
    ret = HTTPRES_LOST_CONNECTION;

leave:
  RTMPSockBuf_Close(&sb);
  return ret;
}

#ifdef CRYPTO

#define CHUNK	16384

struct info
{
  z_stream *zs;
  HMAC_CTX ctx;
  int first;
  int zlib;
  int size;
};

static size_t
swfcrunch(void *ptr, size_t size, size_t nmemb, void *stream)
{
  struct info *i = stream;
  char *p = ptr;
  size_t len = size * nmemb;

  if (i->first)
    {
      i->first = 0;
      /* compressed? */
      if (!strncmp(p, "CWS", 3))
	{
	  *p = 'F';
	  i->zlib = 1;
	}
      HMAC_crunch(i->ctx, (unsigned char *)p, 8);
      p += 8;
      len -= 8;
      i->size = 8;
    }

  if (i->zlib)
    {
      unsigned char out[CHUNK];
      i->zs->next_in = (unsigned char *)p;
      i->zs->avail_in = len;
      do
	{
	  i->zs->avail_out = CHUNK;
	  i->zs->next_out = out;
	  inflate(i->zs, Z_NO_FLUSH);
	  len = CHUNK - i->zs->avail_out;
	  i->size += len;
	  HMAC_crunch(i->ctx, out, len);
	}
      while (i->zs->avail_out == 0);
    }
  else
    {
      i->size += len;
      HMAC_crunch(i->ctx, (unsigned char *)p, len);
    }
  return size * nmemb;
}

static int tzoff;
static int tzchecked;

#define	JAN02_1980	318340800

static const char *monthtab[12] = { "Jan", "Feb", "Mar",
  "Apr", "May", "Jun",
  "Jul", "Aug", "Sep",
  "Oct", "Nov", "Dec"
};
static const char *days[] =
  { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

/* Parse an HTTP datestamp into Unix time */
static time_t
make_unix_time(char *s)
{
  struct tm time;
  int i, ysub = 1900, fmt = 0;
  char *month;
  char *n;
  time_t res;

  if (s[3] != ' ')
    {
      fmt = 1;
      if (s[3] != ',')
	ysub = 0;
    }
  for (n = s; *n; ++n)
    if (*n == '-' || *n == ':')
      *n = ' ';

  time.tm_mon = 0;
  n = strchr(s, ' ');
  if (fmt)
    {
      /* Day, DD-MMM-YYYY HH:MM:SS GMT */
      time.tm_mday = strtol(n + 1, &n, 0);
      month = n + 1;
      n = strchr(month, ' ');
      time.tm_year = strtol(n + 1, &n, 0);
      time.tm_hour = strtol(n + 1, &n, 0);
      time.tm_min = strtol(n + 1, &n, 0);
      time.tm_sec = strtol(n + 1, NULL, 0);
    }
  else
    {
      /* Unix ctime() format. Does not conform to HTTP spec. */
      /* Day MMM DD HH:MM:SS YYYY */
      month = n + 1;
      n = strchr(month, ' ');
      while (isspace(*n))
	n++;
      time.tm_mday = strtol(n, &n, 0);
      time.tm_hour = strtol(n + 1, &n, 0);
      time.tm_min = strtol(n + 1, &n, 0);
      time.tm_sec = strtol(n + 1, &n, 0);
      time.tm_year = strtol(n + 1, NULL, 0);
    }
  if (time.tm_year > 100)
    time.tm_year -= ysub;

  for (i = 0; i < 12; i++)
    if (!strncasecmp(month, monthtab[i], 3))
      {
	time.tm_mon = i;
	break;
      }
  time.tm_isdst = 0;		/* daylight saving is never in effect in GMT */

  /* this is normally the value of extern int timezone, but some
   * braindead C libraries don't provide it.
   */
  if (!tzchecked)
    {
      struct tm *tc;
      time_t then = JAN02_1980;
      tc = localtime(&then);
      tzoff = (12 - tc->tm_hour) * 3600 + tc->tm_min * 60 + tc->tm_sec;
      tzchecked = 1;
    }
  res = mktime(&time);
  /* Unfortunately, mktime() assumes the input is in local time,
   * not GMT, so we have to correct it here.
   */
  if (res != -1)
    res += tzoff;
  return res;
}

/* Convert a Unix time to a network time string
 * Weekday, DD-MMM-YYYY HH:MM:SS GMT
 */
static void
strtime(time_t * t, char *s)
{
  struct tm *tm;

  tm = gmtime((time_t *) t);
  sprintf(s, "%s, %02d %s %d %02d:%02d:%02d GMT",
	  days[tm->tm_wday], tm->tm_mday, monthtab[tm->tm_mon],
	  tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec);
}

#define HEX2BIN(a)      (((a)&0x40)?((a)&0xf)+9:((a)&0xf))

int
RTMP_HashSWF(const char *url, unsigned int *size, unsigned char *hash,
	     int age)
{
  FILE *f = NULL;
  char *path, date[64], cctim[64];
  long pos = 0;
  time_t ctim = -1, cnow;
  int i, got = 0, ret = 0;
  unsigned int hlen;
  struct info in = { 0 };
  struct HTTP_ctx http = { 0 };
  HTTPResult httpres;
  z_stream zs = { 0 };
  AVal home, hpre;

  date[0] = '\0';
#ifdef _WIN32
#ifdef XBMC4XBOX
  hpre.av_val = "Q:";
  hpre.av_len = 2;
  home.av_val = "\\UserData";
#else
  hpre.av_val = getenv("HOMEDRIVE");
  hpre.av_len = strlen(hpre.av_val);
  home.av_val = getenv("HOMEPATH");
#endif
#define DIRSEP	"\\"

#else /* !_WIN32 */
  hpre.av_val = "";
  hpre.av_len = 0;
  home.av_val = getenv("HOME");
#define DIRSEP	"/"
#endif
  if (!home.av_val)
    home.av_val = ".";
  home.av_len = strlen(home.av_val);

  /* SWF hash info is cached in a fixed-format file.
   * url: <url of SWF file>
   * ctim: HTTP datestamp of when we last checked it.
   * date: HTTP datestamp of the SWF's last modification.
   * size: SWF size in hex
   * hash: SWF hash in hex
   *
   * These fields must be present in this order. All fields
   * besides URL are fixed size.
   */
  path = malloc(hpre.av_len + home.av_len + sizeof(DIRSEP ".swfinfo"));
  sprintf(path, "%s%s" DIRSEP ".swfinfo", hpre.av_val, home.av_val);

  f = fopen(path, "r+");
  while (f)
    {
      char buf[4096], *file, *p;

      file = strchr(url, '/');
      if (!file)
	break;
      file += 2;
      file = strchr(file, '/');
      if (!file)
	break;
      file++;
      hlen = file - url;
      p = strrchr(file, '/');
      if (p)
	file = p;
      else
	file--;

      while (fgets(buf, sizeof(buf), f))
	{
	  char *r1;

	  got = 0;

	  if (strncmp(buf, "url: ", 5))
	    continue;
	  if (strncmp(buf + 5, url, strlen(buf + 5) - 1))
	    continue;
	  r1 = strrchr(buf, '/');
	  i = strlen(r1);
	  r1[--i] = '\0';
	  if (strncmp(r1, file, i))
	    continue;
	  pos = ftell(f);
	  while (got < 4 && fgets(buf, sizeof(buf), f))
	    {
	      if (!strncmp(buf, "size: ", 6))
		{
		  *size = strtol(buf + 6, NULL, 16);
		  got++;
		}
	      else if (!strncmp(buf, "hash: ", 6))
		{
		  unsigned char *ptr = hash, *in = (unsigned char *)buf + 6;
		  int l = strlen((char *)in) - 1;
		  for (i = 0; i < l; i += 2)
		    *ptr++ = (HEX2BIN(in[i]) << 4) | HEX2BIN(in[i + 1]);
		  got++;
		}
	      else if (!strncmp(buf, "date: ", 6))
		{
		  buf[strlen(buf) - 1] = '\0';
		  strncpy(date, buf + 6, sizeof(date));
		  got++;
		}
	      else if (!strncmp(buf, "ctim: ", 6))
		{
		  buf[strlen(buf) - 1] = '\0';
		  ctim = make_unix_time(buf + 6);
		  got++;
		}
	      else if (!strncmp(buf, "url: ", 5))
		break;
	    }
	  break;
	}
      break;
    }

  cnow = time(NULL);
  /* If we got a cache time, see if it's young enough to use directly */
  if (age && ctim > 0)
    {
      ctim = cnow - ctim;
      ctim /= 3600 * 24;	/* seconds to days */
      if (ctim < age)		/* ok, it's new enough */
	goto out;
    }

  in.first = 1;
  HMAC_setup(in.ctx, "Genuine Adobe Flash Player 001", 30);
  inflateInit(&zs);
  in.zs = &zs;

  http.date = date;
  http.data = &in;

  httpres = HTTP_get(&http, url, swfcrunch);

  inflateEnd(&zs);

  if (httpres != HTTPRES_OK && httpres != HTTPRES_OK_NOT_MODIFIED)
    {
      ret = -1;
      if (httpres == HTTPRES_LOST_CONNECTION)
	RTMP_Log(RTMP_LOGERROR, "%s: connection lost while downloading swfurl %s",
	    __FUNCTION__, url);
      else if (httpres == HTTPRES_NOT_FOUND)
	RTMP_Log(RTMP_LOGERROR, "%s: swfurl %s not found", __FUNCTION__, url);
      else
	RTMP_Log(RTMP_LOGERROR, "%s: couldn't contact swfurl %s (HTTP error %d)",
	    __FUNCTION__, url, http.status);
    }
  else
    {
      if (got && pos)
	fseek(f, pos, SEEK_SET);
      else
	{
	  char *q;
	  if (!f)
	    f = fopen(path, "w");
	  if (!f)
	    {
	      int err = errno;
	      RTMP_Log(RTMP_LOGERROR,
		  "%s: couldn't open %s for writing, errno %d (%s)",
		  __FUNCTION__, path, err, strerror(err));
	      ret = -1;
	      goto out;
	    }
	  fseek(f, 0, SEEK_END);
	  q = strchr(url, '?');
	  if (q)
	    i = q - url;
	  else
	    i = strlen(url);

	  fprintf(f, "url: %.*s\n", i, url);
	}
      strtime(&cnow, cctim);
      fprintf(f, "ctim: %s\n", cctim);

      if (!in.first)
	{
	  HMAC_finish(in.ctx, hash, hlen);
	  *size = in.size;

	  fprintf(f, "date: %s\n", date);
	  fprintf(f, "size: %08x\n", in.size);
	  fprintf(f, "hash: ");
	  for (i = 0; i < SHA256_DIGEST_LENGTH; i++)
	    fprintf(f, "%02x", hash[i]);
	  fprintf(f, "\n");
	}
    }
  HMAC_close(in.ctx);
out:
  free(path);
  if (f)
    fclose(f);
  return ret;
}
#else
int
RTMP_HashSWF(const char *url, unsigned int *size, unsigned char *hash,
	     int age)
{
  return -1;
}
#endif
