/*
 * Copyright (C) 2002-2017 the xine project
 *
 * This file is part of xine, a free video player.
 *
 * xine is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * xine 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 General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
 *
 */

#include "globals.h"
#include "defs.h"

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef HAVE_NETDB_H
#include <netdb.h>
#endif
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#ifdef HAVE_POLL_H
#include <poll.h>
#endif
#include <time.h>

#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef _WIN32
#include <winsock.h>
#endif

#include "http.h"
#include "utils.h"
#include "watchdog.h"

/*
#define LOG
*/

#define BUFSIZE                 1024
#define BUFSIZE_STR		"1023"
#define DEFAULT_HTTP_PORT         80

typedef struct http_s http_t;

struct http_s {
  off_t            curpos;
  off_t            contentlength;

  char             mime_type[BUFSIZE];

  char             buf[BUFSIZE];
  char             mrlbuf[BUFSIZE];
  char             proxybuf[BUFSIZE];

  char             auth[BUFSIZE];
  char             proxyauth[BUFSIZE];

  const char      *user;
  const char      *password;
  const char      *host;
  int              port;

  int              fh;

  const char      *filename;

  const char      *proxyuser;
  const char      *proxypassword;
  const char      *proxyhost;
  int              proxyport;

  int		   redir_count;
};

static int http_host_connect_attempt (struct in_addr ia, int port)
{
  union {
    struct sockaddr_in i;
    struct sockaddr s;
  } sin;

  int s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (s == -1)
  {
    display_error (FROM_GXINE, _("HTTP error"),
		   _("Failed to open socket: %s"), strerror (errno));
    return -1;
  }

  sin.i.sin_family = AF_INET;
  sin.i.sin_addr   = ia;
  sin.i.sin_port   = htons(port);

#ifndef _WIN32
  int blocking = fcntl (s, F_SETFL, fcntl (s, F_GETFL) | O_NONBLOCK) == -1;
#else
  unsigned long non_block = 1;
  int blocking = ioctlsocket(s, FIONBIO, &non_block) != SOCKET_ERROR;
#endif
  if (blocking)
    watchdog_off ();

  if (!connect (s, &sin.s, sizeof (sin)))
  {
    if (blocking)
      watchdog_on ();
    return s;
  }

  if (errno != EINPROGRESS)
    goto fail;

  /* if we get here, we have a non-blocking socket not yet connected */

#ifndef _WIN32
  struct pollfd fd = { s, POLLIN | POLLPRI | POLLOUT | POLLERR | POLLHUP };
#endif

  time_t start = time (NULL);
  do
  {
#ifdef _WIN32
    struct timeval select_timeout;
    fd_set fdset;
    FD_ZERO (&fdset);
    FD_SET  (s, &fdset);
    select_timeout.tv_sec  = 0;
    select_timeout.tv_usec = 1000000;
    int res = select (s + 1, &fdset, NULL, NULL, &select_timeout);
#else
    int res = poll (&fd, 1, 1000);
#endif
    switch (res)
    {
    case -1:
      goto fail;
    case 0:
      do_pending_events ();
      continue;
    default:
#ifdef _WIN32
      if (!FD_ISSET(s, &fdset))
        goto fail;
#else
      if (fd.revents & POLLERR)
	goto fail;
      if (fd.revents & POLLHUP)
      {
	errno = ETIMEDOUT; /* or something */
	goto fail;
      }
#endif
      return s;
    }
  } while (time (NULL) - start < 30);

  fail:
  display_error (FROM_GXINE, _("HTTP error"),
		 _("Cannot connect to host: %s"), strerror (errno));
  close (s);
  if (blocking)
    watchdog_on ();
  return -1;
}

static int http_host_connect (const char *host, int port)
{
  struct hostent *h = gethostbyname(host);
  if (h==NULL)
  {
    display_error (FROM_GXINE, _("HTTP error"),
		   _("Unable to resolve ‘%s’"), host);
    return -1;
  }

  int i, s;
  for (i = 0, s = -1; s == -1 && h->h_addr_list[i]; ++i)
  {
    struct in_addr ia;
    memcpy (&ia, h->h_addr_list[i], 4);
    s = http_host_connect_attempt (ia, port);
  }

  return s;
}

