/*
 * Copyright (c) 2014, Ericsson AB. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this
 * list of conditions and the following disclaimer in the documentation and/or other
 * materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

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

#include <gst/gst.h>

#include "gstdtlsagent.h"

#ifdef __APPLE__
# define __AVAILABILITYMACROS__
# define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
#endif

#include <openssl/err.h>
#include <openssl/ssl.h>

GST_DEBUG_CATEGORY_STATIC (gst_dtls_agent_debug);
#define GST_CAT_DEFAULT gst_dtls_agent_debug

enum
{
  PROP_0,
  PROP_CERTIFICATE,
  NUM_PROPERTIES
};

static GParamSpec *properties[NUM_PROPERTIES];

struct _GstDtlsAgentPrivate
{
  SSL_CTX *ssl_context;

  GstDtlsCertificate *certificate;
};

G_DEFINE_TYPE_WITH_PRIVATE (GstDtlsAgent, gst_dtls_agent, G_TYPE_OBJECT);

static void gst_dtls_agent_finalize (GObject * gobject);
static void gst_dtls_agent_set_property (GObject *, guint prop_id,
    const GValue *, GParamSpec *);
const gchar *gst_dtls_agent_peek_id (GstDtlsAgent *);

#if OPENSSL_VERSION_NUMBER < 0x10100000L
static GRWLock *ssl_locks;

static void
ssl_locking_function (gint mode, gint lock_num, const gchar * file, gint line)
{
  gboolean locking;
  gboolean reading;
  GRWLock *lock;

  locking = mode & CRYPTO_LOCK;
  reading = mode & CRYPTO_READ;
  lock = &ssl_locks[lock_num];

  GST_TRACE_OBJECT (NULL, "%s SSL lock for %s, thread=%p location=%s:%d",
      locking ? "locking" : "unlocking", reading ? "reading" : "writing",
      g_thread_self (), file, line);

  if (locking) {
    if (reading) {
      g_rw_lock_reader_lock (lock);
    } else {
      g_rw_lock_writer_lock (lock);
    }
  } else {
    if (reading) {
      g_rw_lock_reader_unlock (lock);
    } else {
      g_rw_lock_writer_unlock (lock);
    }
  }
}

static void
ssl_thread_id_function (CRYPTO_THREADID * id)
{
  CRYPTO_THREADID_set_pointer (id, g_thread_self ());
}
#endif

void
_gst_dtls_init_openssl (void)
{
  static gsize is_init = 0;

  if (g_once_init_enter (&is_init)) {
    GST_DEBUG_CATEGORY_INIT (gst_dtls_agent_debug, "dtlsagent", 0,
        "DTLS Agent");

    if (OPENSSL_VERSION_NUMBER < 0x1000100fL) {
      GST_WARNING_OBJECT (NULL,
          "Incorrect OpenSSL version, should be >= 1.0.1, is %s",
          OPENSSL_VERSION_TEXT);
      g_assert_not_reached ();
    }
#if OPENSSL_VERSION_NUMBER < 0x10100000L
    GST_INFO_OBJECT (NULL, "initializing openssl %lx", OPENSSL_VERSION_NUMBER);
    SSL_library_init ();
    SSL_load_error_strings ();
    ERR_load_BIO_strings ();

    if (!CRYPTO_get_locking_callback ()) {
      gint i;
      gint num_locks;
      num_locks = CRYPTO_num_locks ();
      ssl_locks = g_new (GRWLock, num_locks);
      for (i = 0; i < num_locks; ++i) {
        g_rw_lock_init (&ssl_locks[i]);
      }
      CRYPTO_set_locking_callback (ssl_locking_function);
      CRYPTO_THREADID_set_callback (ssl_thread_id_function);
    }
#endif

    g_once_init_leave (&is_init, 1);
  }
}

static void
gst_dtls_agent_class_init (GstDtlsAgentClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->set_property = gst_dtls_agent_set_property;
  gobject_class->finalize = gst_dtls_agent_finalize;

  properties[PROP_CERTIFICATE] =
      g_param_spec_object ("certificate",
      "GstDtlsCertificate",
      "Sets the certificate of the agent",
      GST_TYPE_DTLS_CERTIFICATE,
      G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (gobject_class, NUM_PROPERTIES, properties);

  _gst_dtls_init_openssl ();
}

static int
ssl_warn_cb (const char *str, size_t len, void *u)
{
  GstDtlsAgent *self = u;
  GST_WARNING_OBJECT (self, "ssl error: %s", str);
  return 0;
}

static void
gst_dtls_agent_init (GstDtlsAgent * self)
{
  GstDtlsAgentPrivate *priv = gst_dtls_agent_get_instance_private (self);
  self->priv = priv;

  ERR_clear_error ();

#if OPENSSL_VERSION_NUMBER >= 0x1000200fL
  priv->ssl_context = SSL_CTX_new (DTLS_method ());
#else
  priv->ssl_context = SSL_CTX_new (DTLSv1_method ());
#endif
  if (!priv->ssl_context) {
    GST_WARNING_OBJECT (self, "Error creating SSL Context");
    ERR_print_errors_cb (ssl_warn_cb, self);

    g_return_if_reached ();
  }
  /* If any non-fatal issues happened, print them out and carry on */
  if (ERR_peek_error ()) {
    ERR_print_errors_cb (ssl_warn_cb, self);
    ERR_clear_error ();
  }

  SSL_CTX_set_verify_depth (priv->ssl_context, 2);
  SSL_CTX_set_tlsext_use_srtp (priv->ssl_context, "SRTP_AES128_CM_SHA1_80");
  SSL_CTX_set_cipher_list (priv->ssl_context,
      "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH");
  SSL_CTX_set_read_ahead (priv->ssl_context, 1);
