/* GStreamer
 * Copyright (C) 2011 Axis Communications <dev-gstreamer@axis.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:element-curlhttpsink
 * @title: curlhttpsink
 * @short_description: sink that uploads data to a server using libcurl
 *
 * This is a network sink that uses libcurl as a client to upload data to
 * an HTTP server.
 *
 * ## Example launch line
 *
 * Upload a JPEG file to an HTTP server.
 *
 * |[
 * gst-launch-1.0 filesrc location=image.jpg ! jpegparse ! curlhttpsink  \
 *     file-name=image.jpg  \
 *     location=http://192.168.0.1:8080/cgi-bin/patupload.cgi/  \
 *     user=test passwd=test  \
 *     content-type=image/jpeg  \
 *     use-content-length=false
 * ]|
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <curl/curl.h>
#include <string.h>
#include <stdio.h>

#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#include <sys/types.h>
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef HAVE_NETINET_IP_H
#include <netinet/ip.h>
#endif
#ifdef HAVE_NETINET_TCP_H
#include <netinet/tcp.h>
#endif
#include <sys/stat.h>
#include <fcntl.h>

#include "gstcurlelements.h"
#include "gstcurltlssink.h"
#include "gstcurlhttpsink.h"

/* Default values */
#define GST_CAT_DEFAULT                gst_curl_http_sink_debug
#define DEFAULT_TIMEOUT                30
#define DEFAULT_PROXY_PORT             3128
#define DEFAULT_USE_CONTENT_LENGTH     FALSE

#define RESPONSE_CONNECT_PROXY         200

/* Plugin specific settings */

GST_DEBUG_CATEGORY_STATIC (gst_curl_http_sink_debug);

enum
{
  PROP_0,
  PROP_PROXY,
  PROP_PROXY_PORT,
  PROP_PROXY_USER_NAME,
  PROP_PROXY_USER_PASSWD,
  PROP_USE_CONTENT_LENGTH,
  PROP_CONTENT_TYPE
};


/* Object class function declarations */

static void gst_curl_http_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_curl_http_sink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static gboolean gst_curl_http_sink_stop (GstBaseSink * bsink);
static void gst_curl_http_sink_finalize (GObject * gobject);
static gboolean gst_curl_http_sink_set_header_unlocked
    (GstCurlBaseSink * bcsink);
static gboolean gst_curl_http_sink_set_options_unlocked
    (GstCurlBaseSink * bcsink);
static void gst_curl_http_sink_set_mime_type
    (GstCurlBaseSink * bcsink, GstCaps * caps);
static gboolean gst_curl_http_sink_transfer_verify_response_code
    (GstCurlBaseSink * bcsink);
static void gst_curl_http_sink_transfer_prepare_poll_wait
    (GstCurlBaseSink * bcsink);

#define gst_curl_http_sink_parent_class parent_class
G_DEFINE_TYPE (GstCurlHttpSink, gst_curl_http_sink, GST_TYPE_CURL_TLS_SINK);
GST_ELEMENT_REGISTER_DEFINE_WITH_CODE (curlhttpsink, "curlhttpsink",
    GST_RANK_NONE, GST_TYPE_CURL_HTTP_SINK, curl_element_init (plugin));
/* private functions */

static gboolean proxy_setup (GstCurlBaseSink * bcsink, const gchar * http_proxy,
    const gchar * https_proxy);