static int http_parse_url (char *urlbuf, const char **user, const char **password,
                           const char** host, int *port, const char **filename)
{
  char *start, *at, *portcolon, *slash;
  char *authcolon = NULL;

  if (user != NULL)
    *user = NULL;

  if (password != NULL)
    *password = NULL;

  if (host != NULL)
    *host = NULL;

  if (filename != NULL)
    *filename = NULL;

  if (port != NULL)
    *port = 0;

  start = strstr(urlbuf, "://");
  if (start != NULL)
    start += 3;
  else
    start = urlbuf;

  at = strchr(start, '@');
  slash = strchr(start, '/');

  if (at != NULL && slash != NULL && at > slash)
    at = NULL;

  if (at != NULL)
  {
    authcolon = strchr(start, ':');
    if(authcolon != NULL && authcolon > at)
      authcolon = NULL;

    portcolon = strchr(at, ':');
  }
  else
    portcolon = strchr(start, ':');

  if (portcolon != NULL && slash != NULL && portcolon > slash)
    portcolon = NULL;

  if (at != NULL)
  {
    *at = '\0';

    if (user != NULL)
      *user = start;

    if (authcolon != NULL)
    {
      *authcolon = '\0';

      if (password != NULL)
	*password = authcolon + 1;
    }

    if (host != NULL)
      *host = at + 1;
  } else
    if (host != NULL)
      *host = start;

  if (slash != NULL)
  {
    *slash = '\0';

    if (filename != NULL)
      *filename = slash + 1;
  } else if (filename != NULL)
    *filename = urlbuf + strlen(urlbuf);

  if (portcolon != NULL)
  {
    *portcolon = '\0';

    if (port != NULL)
      *port = atoi(portcolon + 1);
  }

  return 0;
}

static int http_basicauth (const char *user, const char *password,
			   char* dest, int len)
{
  static const char enctable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  gchar       *tmp;
  gchar       *sptr;
  gchar       *dptr;
  int          count;

  count = strlen (user) + 1;
  if (password != NULL)
    count += strlen (password);

  if (len < ((count + 2) / 3) * 4 + 1)
    return -1;

  sptr = tmp = g_strconcat (user, ":", password, NULL);
  dptr = dest;
  while (count >= 3)
  {
    *dptr++ = enctable[(sptr[0] & 0xFC) >> 2];
    *dptr++ = enctable[((sptr[0] & 0x3) << 4) | ((sptr[1] & 0xF0) >> 4)];
    *dptr++ = enctable[((sptr[1] & 0x0F) << 2) | ((sptr[2] & 0xC0) >> 6)];
    *dptr++ = enctable[sptr[2] & 0x3F];
    count -= 3;
    sptr += 3;
  }

  if (count > 0)
  {
    *dptr++ = enctable[(sptr[0] & 0xFC) >> 2];

    if (count > 1)
    {
      *dptr++ = enctable[((sptr[0] & 0x3) << 4) | ((sptr[1] & 0xF0) >> 4)];
      *dptr++ = enctable[(sptr[1] & 0x0F) << 2];
    }
    else
    {
      *dptr++ = enctable[(sptr[0] & 0x3) << 4];
      *dptr++ = '=';
    }

    *dptr++ = '=';
  }

  dptr[0] = '\0';

  g_free (tmp);
  return 0;
}

static void __attribute__ ((format (printf, 2, 3)))
report_error (const char *url, const char *fmt, ...)
{
  va_list ap;
  gchar *msg;
  va_start (ap, fmt);
  msg = g_strdup_vprintf (fmt, ap);
  va_end (ap);
  display_error (FROM_GXINE, _("HTTP error"),
		 _("There was an error while trying to download\n%s\n:\n%s"),
		 url, msg);
  g_free (msg);
}

static void http_close (http_t *http)
{
  if (http->fh >= 0)
    close (http->fh);
  free (http);

#ifdef _WIN32
  WSACleanup();
#endif
}

