/*
 * This file Copyright (C) 2008-2014 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 * $Id: web.c 14644 2015-12-29 19:37:31Z mikedld $
 */

#include <assert.h>
#include <string.h> /* strlen (), strstr () */

#ifdef _WIN32
  #include <ws2tcpip.h>
#else
  #include <sys/select.h>
#endif

#include <curl/curl.h>

#include <event2/buffer.h>

#include "transmission.h"
#include "file.h"
#include "list.h"
#include "log.h"
#include "net.h" /* tr_address */
#include "torrent.h"
#include "platform.h" /* mutex */
#include "session.h"
#include "trevent.h" /* tr_runInEventThread () */
#include "utils.h"
#include "version.h" /* User-Agent */
#include "web.h"

#if LIBCURL_VERSION_NUM >= 0x070F06 /* CURLOPT_SOCKOPT* was added in 7.15.6 */
 #define USE_LIBCURL_SOCKOPT
#endif

enum
{
  THREADFUNC_MAX_SLEEP_MSEC = 200,
};

#if 0
#define dbgmsg(...) \
  do { \
    fprintf (stderr, __VA_ARGS__); \
    fprintf (stderr, "\n"); \
  } while (0)
#else
#define dbgmsg(...) \
  do { \
    if (tr_logGetDeepEnabled ()) \
      tr_logAddDeep (__FILE__, __LINE__, "web", __VA_ARGS__); \
  } while (0)
#endif

/***
****
***/

struct tr_web_task
{
  int torrentId;
  long code;
  long timeout_secs;
  bool did_connect;
  bool did_timeout;
  struct evbuffer * response;
  struct evbuffer * freebuf;
  char * url;
  char * range;
  char * cookies;
  tr_session * session;
  tr_web_done_func done_func;
  void * done_func_user_data;
  CURL * curl_easy;
  struct tr_web_task * next;
};

static void
task_free (struct tr_web_task * task)
{
  if (task->freebuf)
    evbuffer_free (task->freebuf);
  tr_free (task->cookies);
  tr_free (task->range);
  tr_free (task->url);
  tr_free (task);
}

/***
****
***/

static tr_list  * paused_easy_handles = NULL;

struct tr_web
{
  bool curl_verbose;
  bool curl_ssl_verify;
  char * curl_ca_bundle;
  int close_mode;
  struct tr_web_task * tasks;
  tr_lock * taskLock;
  char * cookie_filename;
};

/***
****
***/

static size_t
writeFunc (void * ptr, size_t size, size_t nmemb, void * vtask)
{
  const size_t byteCount = size * nmemb;
  struct tr_web_task * task = vtask;

  /* webseed downloads should be speed limited */
  if (task->torrentId != -1)
    {
      tr_torrent * tor = tr_torrentFindFromId (task->session, task->torrentId);

      if (tor && !tr_bandwidthClamp (&tor->bandwidth, TR_DOWN, nmemb))
        {
          tr_list_append (&paused_easy_handles, task->curl_easy);
          return CURL_WRITEFUNC_PAUSE;
        }
    }

  evbuffer_add (task->response, ptr, byteCount);
  dbgmsg ("wrote %zu bytes to task %p's buffer", byteCount, (void*)task);
  return byteCount;
}

#ifdef USE_LIBCURL_SOCKOPT
static int
sockoptfunction (void * vtask, curl_socket_t fd, curlsocktype purpose UNUSED)
{
  struct tr_web_task * task = vtask;
  const bool isScrape = strstr (task->url, "scrape") != NULL;
  const bool isAnnounce = strstr (task->url, "announce") != NULL;

  /* announce and scrape requests have tiny payloads. */
  if (isScrape || isAnnounce)
    {
      const int sndbuf = isScrape ? 4096 : 1024;
      const int rcvbuf = isScrape ? 4096 : 3072;
      setsockopt (fd, SOL_SOCKET, SO_SNDBUF, (const void *) &sndbuf, sizeof (sndbuf));
      setsockopt (fd, SOL_SOCKET, SO_RCVBUF, (const void *) &rcvbuf, sizeof (rcvbuf));
    }

  /* return nonzero if this function encountered an error */
  return 0;
}
#endif