static void
gst_curl_http_sink_class_init (GstCurlHttpSinkClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstCurlBaseSinkClass *gstcurlbasesink_class = (GstCurlBaseSinkClass *) klass;
  GstBaseSinkClass *gstbasesink_class = (GstBaseSinkClass *) klass;
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);

  GST_DEBUG_CATEGORY_INIT (gst_curl_http_sink_debug, "curlhttpsink", 0,
      "curl http sink element");

  gst_element_class_set_static_metadata (element_class,
      "Curl http sink",
      "Sink/Network",
      "Upload data over HTTP/HTTPS protocol using libcurl",
      "Patricia Muscalu <patricia@axis.com>");

  gstcurlbasesink_class->set_protocol_dynamic_options_unlocked =
      gst_curl_http_sink_set_header_unlocked;
  gstcurlbasesink_class->set_options_unlocked =
      gst_curl_http_sink_set_options_unlocked;
  gstcurlbasesink_class->set_mime_type = gst_curl_http_sink_set_mime_type;
  gstcurlbasesink_class->transfer_verify_response_code =
      gst_curl_http_sink_transfer_verify_response_code;
  gstcurlbasesink_class->transfer_prepare_poll_wait =
      gst_curl_http_sink_transfer_prepare_poll_wait;

  gstbasesink_class->stop = GST_DEBUG_FUNCPTR (gst_curl_http_sink_stop);
  gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_curl_http_sink_finalize);

  gobject_class->set_property = gst_curl_http_sink_set_property;
  gobject_class->get_property = gst_curl_http_sink_get_property;

  g_object_class_install_property (gobject_class, PROP_PROXY,
      g_param_spec_string ("proxy", "Proxy", "HTTP proxy server URI", NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_PROXY_PORT,
      g_param_spec_int ("proxy-port", "Proxy port",
          "HTTP proxy server port", 0, G_MAXINT, DEFAULT_PROXY_PORT,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_PROXY_USER_NAME,
      g_param_spec_string ("proxy-user", "Proxy user name",
          "Proxy user name to use for proxy authentication",
          NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_PROXY_USER_PASSWD,
      g_param_spec_string ("proxy-passwd", "Proxy user password",
          "Proxy user password to use for proxy authentication",
          NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_USE_CONTENT_LENGTH,
      g_param_spec_boolean ("use-content-length", "Use content length header",
          "Use the Content-Length HTTP header instead of "
          "Transfer-Encoding header", DEFAULT_USE_CONTENT_LENGTH,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_CONTENT_TYPE,
      g_param_spec_string ("content-type", "Content type",
          "Content Type to use for the Content-Type header. If not set, "
          "detected mime type will be used", NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

static void
gst_curl_http_sink_init (GstCurlHttpSink * sink)
{
  sink->header_list = NULL;
  sink->use_content_length = DEFAULT_USE_CONTENT_LENGTH;
  sink->content_type = NULL;
  sink->discovered_content_type = NULL;

  sink->proxy_port = DEFAULT_PROXY_PORT;
  sink->proxy_auth = FALSE;
  sink->proxy_conn_established = FALSE;
  sink->proxy_resp = -1;
}

static void
gst_curl_http_sink_finalize (GObject * gobject)
{
  GstCurlHttpSink *this = GST_CURL_HTTP_SINK (gobject);

  GST_DEBUG ("finalizing curlhttpsink");
  g_free (this->proxy);
  g_free (this->proxy_user);
  g_free (this->proxy_passwd);
  g_free (this->content_type);
  g_free (this->discovered_content_type);

  if (this->header_list) {
    curl_slist_free_all (this->header_list);
    this->header_list = NULL;
  }

  G_OBJECT_CLASS (parent_class)->finalize (gobject);
}

static void
gst_curl_http_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstCurlHttpSink *sink;
  GstState cur_state;

  g_return_if_fail (GST_IS_CURL_HTTP_SINK (object));
  sink = GST_CURL_HTTP_SINK (object);

  gst_element_get_state (GST_ELEMENT (sink), &cur_state, NULL, 0);
  if (cur_state != GST_STATE_PLAYING && cur_state != GST_STATE_PAUSED) {
    GST_OBJECT_LOCK (sink);

    switch (prop_id) {
      case PROP_PROXY:
        g_free (sink->proxy);
        sink->proxy = g_value_dup_string (value);
        GST_DEBUG_OBJECT (sink, "proxy set to %s", sink->proxy);
        break;
      case PROP_PROXY_PORT:
        sink->proxy_port = g_value_get_int (value);
        GST_DEBUG_OBJECT (sink, "proxy port set to %d", sink->proxy_port);
        break;
      case PROP_PROXY_USER_NAME:
        g_free (sink->proxy_user);
        sink->proxy_user = g_value_dup_string (value);
        GST_DEBUG_OBJECT (sink, "proxy user set to %s", sink->proxy_user);
        break;
      case PROP_PROXY_USER_PASSWD:
        g_free (sink->proxy_passwd);
        sink->proxy_passwd = g_value_dup_string (value);
        GST_DEBUG_OBJECT (sink, "proxy password set to %s", sink->proxy_passwd);
        break;
      case PROP_USE_CONTENT_LENGTH:
        sink->use_content_length = g_value_get_boolean (value);
        GST_DEBUG_OBJECT (sink, "use_content_length set to %d",
            sink->use_content_length);
        break;
      case PROP_CONTENT_TYPE:
        g_free (sink->content_type);
        sink->content_type = g_value_dup_string (value);
        GST_DEBUG_OBJECT (sink, "content type set to %s", sink->content_type);
        break;
      default:
        GST_DEBUG_OBJECT (sink, "invalid property id %d", prop_id);
        break;
    }

    GST_OBJECT_UNLOCK (sink);

    return;
  }

  /* in PLAYING or PAUSED state */
  GST_OBJECT_LOCK (sink);

  switch (prop_id) {
    case PROP_CONTENT_TYPE:
      g_free (sink->content_type);
      sink->content_type = g_value_dup_string (value);
      GST_DEBUG_OBJECT (sink, "content type set to %s", sink->content_type);
      break;
    default:
      GST_WARNING_OBJECT (sink, "cannot set property when PLAYING");
      break;
  }

  GST_OBJECT_UNLOCK (sink);
}

static void
gst_curl_http_sink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstCurlHttpSink *sink;

  g_return_if_fail (GST_IS_CURL_HTTP_SINK (object));
  sink = GST_CURL_HTTP_SINK (object);

  switch (prop_id) {
    case PROP_PROXY:
      g_value_set_string (value, sink->proxy);
      break;
    case PROP_PROXY_PORT:
      g_value_set_int (value, sink->proxy_port);
      break;
    case PROP_PROXY_USER_NAME:
      g_value_set_string (value, sink->proxy_user);
      break;
    case PROP_PROXY_USER_PASSWD:
      g_value_set_string (value, sink->proxy_passwd);
      break;
    case PROP_USE_CONTENT_LENGTH:
      g_value_set_boolean (value, sink->use_content_length);
      break;
    case PROP_CONTENT_TYPE:
      g_value_set_string (value, sink->content_type);
      break;
    default:
      GST_DEBUG_OBJECT (sink, "invalid property id");
      break;
  }
}

static gboolean
gst_curl_http_sink_set_header_unlocked (GstCurlBaseSink * bcsink)
{
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bcsink);
  gchar *tmp;
  const gchar *used_content_type;

  CURLcode res;

  if (sink->header_list) {
    curl_slist_free_all (sink->header_list);
    sink->header_list = NULL;
  }

  if (sink->use_content_length) {
    /* if content length is used we assume that every buffer is one
     * entire file, which is the case when uploading several jpegs */
    res =
        curl_easy_setopt (bcsink->curl, CURLOPT_POSTFIELDSIZE,
        (long) bcsink->transfer_buf->len);
    if (res != CURLE_OK) {
      bcsink->error = g_strdup_printf ("failed to set HTTP content-length: %s",
          curl_easy_strerror (res));
      return FALSE;
    }
  } else {
    /* when sending a POST request to a HTTP 1.1 server, you can send data
     * without knowing the size before starting the POST if you use chunked
     * encoding */
    sink->header_list = curl_slist_append (sink->header_list,
        "Transfer-Encoding: chunked");
  }

  if (sink->content_type)
    used_content_type = sink->content_type;
  else
    used_content_type = sink->discovered_content_type;

  if (used_content_type) {
    tmp = g_strdup_printf ("Content-Type: %s", used_content_type);
    sink->header_list = curl_slist_append (sink->header_list, tmp);
    g_free (tmp);
  } else {
    GST_WARNING_OBJECT (sink,
        "No content-type available to set in header, continue without it");
  }


  if (bcsink->file_name) {
    tmp = g_strdup_printf ("Content-Disposition: attachment; filename="
        "\"%s\"", bcsink->file_name);
    sink->header_list = curl_slist_append (sink->header_list, tmp);
    g_free (tmp);
  }

  /* set 'Expect: 100-continue'-header explicitly */
  if (sink->use_content_length) {
    sink->header_list =
        curl_slist_append (sink->header_list, "Expect: 100-continue");
  }

  res = curl_easy_setopt (bcsink->curl, CURLOPT_HTTPHEADER, sink->header_list);
  if (res != CURLE_OK) {
    bcsink->error = g_strdup_printf ("failed to set HTTP headers: %s",
        curl_easy_strerror (res));
    return FALSE;
  }

  return TRUE;
}

static gboolean
proxy_enabled (GstCurlHttpSink * sink, gchar ** http_proxy,
    gchar ** https_proxy)
{
  gboolean res = FALSE;

  if (sink->proxy != NULL)
    res = TRUE;

  *http_proxy = getenv ("http_proxy");
  if (*http_proxy != NULL)
    res = TRUE;

  *https_proxy = getenv ("https_proxy");
  if (*https_proxy != NULL)
    res = TRUE;

  return res;
}

static gboolean
gst_curl_http_sink_set_options_unlocked (GstCurlBaseSink * bcsink)
{
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bcsink);
  GstCurlTlsSinkClass *parent_class;
  CURLcode res;
  gchar *http_proxy = NULL;
  gchar *https_proxy = NULL;

  /* proxy settings */
  if (proxy_enabled (sink, &http_proxy, &https_proxy)) {
    if (!proxy_setup (bcsink, http_proxy, https_proxy)) {
      return FALSE;
    }
  }

  res = curl_easy_setopt (bcsink->curl, CURLOPT_POST, 1L);
  if (res != CURLE_OK) {
    bcsink->error = g_strdup_printf ("failed to set HTTP POST: %s",
        curl_easy_strerror (res));
    return FALSE;
  }

  /* FIXME: check user & passwd */
  res = curl_easy_setopt (bcsink->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
  if (res != CURLE_OK) {
    bcsink->error =
        g_strdup_printf ("failed to set HTTP authentication methods: %s",
        curl_easy_strerror (res));
    return FALSE;
  }

  parent_class = GST_CURL_TLS_SINK_GET_CLASS (sink);

  if (g_str_has_prefix (bcsink->url, "https://")) {
    GST_DEBUG_OBJECT (bcsink, "setting up tls options");
    return parent_class->set_options_unlocked (bcsink);
  }

  return TRUE;
}

static gboolean
gst_curl_http_sink_transfer_verify_response_code (GstCurlBaseSink * bcsink)
{
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bcsink);
  glong resp;

  curl_easy_getinfo (bcsink->curl, CURLINFO_RESPONSE_CODE, &resp);
  GST_DEBUG_OBJECT (sink, "response code: %ld", resp);

  if (resp < 100 || resp >= 300) {
    GST_ELEMENT_ERROR_WITH_DETAILS (sink, RESOURCE, WRITE,
        ("HTTP response error code: %ld", resp), (NULL), ("http-status-code",
            G_TYPE_UINT, (guint) resp, NULL));
    return FALSE;
  }

  return TRUE;
}

static void
gst_curl_http_sink_transfer_prepare_poll_wait (GstCurlBaseSink * bcsink)
{
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bcsink);

  if (!sink->proxy_conn_established
      && (sink->proxy_resp != RESPONSE_CONNECT_PROXY)
      && sink->proxy_auth) {
    GST_DEBUG_OBJECT (sink, "prep transfers: connecting proxy");
    curl_easy_getinfo (bcsink->curl, CURLINFO_HTTP_CONNECTCODE,
        &sink->proxy_resp);
    if (sink->proxy_resp == RESPONSE_CONNECT_PROXY) {
      GST_LOG ("received HTTP/1.0 200 Connection Established");
      /* Workaround: redefine HTTP headers before connecting to HTTP server.
       * When talking to proxy, the Content-Length: 0 is send with the request.
       */
      curl_multi_remove_handle (bcsink->multi_handle, bcsink->curl);
      gst_curl_http_sink_set_header_unlocked (bcsink);
      curl_multi_add_handle (bcsink->multi_handle, bcsink->curl);
      sink->proxy_conn_established = TRUE;
    }
  }
}

// FIXME check this: why critical when no mime is set???
static void
gst_curl_http_sink_set_mime_type (GstCurlBaseSink * bcsink, GstCaps * caps)
{
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bcsink);
  GstStructure *structure;
  const gchar *mime_type;

  structure = gst_caps_get_structure (caps, 0);
  mime_type = gst_structure_get_name (structure);

  g_free (sink->discovered_content_type);
  if (!g_strcmp0 (mime_type, "multipart/form-data") &&
      gst_structure_has_field_typed (structure, "boundary", G_TYPE_STRING)) {
    const gchar *boundary;

    boundary = gst_structure_get_string (structure, "boundary");
    sink->discovered_content_type =
        g_strconcat (mime_type, "; boundary=", boundary, NULL);
  } else {
    sink->discovered_content_type = g_strdup (mime_type);
  }
}

static gboolean
gst_curl_http_sink_stop (GstBaseSink * bsink)
{
  gboolean ret;
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bsink);
  ret = GST_BASE_SINK_CLASS (parent_class)->stop (bsink);

  g_clear_pointer (&sink->discovered_content_type, g_free);

  return ret;
}