static http_t *http_open (const char *mrl)
{
  char     *proxy;
  gchar    *request;
  int       done, len, linenum;
  char      mime_type[BUFSIZE];
  char      location[BUFSIZE];
  int       redirect = 0;

  http_t *this = calloc (1, sizeof (http_t));
  if (!this) {
    return NULL;
  }

#ifdef _WIN32
  {
    /* WinSock Library Init */
    WSADATA Data;
    int i_err = WSAStartup( MAKEWORD( 1, 1 ), &Data );
    if (i_err) {
      fprintf( stderr, "error: can't initiate WinSocks, error %i\n", i_err );
    }
  }
#endif /* WIN32 */

  this->fh = -1;
  this->redir_count = 0;
  snprintf (this->mrlbuf, sizeof (this->mrlbuf), "%s", mrl);

  this->proxybuf[0] = '\0';
  proxy = getenv("http_proxy");

  retry:

  strcpy (location, this->mrlbuf);

  if (proxy != NULL)
  {
    snprintf (this->proxybuf, sizeof (this->proxybuf), "%s", proxy);

    if (http_parse_url (this->proxybuf, &this->proxyuser,
			&this->proxypassword, &this->proxyhost,
			&this->proxyport, NULL))
    {
      /* malformed URL */
      report_error (location, _("URL parse error"));
      http_close (this);
      return NULL;
    }

    if (this->proxyport == 0)
      this->proxyport = DEFAULT_HTTP_PORT;

    if (this->proxyuser != NULL &&
	http_basicauth (this->proxyuser, this->proxypassword,
			this->proxyauth, BUFSIZE))
    {
      report_error (location, _("proxy authentication error"));
      http_close (this);
      return NULL;
    }
  }

  if (http_parse_url (this->mrlbuf, &this->user, &this->password,
		      &this->host, &this->port, &this->filename))
  {
    http_close (this);
    /* malformed URL */
    report_error (location, _("URL parse error"));
    return NULL;
  }

  if (this->port == 0)
    this->port = DEFAULT_HTTP_PORT;

  if (this->user != NULL &&
      http_basicauth (this->user, this->password, this->auth, BUFSIZE))
  {
    report_error (location, _("proxy authentication error"));
    http_close (this);
    return NULL;
  }

  if (proxy != NULL) {
    logprintf ("http: opening /%s on host %s via proxy %s",
	       this->filename, this->host, this->proxyhost);
  } else {
    logprintf ("http: opening /%s on host %s",
	       this->filename, this->host);
  }

  if (proxy != NULL)
    this->fh = http_host_connect (this->proxyhost, this->proxyport);
  else
    this->fh = http_host_connect (this->host, this->port);

  this->curpos = 0;

  if (this->fh == -1)
  { /* couldn't connect */
    report_error (location, _("host connect error"));
    http_close (this);
    return NULL;
  }

  if (proxy != NULL)
  {
    if (this->port != DEFAULT_HTTP_PORT)
      request = g_strdup_printf ("GET http://%s:%d/%s HTTP/1.0\015\012",
		       this->host, this->port, this->filename);
    else
      request = g_strdup_printf ("GET http://%s/%s HTTP/1.0\015\012",
		       this->host, this->filename);
  }
  else
    request = g_strdup_printf ("GET /%s HTTP/1.0\015\012", this->filename);

  if (this->port != DEFAULT_HTTP_PORT)
    asreprintf (&request, "%sHost: %s:%d\015\012", request,
		this->host, this->port);
  else
    asreprintf (&request, "%sHost: %s\015\012", request, this->host);

  if (this->proxyuser != NULL)
    asreprintf (&request, "%sProxy-Authorization: Basic %s\015\012", request,
		this->proxyauth);

  if (this->user != NULL)
    asreprintf (&request, "%sAuthorization: Basic %s\015\012", request,
	     this->auth);

  asreprintf (&request,
	      "%sUser-Agent: xine/%s\015\012Accept: */*\015\012\015\012",
	      request, VERSION);

  len = strlen (request);
  if (write (this->fh, request, len) != len)
  {
    g_free (request);
    report_error (location, _("couldn't send request"));
abort:
    http_close (this);
    return NULL;
  }

  logprintf ("http: request sent: >%s<\n", request);
  g_free (request);

  /* read and parse reply */
  done = len = linenum = 0;
  this->contentlength = 0;

  while (!done)
  {
    if (read (this->fh, &this->buf[len], 1) <=0)
    {
      switch (errno)
      {
      case EAGAIN:
	logprintf ("http: EAGAIN\n");
	continue;
      default:
	report_error (location, _("read error: %s"), strerror (errno));
	goto abort;
      }
    }

    if (this->buf[len] == '\012' || len >= (BUFSIZE - 2))
    {
      this->buf[len] = '\0';
      len--;

      if (len >= 0 && this->buf[len] == '\015')
      {
	this->buf[len] = '\0';
	len--;
      }

      linenum++;

      logprintf ("input_http: answer: >%s<\n", this->buf);

      if (linenum == 1)
      {
	int httpver, httpsub, httpcode;
	char httpstatus[BUFSIZE];

	if (sscanf(this->buf, "HTTP/%d.%d %d %[^\015\012]", &httpver, &httpsub,
		   &httpcode, httpstatus) != 4)
	{
	  report_error (location, _("invalid HTTP response"));
	  goto abort;
	}

	switch (httpcode)
	{
	/* The following codes have cacheable Location headers,
	 * but we don't yet handle updating the MRLs.
	 */
	case 300: /* Multiple Choices */
	case 301: /* Moved Permanently */
	/* The following codes have uncacheable Location headers. */
	case 302: /* Found */
	case 303: /* See Other */
	case 307: /* Temporary Redirect */
	  if (++this->redir_count >= 10)
	  {
	    report_error (mrl, _("Too many HTTP redirections; aborted"));
	    goto abort;
	  }
	  redirect = httpcode;
	  break;

	case 200 ... 299:
	  break;

	default:
	  report_error (location, _("Unexpected HTTP status (%d)"), httpcode);
	    goto abort;
	}
      }
      else
      {
	if (this->contentlength == 0)
	{
	  off_t contentlength;
	  if (sscanf(this->buf, "Content-Length: %ld", &contentlength) == 1)
	  {
	    logprintf ("http: content length = %ld bytes\n", contentlength);
	    this->contentlength = contentlength;
	  }
	}
	if (sscanf(this->buf, "Content-Type: %"BUFSIZE_STR"s", mime_type) == 1)
	{
	  logprintf ("http: content type = '%s'\n", mime_type);
	  strcpy (this->mime_type, mime_type); /* note buffer sizes */
	}
	if (redirect && !strncasecmp(this->buf, "Location:", 9))
	{
	  off_t index = 9;
	  while (isspace (this->buf[index]))
	    ++index;
	  snprintf (this->mrlbuf, sizeof (this->mrlbuf), "%s", this->buf + 10);
	  logprintf ("http: location = '%s'\n", this->mrlbuf);
	  close (this->fh); /* heading for disconnect anyway due to protocol */
          this->fh = -1;
	  redirect = 0;
	  goto retry;
	}
      }

      if (len == -1)
	done = 1;
      else
	len = 0;
    } else
      len ++;
  }

  if (redirect)
  { /* the server says that the resource is elsewhere but hasn't said where */
    report_error (location, _("HTTP redirection (%d) is missing a location"), redirect);
    goto abort;
  }

  return this;
}