#if (OPENSSL_VERSION_NUMBER >= 0x1000200fL) && (OPENSSL_VERSION_NUMBER < 0x10100000L)
  SSL_CTX_set_ecdh_auto (priv->ssl_context, 1);
#endif
}

static void
gst_dtls_agent_finalize (GObject * gobject)
{
  GstDtlsAgentPrivate *priv = GST_DTLS_AGENT (gobject)->priv;

  SSL_CTX_free (priv->ssl_context);
  priv->ssl_context = NULL;

  g_clear_object (&priv->certificate);

  GST_DEBUG_OBJECT (gobject, "finalized");

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

static void
gst_dtls_agent_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstDtlsAgent *self = GST_DTLS_AGENT (object);
  GstDtlsCertificate *certificate;

  switch (prop_id) {
    case PROP_CERTIFICATE:
      certificate = GST_DTLS_CERTIFICATE (g_value_get_object (value));
      g_return_if_fail (GST_IS_DTLS_CERTIFICATE (certificate));
      g_return_if_fail (self->priv->ssl_context);

      self->priv->certificate = certificate;
      g_object_ref (certificate);

      if (!SSL_CTX_use_certificate (self->priv->ssl_context,
              _gst_dtls_certificate_get_internal_certificate (certificate))) {
        GST_WARNING_OBJECT (self, "could not use certificate");
        g_return_if_reached ();
      }

      if (!SSL_CTX_use_PrivateKey (self->priv->ssl_context,
              _gst_dtls_certificate_get_internal_key (certificate))) {
        GST_WARNING_OBJECT (self, "could not use private key");
        g_return_if_reached ();
      }

      if (!SSL_CTX_check_private_key (self->priv->ssl_context)) {
        GST_WARNING_OBJECT (self, "invalid private key");
        g_return_if_reached ();
      }
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (self, prop_id, pspec);
  }
}

GstDtlsCertificate *
gst_dtls_agent_get_certificate (GstDtlsAgent * self)
{
  g_return_val_if_fail (GST_IS_DTLS_AGENT (self), NULL);
  if (self->priv->certificate) {
    g_object_ref (self->priv->certificate);
  }
  return self->priv->certificate;
}

gchar *
gst_dtls_agent_get_certificate_pem (GstDtlsAgent * self)
{
  gchar *pem;
  g_return_val_if_fail (GST_IS_DTLS_AGENT (self), NULL);
  g_return_val_if_fail (GST_IS_DTLS_CERTIFICATE (self->priv->certificate),
      NULL);

  g_object_get (self->priv->certificate, "pem", &pem, NULL);

  return pem;
}

const GstDtlsAgentContext
_gst_dtls_agent_peek_context (GstDtlsAgent * self)
{
  g_return_val_if_fail (GST_IS_DTLS_AGENT (self), NULL);
  return self->priv->ssl_context;
}