static long
getTimeoutFromURL (const struct tr_web_task * task)
{
  long timeout;
  const tr_session * session = task->session;

  if (!session || session->isClosed) timeout = 20L;
  else if (strstr (task->url, "scrape") != NULL) timeout = 30L;
  else if (strstr (task->url, "announce") != NULL) timeout = 90L;
  else timeout = 240L;

  return timeout;
}

static CURL *
createEasy (tr_session * s, struct tr_web * web, struct tr_web_task * task)
{
  bool is_default_value;
  const tr_address * addr;
  CURL * e = task->curl_easy = curl_easy_init ();

  task->timeout_secs = getTimeoutFromURL (task);

  curl_easy_setopt (e, CURLOPT_AUTOREFERER, 1L);
  curl_easy_setopt (e, CURLOPT_ENCODING, "gzip;q=1.0, deflate, identity");
  curl_easy_setopt (e, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt (e, CURLOPT_MAXREDIRS, -1L);
  curl_easy_setopt (e, CURLOPT_NOSIGNAL, 1L);
  curl_easy_setopt (e, CURLOPT_PRIVATE, task);
#ifdef USE_LIBCURL_SOCKOPT
  curl_easy_setopt (e, CURLOPT_SOCKOPTFUNCTION, sockoptfunction);
  curl_easy_setopt (e, CURLOPT_SOCKOPTDATA, task);
#endif
  if (web->curl_ssl_verify)
    {
      curl_easy_setopt (e, CURLOPT_CAINFO, web->curl_ca_bundle);
    }
  else
    {
      curl_easy_setopt (e, CURLOPT_SSL_VERIFYHOST, 0L);
      curl_easy_setopt (e, CURLOPT_SSL_VERIFYPEER, 0L);
    }
  curl_easy_setopt (e, CURLOPT_TIMEOUT, task->timeout_secs);
  curl_easy_setopt (e, CURLOPT_URL, task->url);
  curl_easy_setopt (e, CURLOPT_USERAGENT, TR_NAME "/" SHORT_VERSION_STRING);
  curl_easy_setopt (e, CURLOPT_VERBOSE, (long)(web->curl_verbose?1:0));
  curl_easy_setopt (e, CURLOPT_WRITEDATA, task);
  curl_easy_setopt (e, CURLOPT_WRITEFUNCTION, writeFunc);

  if (((addr = tr_sessionGetPublicAddress (s, TR_AF_INET, &is_default_value))) && !is_default_value)
    curl_easy_setopt (e, CURLOPT_INTERFACE, tr_address_to_string (addr));
  else if (((addr = tr_sessionGetPublicAddress (s, TR_AF_INET6, &is_default_value))) && !is_default_value)
    curl_easy_setopt (e, CURLOPT_INTERFACE, tr_address_to_string (addr));

  if (task->cookies != NULL)
    curl_easy_setopt (e, CURLOPT_COOKIE, task->cookies);

  if (web->cookie_filename != NULL)
    curl_easy_setopt (e, CURLOPT_COOKIEFILE, web->cookie_filename);

  if (task->range != NULL)
    {
      curl_easy_setopt (e, CURLOPT_RANGE, task->range);
      /* don't bother asking the server to compress webseed fragments */
      curl_easy_setopt (e, CURLOPT_ENCODING, "identity");
    }

  return e;
}

/***
****
***/

static void
task_finish_func (void * vtask)
{
  struct tr_web_task * task = vtask;
  dbgmsg ("finished web task %p; got %ld", (void*)task, task->code);

  if (task->done_func != NULL)
    task->done_func (task->session,
                     task->did_connect,
                     task->did_timeout,
                     task->code,
                     evbuffer_pullup (task->response, -1),
                     evbuffer_get_length (task->response),
                     task->done_func_user_data);

  task_free (task);
}

/****
*****
****/

static void tr_webThreadFunc (void * vsession);

static struct tr_web_task *
tr_webRunImpl (tr_session         * session,
               int                  torrentId,
               const char         * url,
               const char         * range,
               const char         * cookies,
               tr_web_done_func     done_func,
               void               * done_func_user_data,
               struct evbuffer    * buffer)
{
  struct tr_web_task * task = NULL;

  if (!session->isClosing)
    {
      if (session->web == NULL)
        {
          tr_threadNew (tr_webThreadFunc, session);

          while (session->web == NULL)
            tr_wait_msec (20);
        }

      task = tr_new0 (struct tr_web_task, 1);
      task->session = session;
      task->torrentId = torrentId;
      task->url = tr_strdup (url);
      task->range = tr_strdup (range);
      task->cookies = tr_strdup (cookies);
      task->done_func = done_func;
      task->done_func_user_data = done_func_user_data;
      task->response = buffer ? buffer : evbuffer_new ();
      task->freebuf = buffer ? NULL : task->response;

      tr_lockLock (session->web->taskLock);
      task->next = session->web->tasks;
      session->web->tasks = task;
      tr_lockUnlock (session->web->taskLock);
    }

  return task;
}

struct tr_web_task *
tr_webRunWithCookies (tr_session        * session,
                      const char        * url,
                      const char        * cookies,
                      tr_web_done_func    done_func,
                      void              * done_func_user_data)
{
  return tr_webRunImpl (session, -1, url,
                        NULL, cookies,
                        done_func, done_func_user_data,
                        NULL);
}

struct tr_web_task *
tr_webRun (tr_session         * session,
           const char         * url,
           tr_web_done_func     done_func,
           void               * done_func_user_data)
{
  return tr_webRunWithCookies (session, url, NULL,
                               done_func, done_func_user_data);
}


struct tr_web_task *
tr_webRunWebseed (tr_torrent        * tor,
                  const char        * url,
                  const char        * range,
                  tr_web_done_func    done_func,
                  void              * done_func_user_data,
                  struct evbuffer   * buffer)
{
  return tr_webRunImpl (tor->session, tr_torrentId (tor), url,
                        range, NULL,
                        done_func, done_func_user_data,
                        buffer);
}

/**
 * Portability wrapper for select ().
 *
 * http://msdn.microsoft.com/en-us/library/ms740141%28VS.85%29.aspx
 * On win32, any two of the parameters, readfds, writefds, or exceptfds,
 * can be given as null. At least one must be non-null, and any non-null
 * descriptor set must contain at least one handle to a socket.
 */
static void
tr_select (int nfds,
           fd_set * r_fd_set, fd_set * w_fd_set, fd_set * c_fd_set,
           struct timeval  * t)
{
#ifdef _WIN32
  (void) nfds;

  if (!r_fd_set->fd_count && !w_fd_set->fd_count && !c_fd_set->fd_count)
    {
      const long int msec = t->tv_sec*1000 + t->tv_usec/1000;
      tr_wait_msec (msec);
    }
  else if (select (0, r_fd_set->fd_count ? r_fd_set : NULL,
                      w_fd_set->fd_count ? w_fd_set : NULL,
                      c_fd_set->fd_count ? c_fd_set : NULL, t) < 0)
    {
      char errstr[512];
      const int e = EVUTIL_SOCKET_ERROR ();
      tr_net_strerror (errstr, sizeof (errstr), e);
      dbgmsg ("Error: select (%d) %s", e, errstr);
    }
#else
  select (nfds, r_fd_set, w_fd_set, c_fd_set, t);
#endif
}

static void
tr_webThreadFunc (void * vsession)
{
  char * str;
  CURLM * multi;
  struct tr_web * web;
  int taskCount = 0;
  struct tr_web_task * task;
  tr_session * session = vsession;

  /* try to enable ssl for https support; but if that fails,
   * try a plain vanilla init */
  if (curl_global_init (CURL_GLOBAL_SSL))
    curl_global_init (0);

  web = tr_new0 (struct tr_web, 1);
  web->close_mode = ~0;
  web->taskLock = tr_lockNew ();
  web->tasks = NULL;
  web->curl_verbose = tr_env_key_exists ("TR_CURL_VERBOSE");
  web->curl_ssl_verify = tr_env_key_exists ("TR_CURL_SSL_VERIFY");
  web->curl_ca_bundle = tr_env_get_string ("CURL_CA_BUNDLE", NULL);
  if (web->curl_ssl_verify)
    {
      tr_logAddNamedInfo ("web", "will verify tracker certs using envvar CURL_CA_BUNDLE: %s",
               web->curl_ca_bundle == NULL ? "none" : web->curl_ca_bundle);
      tr_logAddNamedInfo ("web", "NB: this only works if you built against libcurl with openssl or gnutls, NOT nss");
      tr_logAddNamedInfo ("web", "NB: invalid certs will show up as 'Could not connect to tracker' like many other errors");
    }

  str = tr_buildPath (session->configDir, "cookies.txt", NULL);
  if (tr_sys_path_exists (str, NULL))
    web->cookie_filename = tr_strdup (str);
  tr_free (str);

  multi = curl_multi_init ();
  session->web = web;

  for (;;)
    {
      long msec;
      int unused;
      CURLMsg * msg;
      CURLMcode mcode;

      if (web->close_mode == TR_WEB_CLOSE_NOW)
        break;
      if ((web->close_mode == TR_WEB_CLOSE_WHEN_IDLE) && (web->tasks == NULL))
        break;

      /* add tasks from the queue */
      tr_lockLock (web->taskLock);
      while (web->tasks != NULL)
        {
          /* pop the task */
          task = web->tasks;
          web->tasks = task->next;
          task->next = NULL;

          dbgmsg ("adding task to curl: [%s]", task->url);
          curl_multi_add_handle (multi, createEasy (session, web, task));
          /*fprintf (stderr, "adding a task.. taskCount is now %d\n", taskCount);*/
          ++taskCount;
        }
      tr_lockUnlock (web->taskLock);

      /* unpause any paused curl handles */
      if (paused_easy_handles != NULL)
        {
          CURL * handle;
          tr_list * tmp;

          /* swap paused_easy_handles to prevent oscillation
             between writeFunc this while loop */
          tmp = paused_easy_handles;
          paused_easy_handles = NULL;

          while ((handle = tr_list_pop_front (&tmp)))
            curl_easy_pause (handle, CURLPAUSE_CONT);
        }

      /* maybe wait a little while before calling curl_multi_perform () */
      msec = 0;
      curl_multi_timeout (multi, &msec);
      if (msec < 0)
        msec = THREADFUNC_MAX_SLEEP_MSEC;
      if (session->isClosed)
        msec = 100; /* on shutdown, call perform () more frequently */
      if (msec > 0)
        {
          int usec;
          int max_fd;
          struct timeval t;
          fd_set r_fd_set, w_fd_set, c_fd_set;

          max_fd = 0;
          FD_ZERO (&r_fd_set);
          FD_ZERO (&w_fd_set);
          FD_ZERO (&c_fd_set);
          curl_multi_fdset (multi, &r_fd_set, &w_fd_set, &c_fd_set, &max_fd);

          if (msec > THREADFUNC_MAX_SLEEP_MSEC)
            msec = THREADFUNC_MAX_SLEEP_MSEC;

          usec = msec * 1000;
          t.tv_sec =  usec / 1000000;
          t.tv_usec = usec % 1000000;
          tr_select (max_fd+1, &r_fd_set, &w_fd_set, &c_fd_set, &t);
        }

      /* call curl_multi_perform () */
      do
        mcode = curl_multi_perform (multi, &unused);
      while (mcode == CURLM_CALL_MULTI_PERFORM);

      /* pump completed tasks from the multi */
      while ((msg = curl_multi_info_read (multi, &unused)))
        {
          if ((msg->msg == CURLMSG_DONE) && (msg->easy_handle != NULL))
            {
              double total_time;
              struct tr_web_task * task;
              long req_bytes_sent;
              CURL * e = msg->easy_handle;
              curl_easy_getinfo (e, CURLINFO_PRIVATE, (void*)&task);
              assert (e == task->curl_easy);
              curl_easy_getinfo (e, CURLINFO_RESPONSE_CODE, &task->code);
              curl_easy_getinfo (e, CURLINFO_REQUEST_SIZE, &req_bytes_sent);
              curl_easy_getinfo (e, CURLINFO_TOTAL_TIME, &total_time);
              task->did_connect = task->code>0 || req_bytes_sent>0;
              task->did_timeout = !task->code && (total_time >= task->timeout_secs);
              curl_multi_remove_handle (multi, e);
              tr_list_remove_data (&paused_easy_handles, e);
              curl_easy_cleanup (e);
              tr_runInEventThread (task->session, task_finish_func, task);
              --taskCount;
            }
        }
    }

  /* Discard any remaining tasks.
   * This is rare, but can happen on shutdown with unresponsive trackers. */
  while (web->tasks != NULL)
    {
      task = web->tasks;
      web->tasks = task->next;
      dbgmsg ("Discarding task \"%s\"", task->url);
      task_free (task);
    }

  /* cleanup */
  tr_list_free (&paused_easy_handles, NULL);
  curl_multi_cleanup (multi);
  tr_lockFree (web->taskLock);
  tr_free (web->curl_ca_bundle);
  tr_free (web->cookie_filename);
  tr_free (web);
  session->web = NULL;
}


void
tr_webClose (tr_session * session, tr_web_close_mode close_mode)
{
  if (session->web != NULL)
    {
      session->web->close_mode = close_mode;

      if (close_mode == TR_WEB_CLOSE_NOW)
        while (session->web != NULL)
          tr_wait_msec (100);
    }
}

void
tr_webGetTaskInfo (struct tr_web_task * task, tr_web_task_info info, void * dst)
{
  curl_easy_getinfo (task->curl_easy, (CURLINFO) info, dst);
}

/*****
******
******
*****/

const char *
tr_webGetResponseStr (long code)
{
  switch (code)
    {
      case   0: return "No Response";
      case 101: return "Switching Protocols";
      case 200: return "OK";
      case 201: return "Created";
      case 202: return "Accepted";
      case 203: return "Non-Authoritative Information";
      case 204: return "No Content";
      case 205: return "Reset Content";
      case 206: return "Partial Content";
      case 300: return "Multiple Choices";
      case 301: return "Moved Permanently";
      case 302: return "Found";
      case 303: return "See Other";
      case 304: return "Not Modified";
      case 305: return "Use Proxy";
      case 306: return " (Unused)";
      case 307: return "Temporary Redirect";
      case 400: return "Bad Request";
      case 401: return "Unauthorized";
      case 402: return "Payment Required";
      case 403: return "Forbidden";
      case 404: return "Not Found";
      case 405: return "Method Not Allowed";
      case 406: return "Not Acceptable";
      case 407: return "Proxy Authentication Required";
      case 408: return "Request Timeout";
      case 409: return "Conflict";
      case 410: return "Gone";
      case 411: return "Length Required";
      case 412: return "Precondition Failed";
      case 413: return "Request Entity Too Large";
      case 414: return "Request-URI Too Long";
      case 415: return "Unsupported Media Type";
      case 416: return "Requested Range Not Satisfiable";
      case 417: return "Expectation Failed";
      case 500: return "Internal Server Error";
      case 501: return "Not Implemented";
      case 502: return "Bad Gateway";
      case 503: return "Service Unavailable";
      case 504: return "Gateway Timeout";
      case 505: return "HTTP Version Not Supported";
      default:  return "Unknown Error";
    }
}

void
tr_http_escape (struct evbuffer  * out,
                const char       * str,
                size_t             len,
                bool               escape_slashes)
{
  if (str == NULL)
    return;

  if (len == TR_BAD_SIZE)
    len = strlen (str);

  for (const char * end = str + len; str != end; ++str)
    {
      if ((*str == ',') || (*str == '-')
                        || (*str == '.')
                        || (('0' <= *str) && (*str <= '9'))
                        || (('A' <= *str) && (*str <= 'Z'))
                        || (('a' <= *str) && (*str <= 'z'))
                        || ((*str == '/') && (!escape_slashes)))
        evbuffer_add_printf (out, "%c", *str);
      else
        evbuffer_add_printf (out, "%%%02X", (unsigned)(*str&0xFF));
    }
}

char *
tr_http_unescape (const char * str, size_t len)
{
  char * tmp = curl_unescape (str, len);
  char * ret = tr_strdup (tmp);
  curl_free (tmp);
  return ret;
}

static bool
is_rfc2396_alnum (uint8_t ch)
{
  return ('0' <= ch && ch <= '9')
      || ('A' <= ch && ch <= 'Z')
      || ('a' <= ch && ch <= 'z')
      || ch == '.'
      || ch == '-'
      || ch == '_'
      || ch == '~';
}

void
tr_http_escape_sha1 (char * out, const uint8_t * sha1_digest)
{
  const uint8_t * in = sha1_digest;
  const uint8_t * end = in + SHA_DIGEST_LENGTH;

  while (in != end)
    if (is_rfc2396_alnum (*in))
      *out++ = (char) *in++;
    else
      out += tr_snprintf (out, 4, "%%%02x", (unsigned int)*in++);

  *out = '\0';
}