char *http_download (const char *url, ssize_t *file_size)
{
  logprintf ("http: attempt to download %s\n", url);

  if (file_size)
    *file_size = 0;

  http_t *http = http_open (url);
  if (!http)
    return NULL;

  logprintf ("http: connected\n");

  char   *buf = NULL;
  ssize_t total = 0;
  for (;;)
  {
    int n;

    buf = realloc (buf, total + 4098); /* two extra bytes for termination */
    n = read (http->fh, &buf[total], 4096);

    if (n > 0)		/* got some data */
      total += n;
    else if (n == 0)	/* end of stream */
      break;
    else		/* error */
    {
      switch (errno)
      {
      case EAGAIN:
	logprintf ("http: EAGAIN\n");
	continue;
      default:
	/* FIXME: report errno in a window? */
	g_print (_("http: read error: %s\n"), strerror (errno));
        free(buf);
        http_close (http);
	return NULL;
      }
    }
  }

  logprintf ("http: got %zd bytes of content\n%s", total, buf);

  http_close (http);

  if (file_size)
    *file_size = total;

  /* ensure that the buffer is LF+NUL-terminated
   * (don't include this in the buffer size!)
   */
  if (total && buf[total - 1] != '\n')
    buf[total++] = '\n';
  buf[total] = 0;

  return buf;
}

ssize_t http_peek (const char *url, char *buffer, size_t buffer_size,
	       char *mime_type, size_t mime_type_size)
{
  logprintf ("http: attempt to download %zu bytes from %s\n", buffer_size, url);

  http_t *http = http_open (url);
  if (!http)
    return 0;

  if (mime_type && mime_type_size)
    snprintf (mime_type, mime_type_size, "%s", http->mime_type);

  logprintf ("http: connected\n");

  int total = read (http->fh, buffer, buffer_size);

  http_close (http);

  return total;
}