static gboolean
url_contains_credentials (const gchar * url)
{
  CURLUcode rc;
  g_autofree gchar *user = NULL;
  g_autofree gchar *pass = NULL;
  CURLU *handle = NULL;

  if (url == NULL) {
    return FALSE;
  }

  handle = curl_url ();

  rc = curl_url_set (handle, CURLUPART_URL, url, 0);
  if (rc != CURLUE_OK)
    goto error;

  rc = curl_url_get (handle, CURLUPART_USER, &user, 0);
  if (rc != CURLUE_OK)
    goto error;

  rc = curl_url_get (handle, CURLUPART_PASSWORD, &pass, 0);
  if (rc != CURLUE_OK)
    goto error;

  curl_url_cleanup (handle);
  return TRUE;

error:
  curl_url_cleanup (handle);
  return FALSE;
}

static gboolean
custom_proxy_setup (GstCurlBaseSink * bcsink)
{
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bcsink);
  CURLcode res;

  res = curl_easy_setopt (bcsink->curl, CURLOPT_PROXY, sink->proxy);
  if (res != CURLE_OK) {
    bcsink->error = g_strdup_printf ("failed to set proxy: %s",
        curl_easy_strerror (res));
    return FALSE;
  }

  res = curl_easy_setopt (bcsink->curl, CURLOPT_PROXYPORT, sink->proxy_port);
  if (res != CURLE_OK) {
    bcsink->error = g_strdup_printf ("failed to set proxy port: %s",
        curl_easy_strerror (res));
    return FALSE;
  }

  if (sink->proxy_user != NULL &&
      strlen (sink->proxy_user) &&
      sink->proxy_passwd != NULL && strlen (sink->proxy_passwd)) {
    res = curl_easy_setopt (bcsink->curl, CURLOPT_PROXYUSERNAME,
        sink->proxy_user);
    if (res != CURLE_OK) {
      bcsink->error = g_strdup_printf ("failed to set proxy user name: %s",
          curl_easy_strerror (res));
      return FALSE;
    }

    res = curl_easy_setopt (bcsink->curl, CURLOPT_PROXYPASSWORD,
        sink->proxy_passwd);
    if (res != CURLE_OK) {
      bcsink->error = g_strdup_printf ("failed to set proxy password: %s",
          curl_easy_strerror (res));
      return FALSE;
    }

    sink->proxy_auth = TRUE;
  }

  if (g_str_has_prefix (bcsink->url, "https://")) {
    /* tunnel all operations through a given HTTP proxy */
    res = curl_easy_setopt (bcsink->curl, CURLOPT_HTTPPROXYTUNNEL, 1L);
    if (res != CURLE_OK) {
      bcsink->error = g_strdup_printf ("failed to set HTTP proxy tunnel: %s",
          curl_easy_strerror (res));
      return FALSE;
    }
  }
  return TRUE;
}

static gboolean
proxy_setup (GstCurlBaseSink * bcsink, const gchar * http_proxy,
    const gchar * https_proxy)
{
  GstCurlHttpSink *sink = GST_CURL_HTTP_SINK (bcsink);
  CURLcode res;

  if (sink->proxy != NULL) {
    if (!custom_proxy_setup (bcsink))
      return FALSE;
  } else {
    sink->proxy_auth = url_contains_credentials (http_proxy)
        || url_contains_credentials (https_proxy);
  }

  if (sink->proxy_auth) {
    res = curl_easy_setopt (bcsink->curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
    if (res != CURLE_OK) {
      bcsink->error =
          g_strdup_printf ("failed to set proxy authentication method: %s",
          curl_easy_strerror (res));
      return FALSE;
    }
  }

  return TRUE;
}
