Blob Blame History Raw
/*
 * Copyright (C) 2010, 2011 Igalia S.L.
 * Copyright (C) 2012 Canonical Ltd.
 *
 * Contact: Iago Toral Quiroga <itoral@igalia.com>
 *
 * Authors: Víctor M. Jáquez L. <vjaquez@igalia.com>
 *          Juan A. Suarez Romero <jasuarez@igalia.com>
 *          Jens Georg <jensg@openismus.com>
 *          Mathias Hasselmann <mathias@openismus.com>
 *
 * This library 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; version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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:grl-net-wc
 * @short_description: small and simple HTTP client
 *
 * Most of the Grilo's sources need to access to web resources. The purpose of
 * this utility class is to provide a thin and lean mechanism for those plugins
 * to interact with those resources.
 */

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

#define LIBSOUP_USE_UNSTABLE_REQUEST_API

#include <errno.h>
#include <glib/gi18n-lib.h>
#include <glib/gstdio.h>
#include <libsoup/soup-cache.h>
#include <libsoup/soup-request-http.h>
#include <libsoup/soup.h>
#include <string.h>

#include <grilo.h>
#include "grl-net-wc.h"
#include "grl-net-mock-private.h"

#define GRL_LOG_DOMAIN_DEFAULT wc_log_domain
GRL_LOG_DOMAIN_STATIC(wc_log_domain);

#define GRL_NET_CAPTURE_DIR_VAR "GRL_NET_CAPTURE_DIR"

enum {
  PROP_0,
  PROP_LOG_LEVEL,
  PROP_THROTTLING,
  PROP_CACHE,
  PROP_CACHE_SIZE,
  PROP_USER_AGENT
};

struct request_res {
  SoupRequest *request;
  gchar *buffer;
  gsize length;
  gsize offset;
};

struct _GrlNetWcPrivate {
  SoupSession *session;
  SoupLoggerLogLevel log_level;
  /* throttling in secs */
  guint throttling;
  /* last request time  */
  GTimeVal last_request;
  /* closure queue for delayed requests */
  GQueue *pending;
  /* cache size in Mb */
  guint cache_size;
  gchar *previous_data;
};

static const char *capture_dir = NULL;

GQuark
grl_net_wc_error_quark (void)
{
  return g_quark_from_static_string ("grl-wc-error-quark");
}

G_DEFINE_TYPE_WITH_PRIVATE (GrlNetWc, grl_net_wc, G_TYPE_OBJECT);

static void grl_net_wc_finalize (GObject *object);
static void grl_net_wc_set_property (GObject *object,
                                     guint propid,
                                     const GValue *value,
                                     GParamSpec *pspec);
static void grl_net_wc_get_property (GObject *object,
                                     guint propid,
                                     GValue *value,
                                     GParamSpec *pspec);

static void
grl_net_wc_class_init (GrlNetWcClass *klass)
{
  GObjectClass *g_klass;

  g_klass = G_OBJECT_CLASS (klass);
  g_klass->finalize = grl_net_wc_finalize;
  g_klass->set_property = grl_net_wc_set_property;
  g_klass->get_property = grl_net_wc_get_property;

  /**
   * GrlNetWc::loglevel:
   *
   * The log level for HTTP connections. This value is used by libsoup.
   */
  g_object_class_install_property (g_klass,
                                   PROP_LOG_LEVEL,
                                   g_param_spec_uint ("loglevel",
                                                      "Log level",
                                                      "Log level for HTTP connections",
                                                      0, 3, 0,
                                                      G_PARAM_READWRITE |
                                                      G_PARAM_STATIC_STRINGS));

  /**
   * GrlNetWc::throttling:
   *
   * The timeout in seconds between connections. All the connections will be
   * queued and each one will be dispatched after waiting this value.
   */
  g_object_class_install_property (g_klass,
                                   PROP_THROTTLING,
                                   g_param_spec_uint ("throttling",
                                                      "throttle timeout",
                                                      "Time to throttle connections",
                                                      0, G_MAXUINT, 0,
                                                      G_PARAM_READWRITE |
                                                      G_PARAM_STATIC_STRINGS));
  /**
   * GrlNetWc::cache:
   *
   * %TRUE if cache must be used. %FALSE otherwise.
   */
  g_object_class_install_property (g_klass,
                                   PROP_CACHE,
                                   g_param_spec_boolean ("cache",
                                                         "Use cache",
                                                         "Use cache",
                                                         TRUE,
                                                         G_PARAM_READWRITE |
                                                         G_PARAM_CONSTRUCT |
                                                         G_PARAM_STATIC_STRINGS));
  /**
   * GrlNetWc::cache-size:
   *
   * Maximum size of cache, in Mb. Default value is 10Mb.
   */
  g_object_class_install_property (g_klass,
                                   PROP_CACHE_SIZE,
                                   g_param_spec_uint ("cache-size",
                                                      "Cache size",
                                                      "Size of cache in Mb",
                                                      0, G_MAXUINT, 10,
                                                      G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT |
                                                      G_PARAM_STATIC_STRINGS));
  /**
   * GrlNetWc::user-agent:
   *
   * User agent identifier.
   */
  g_object_class_install_property (g_klass,
                                   PROP_USER_AGENT,
                                   g_param_spec_string ("user-agent",
                                                        "User Agent",
                                                        "User agent identifier",
                                                        NULL,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT |
                                                        G_PARAM_STATIC_STRINGS));
}

static void
free_op_res (void *op)
{
  struct request_res *rr = op;

  g_object_unref (rr->request);
  g_slice_free (struct request_res, rr);
}

/*
 * use-thread-context is available for libsoup-2.4 >= 2.39.0
 * We check in run-time if it's available
 */
static void
set_thread_context (GrlNetWc *self)
{
    GrlNetWcPrivate *priv = self->priv;
    GObjectClass *klass = G_OBJECT_GET_CLASS (priv->session);
    GParamSpec *spec = g_object_class_find_property (klass,
                                                     "use-thread-context");
    if (spec)
      g_object_set (priv->session, "use-thread-context", TRUE, NULL);
}

static void
init_dump_directory (void)
{
  capture_dir = g_getenv (GRL_NET_CAPTURE_DIR_VAR);

  if (capture_dir && is_mocked ()) {
    GRL_WARNING ("Cannot capture while mocking is enabled.");
    capture_dir = NULL;
    return;
  }

  if (capture_dir && g_mkdir_with_parents (capture_dir, 0700) != 0) {
    GRL_WARNING ("Could not create capture directory \"%s\": %s",
                 capture_dir, g_strerror (errno));
    capture_dir = NULL;
    return;
  }
}

static void
cache_down (GrlNetWc *self)
{
  GFile *cache_dir_file;
  GrlNetWcPrivate *priv = self->priv;
  SoupSessionFeature *cache = soup_session_get_feature (priv->session, SOUP_TYPE_CACHE);
  gchar *cache_dir;

  GRL_DEBUG ("cache down");

  if (!cache) {
    return;
  }

  soup_cache_clear (SOUP_CACHE (cache));

  g_object_get (cache, "cache-dir", &cache_dir, NULL);
  cache_dir_file = g_file_new_for_path (cache_dir);
  g_free (cache_dir);

  g_file_delete (cache_dir_file, NULL, NULL);
  g_object_unref (G_OBJECT (cache_dir_file));

  soup_session_remove_feature (priv->session, cache);
}

static void
cache_up (GrlNetWc *self)
{
  SoupCache *cache;
  GrlNetWcPrivate *priv = self->priv;
  gchar *dir;

  GRL_DEBUG ("cache up");

  dir = g_dir_make_tmp ("grilo-plugin-cache-XXXXXX", NULL);
  if (!dir)
    return;

  cache = soup_cache_new (dir, SOUP_CACHE_SINGLE_USER);
  g_free (dir);

  soup_session_add_feature (priv->session,
                            SOUP_SESSION_FEATURE (cache));

  if (priv->cache_size) {
    soup_cache_set_max_size (cache, priv->cache_size * 1024 * 1024);
  }

  g_object_unref (cache);
}

static gboolean
cache_is_available (GrlNetWc *self)
{
  return soup_session_get_feature (self->priv->session, SOUP_TYPE_CACHE) != NULL;
}

static void
init_requester (GrlNetWc *self)
{
  init_dump_directory ();
}

static void
finalize_requester (GrlNetWc *self)
{
  GrlNetWcPrivate *priv = self->priv;

  cache_down (self);
  g_free (priv->previous_data);
}

static void
grl_net_wc_init (GrlNetWc *wc)
{
  GRL_LOG_DOMAIN_INIT (wc_log_domain, "wc");

  wc->priv = grl_net_wc_get_instance_private (wc);

  wc->priv->session = soup_session_async_new ();
  wc->priv->pending = g_queue_new ();

  set_thread_context (wc);
  init_mock_requester (wc);
  init_requester (wc);
}

static void
grl_net_wc_finalize (GObject *object)
{
  GrlNetWc *wc;

  wc = GRL_NET_WC (object);
  grl_net_wc_flush_delayed_requests (wc);

  cache_down (wc);
  finalize_requester (wc);
  finalize_mock_requester (wc);

  g_queue_free (wc->priv->pending);
  g_object_unref (wc->priv->session);

  G_OBJECT_CLASS (grl_net_wc_parent_class)->finalize (object);
}

static void
grl_net_wc_set_property (GObject *object,
                         guint propid,
                         const GValue *value,
                         GParamSpec *pspec)
{
  GrlNetWc *wc;

  wc = GRL_NET_WC (object);

  switch (propid) {
  case PROP_LOG_LEVEL:
    grl_net_wc_set_log_level (wc, g_value_get_uint (value));
    break;
  case PROP_THROTTLING:
    grl_net_wc_set_throttling (wc, g_value_get_uint (value));
    break;
  case PROP_CACHE:
    grl_net_wc_set_cache (wc, g_value_get_boolean (value));
    break;
  case PROP_CACHE_SIZE:
    grl_net_wc_set_cache_size (wc, g_value_get_uint (value));
    break;
  case PROP_USER_AGENT:
    g_object_set (G_OBJECT (wc->priv->session),
                  "user-agent", g_value_get_string (value),
                  NULL);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (wc, propid, pspec);
  }
}

static void
grl_net_wc_get_property (GObject *object,
                         guint propid,
                         GValue *value,
                         GParamSpec *pspec)
{
  GrlNetWc *wc;

  wc = GRL_NET_WC (object);

  switch (propid) {
  case PROP_LOG_LEVEL:
    g_value_set_uint (value, wc->priv->log_level);
    break;
  case PROP_THROTTLING:
    g_value_set_uint (value, wc->priv->throttling);
    break;
  case PROP_CACHE:
    g_value_set_boolean(value, cache_is_available (wc));
    break;
  case PROP_CACHE_SIZE:
    g_value_set_uint (value, wc->priv->cache_size);
    break;
  case PROP_USER_AGENT:
    g_object_get_property (G_OBJECT (wc->priv->session), "user_agent", value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (wc, propid, pspec);
  }
}

struct request_clos {
  GrlNetWc *self;
  char *url;
  GAsyncResult *result;
  GCancellable *cancellable;
  GHashTable *headers;
  guint source_id;
};

static void
request_clos_destroy (gpointer data)
{
  struct request_clos *c = (struct request_clos *) data;

  g_free (c->url);
  g_clear_object (&c->cancellable);
  g_clear_pointer (&c->headers, g_hash_table_unref);
  g_free (c);
}

static void
parse_error (guint status,
             const gchar *reason,
             const gchar *response,
             GSimpleAsyncResult *result)
{
  if (!response || *response == '\0')
    response = reason;

  switch (status) {
  case SOUP_STATUS_CANT_RESOLVE:
  case SOUP_STATUS_CANT_CONNECT:
  case SOUP_STATUS_SSL_FAILED:
  case SOUP_STATUS_IO_ERROR:
    g_simple_async_result_set_error (result, GRL_NET_WC_ERROR,
                                     GRL_NET_WC_ERROR_NETWORK_ERROR,
                                     _("Cannot connect to the server"));
    return;
  case SOUP_STATUS_CANT_RESOLVE_PROXY:
  case SOUP_STATUS_CANT_CONNECT_PROXY:
    g_simple_async_result_set_error (result, G_IO_ERROR,
                                     G_IO_ERROR_PROXY_FAILED,
                                     _("Cannot connect to the proxy server"));
    return;
  case SOUP_STATUS_INTERNAL_SERVER_ERROR: /* 500 */
  case SOUP_STATUS_MALFORMED:
  case SOUP_STATUS_BAD_REQUEST: /* 400 */
    g_simple_async_result_set_error (result, GRL_NET_WC_ERROR,
                                     GRL_NET_WC_ERROR_PROTOCOL_ERROR,
                                     _("Invalid request URI or header: %s"),
                                     response);
    return;
  case SOUP_STATUS_UNAUTHORIZED: /* 401 */
  case SOUP_STATUS_FORBIDDEN: /* 403 */
    g_simple_async_result_set_error (result, GRL_NET_WC_ERROR,
                                     GRL_NET_WC_ERROR_AUTHENTICATION_REQUIRED,
                                     _("Authentication required: %s"), response);
    return;
  case SOUP_STATUS_NOT_FOUND: /* 404 */
    g_simple_async_result_set_error (result, GRL_NET_WC_ERROR,
                                     GRL_NET_WC_ERROR_NOT_FOUND,
                                     _("The requested resource was not found: %s"),
                                     response);
    return;
  case SOUP_STATUS_CONFLICT: /* 409 */
  case SOUP_STATUS_PRECONDITION_FAILED: /* 412 */
    g_simple_async_result_set_error (result, GRL_NET_WC_ERROR,
                                     GRL_NET_WC_ERROR_CONFLICT,
                                     _("The entry has been modified since it was downloaded: %s"),
                                     response);
    return;
  case SOUP_STATUS_CANCELLED:
    g_simple_async_result_set_error (result, G_IO_ERROR,
                                     G_IO_ERROR_CANCELLED,
                                     _("Operation was cancelled"));
    return;
  default:
    GRL_DEBUG ("Unhandled status: %s", soup_status_get_phrase (status));
    g_simple_async_result_set_error (result, G_IO_ERROR,
                                     G_IO_ERROR_FAILED,
                                     "%s", soup_status_get_phrase (status));
  }
}

static char *
build_request_filename (const char *uri)
{
  char *hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, uri, -1);

  char *filename = g_strdup_printf ("%"G_GINT64_FORMAT "-%s.data",
                                    g_get_monotonic_time (), hash);

  g_free (hash);
  return filename;
}

static void
dump_data (SoupURI *uri,
           const char *buffer,
           const gsize length)
{
  if (!capture_dir)
    return;

  char *uri_string = soup_uri_to_string (uri, FALSE);

  /* Write request content to file in capture directory. */
  char *request_filename = build_request_filename (uri_string);
  char *path = g_build_filename (capture_dir, request_filename, NULL);

  GError *error = NULL;
  if (!g_file_set_contents (path, buffer, length, &error)) {
    GRL_WARNING ("Could not write contents to disk: %s", error->message);
    g_error_free (error);
  }

  g_free (path);

  /* Append record about the just written file to "grl-net-mock-data-%PID.ini"
   * in the capture directory. */
  char *filename = g_strdup_printf ("grl-net-mock-data-%u.ini", getpid());
  path = g_build_filename (capture_dir, filename, NULL);
  g_free (filename);

  FILE *stream = g_fopen (path, "at");
  g_free (path);

  if (!stream) {
    GRL_WARNING ("Could not write contents to disk: %s", g_strerror (errno));
  } else {
    if (ftell (stream) == 0)
      fprintf (stream, "[default]\nversion=%d\n\n", GRL_NET_MOCK_VERSION);

    fprintf (stream, "[%s]\ndata=%s\n\n", uri_string, request_filename);
    fclose (stream);
  }

  g_free (request_filename);
  g_free (uri_string);
}

static void
read_async_cb (GObject *source,
               GAsyncResult *res,
               gpointer user_data)
{
  GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT (user_data);
  struct request_res *rr = g_simple_async_result_get_op_res_gpointer (result);;

  GError *error = NULL;
  gssize s = g_input_stream_read_finish (G_INPUT_STREAM (source), res, &error);

  gsize to_read;

  if (s > 0) {
    /* Continue reading */
    rr->offset += s;
    to_read = rr->length - rr->offset;

    if (!to_read) {
      /* Buffer is not enough; we need to assign more space */
      rr->length *= 2;
      rr->buffer = g_renew (gchar, rr->buffer, rr->length);
      to_read = rr->length - rr->offset;
    }

    g_input_stream_read_async (G_INPUT_STREAM (source),
                               rr->buffer + rr->offset,
                               to_read,
                               G_PRIORITY_DEFAULT,
                               NULL,
                               read_async_cb,
                               user_data);
    return;
  }

  /* Put the end of string */
  rr->buffer[rr->offset] = '\0';

  g_input_stream_close (G_INPUT_STREAM (source), NULL, NULL);
  g_object_unref (source);

  if (error) {
    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      g_simple_async_result_set_error (result, G_IO_ERROR,
                                       G_IO_ERROR_CANCELLED,
                                       _("Operation was cancelled"));
    } else {
      g_simple_async_result_set_error (result, G_IO_ERROR,
                                       G_IO_ERROR_FAILED,
                                       _("Data not available"));
    }

    g_error_free (error);

    g_simple_async_result_complete (result);
    g_object_unref (result);
    return;
  }

  {
    SoupMessage *msg =
      soup_request_http_get_message (SOUP_REQUEST_HTTP (rr->request));

    if (msg && msg->status_code != SOUP_STATUS_OK) {
        parse_error (msg->status_code,
                     msg->reason_phrase,
                     msg->response_body->data,
                     G_SIMPLE_ASYNC_RESULT (user_data));
        g_object_unref (msg);
    }
  }

  g_simple_async_result_complete (result);
  g_object_unref (result);
}

static void
reply_cb (GObject *source,
          GAsyncResult *res,
          gpointer user_data)
{
  GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT (user_data);
  struct request_res *rr = g_simple_async_result_get_op_res_gpointer (result);

  GError *error = NULL;
  GInputStream *in = soup_request_send_finish (rr->request, res, &error);

  if (error) {
    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      g_simple_async_result_set_from_error (result, error);
    } else {
      g_simple_async_result_set_error (result, G_IO_ERROR,
                                       G_IO_ERROR_FAILED,
                                       _("Data not available"));
    }
    g_error_free (error);

    g_simple_async_result_complete (result);
    g_object_unref (result);
    return;
  }

  rr->length = soup_request_get_content_length (rr->request) + 1;
  if (rr->length == 1)
    rr->length = 50 * 1024;

  rr->buffer = g_new (gchar, rr->length);

  g_input_stream_read_async (in,
                             rr->buffer,
                             rr->length,
                             G_PRIORITY_DEFAULT,
                             NULL,
                             read_async_cb,
                             user_data);
}

static void
get_url_now (GrlNetWc *self,
             const char *url,
             GHashTable *headers,
             GAsyncResult *result,
             GCancellable *cancellable)
{
  GrlNetWcPrivate *priv = self->priv;
  SoupURI *uri;
  struct request_res *rr = g_slice_new0 (struct request_res);

  g_simple_async_result_set_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result),
                                             rr,
                                             NULL);

  uri = soup_uri_new (url);
  if (uri) {
    rr->request = soup_session_request_uri (priv->session, uri, NULL);
    soup_uri_free (uri);
  } else {
    rr->request = NULL;
  }

  if (!rr->request) {
    g_simple_async_result_set_error (G_SIMPLE_ASYNC_RESULT (result),
                                     G_IO_ERROR,
                                     G_IO_ERROR_INVALID_ARGUMENT,
                                     _("Invalid URL %s"),
                                     url);
    g_simple_async_result_complete (G_SIMPLE_ASYNC_RESULT (result));
    g_object_unref (result);
    return;
  }

  if (headers != NULL) {
    SoupMessage *message;
    GHashTableIter iter;
    const char *key, *value;

    message = soup_request_http_get_message (SOUP_REQUEST_HTTP (rr->request));

    if (message) {
      g_hash_table_iter_init (&iter, headers);
      while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *)&value)) {
        soup_message_headers_append (message->request_headers, key, value);
      }
      g_object_unref (message);
    }
  }

  soup_request_send_async (rr->request, cancellable, reply_cb, result);
}

static gboolean
get_url_cb (gpointer user_data)
{
  struct request_clos *c = (struct request_clos *) user_data;

  /* validation */
  {
    GrlNetWcPrivate *priv = c->self->priv;
    struct request_clos *d = g_queue_pop_tail (priv->pending);
    g_assert (c == d);
  }

  if (is_mocked ())
    get_url_mocked (c->self, c->url, c->headers, c->result, c->cancellable);
  else
    get_url_now (c->self, c->url, c->headers, c->result, c->cancellable);

  return FALSE;
}

static void
get_url (GrlNetWc *self,
         const char *url,
         GHashTable *headers,
         GAsyncResult *result,
         GCancellable *cancellable)
{
  guint id;
  GTimeVal now;
  struct request_clos *c;
  GrlNetWcPrivate *priv = self->priv;

  /* closure */
  c = g_new (struct request_clos, 1);
  c->self = self;
  c->url = g_strdup (url);
  c->headers = headers? g_hash_table_ref (headers): NULL;
  c->result = result;
  c->cancellable = cancellable ? g_object_ref (cancellable) : NULL;

  g_get_current_time (&now);

  /* If grl-net-wc is not mocked, we need to check if throttling is set
   * otherwise the throttling delay check would always be true */
  if (is_mocked ()
      || priv->throttling == 0
      || (now.tv_sec - priv->last_request.tv_sec) > priv->throttling) {
    priv->last_request = now;
    id = g_idle_add_full (G_PRIORITY_HIGH_IDLE,
                          get_url_cb, c, request_clos_destroy);
  } else {
    priv->last_request.tv_sec += priv->throttling;

    GRL_DEBUG ("delaying web request by %lu seconds",
               priv->last_request.tv_sec - now.tv_sec);
    id = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
                                     priv->last_request.tv_sec - now.tv_sec,
                                     get_url_cb, c, request_clos_destroy);
  }
  g_source_set_name_by_id (id, "[grl-net] get_url_cb");

  c->source_id = id;
  g_queue_push_head (self->priv->pending, c);
}

static void
get_content (GrlNetWc *self,
             void *op,
             gchar **content,
             gsize *length)
{
  GrlNetWcPrivate *priv = self->priv;
  struct request_res *rr = op;

  g_clear_pointer (&priv->previous_data, g_free);

  if (is_mocked ()) {
    get_content_mocked (self, op, &(priv->previous_data), length);
  } else {
    dump_data (soup_request_get_uri (rr->request),
               rr->buffer,
               rr->offset);
    priv->previous_data = rr->buffer;
    if (length) {
      *length = rr->offset;
    }
  }

  if (content)
    *content = self->priv->previous_data;
  else {
    if (length) {
      *length = 0;
    }
  }
}

/**
 * grl_net_wc_new:
 *
 * Creates a new #GrlNetWc.
 *
 * Returns: a new allocated instance of #GrlNetWc. Do g_object_unref() after
 * use it.
 */
GrlNetWc *
grl_net_wc_new ()
{
  return g_object_new (GRL_TYPE_NET_WC,
                       NULL);
}

/**
 * grl_net_wc_request_async:
 * @self: a #GrlNetWc instance
 * @uri: The URI of the resource to request
 * @cancellable: (allow-none): a #GCancellable instance or %NULL to ignore
 * @callback: The callback when the result is ready
 * @user_data: User data set for the @callback
 *
 * Request the fetching of a web resource given the @uri. This request is
 * asynchronous, thus the result will be returned within the @callback.
 */
void
grl_net_wc_request_async (GrlNetWc *self,
                          const char *uri,
                          GCancellable *cancellable,
                          GAsyncReadyCallback callback,
                          gpointer user_data)
{
  grl_net_wc_request_with_headers_hash_async (self,
                                              uri,
                                              NULL,
                                              cancellable,
                                              callback,
                                              user_data);
}

/**
 * grl_net_wc_request_with_headers_async:
 * @self: a #GrlNetWc instance
 * @uri: The URI of the resource to request
 * @cancellable: (allow-none): a #GCancellable instance or %NULL to ignore
 * @callback: The callback when the result is ready
 * @user_data: User data set for the @callback
 * @...: List of tuples of header name and header value, terminated by
 * %NULL.
 *
 * Request the fetching of a web resource given the @uri. This request is
 * asynchronous, thus the result will be returned within the @callback.
 *
 * Since: 0.2.2
 */
void grl_net_wc_request_with_headers_async (GrlNetWc *self,
                                            const char *uri,
                                            GCancellable *cancellable,
                                            GAsyncReadyCallback callback,
                                            gpointer user_data,
                                            ...)
{
  va_list va_args;
  const gchar *header_name = NULL, *header_value = NULL;
  GHashTable *headers = NULL;

  va_start (va_args, user_data);

  header_name = va_arg (va_args, const gchar *);
  while (header_name) {
    header_value = va_arg (va_args, const gchar *);
    if (header_value) {
      if (headers == NULL) {
        headers = g_hash_table_new_full (g_str_hash,
                                         g_str_equal,
                                         g_free,
                                         g_free);
      }
      g_hash_table_insert (headers, g_strdup (header_name), g_strdup (header_value));
    }
    header_name = va_arg (va_args, const gchar *);
  }

  va_end (va_args);

  grl_net_wc_request_with_headers_hash_async (self,
                                              uri,
                                              headers,
                                              cancellable,
                                              callback,
                                              user_data);

  g_clear_pointer (&headers, g_hash_table_unref);
}


/**
 * grl_net_wc_request_with_headers_hash_async: (rename-to grl_net_wc_request_with_headers_async)
 * @self: a #GrlNetWc instance
 * @uri: The URI of the resource to request
 * @headers: (allow-none) (element-type utf8 utf8): a set of additional HTTP
 * headers for this request or %NULL to ignore
 * @cancellable: (allow-none): a #GCancellable instance or %NULL to ignore
 * @callback: The callback when the result is ready
 * @user_data: User data set for the @callback
 *
 * Request the fetching of a web resource given the @uri. This request is
 * asynchronous, thus the result will be returned within the @callback.
 *
 * Since: 0.2.2
 */
void
grl_net_wc_request_with_headers_hash_async (GrlNetWc *self,
                                            const char *uri,
                                            GHashTable *headers,
                                            GCancellable *cancellable,
                                            GAsyncReadyCallback callback,
                                            gpointer user_data)
{
  GSimpleAsyncResult *result;

  result = g_simple_async_result_new (G_OBJECT (self),
                                      callback,
                                      user_data,
                                      grl_net_wc_request_async);

  get_url (self, uri, headers, G_ASYNC_RESULT (result), cancellable);
}


/**
 * grl_net_wc_request_finish:
 * @self: a #GrlNetWc instance
 * @result: The result of the request
 * @content: (out) (array length=length) (element-type guint8) (allow-none)
 * (transfer full): The contents of the resource
 * @length: (out) (allow-none): The length of the contents or %NULL if it is not
 * needed
 * @error: return location for a #GError, or %NULL
 *
 * Finishes an asynchronous load of the file's contents.
 * The contents are placed in contents, and length is set to the size of the
 * contents string.
 *
 * The content address will be invalidated at the next request. So if you
 * want to keep it, please copy it into another address.
 *
 * Returns: %TRUE if the request was successfull. If %FALSE an error occurred.
 */
gboolean
grl_net_wc_request_finish (GrlNetWc *self,
                           GAsyncResult *result,
                           gchar **content,
                           gsize *length,
                           GError **error)
{
  GSimpleAsyncResult *res = G_SIMPLE_ASYNC_RESULT (result);
  gboolean ret = TRUE;

  g_warn_if_fail (g_simple_async_result_get_source_tag (res) ==
                  grl_net_wc_request_async);

  void *op = g_simple_async_result_get_op_res_gpointer (res);

  if (g_simple_async_result_propagate_error (res, error) == TRUE) {
    ret = FALSE;
    goto end_func;
  }

  get_content(self, op, content, length);

end_func:
  if (is_mocked ())
    free_mock_op_res (op);
  else
    free_op_res (op);

  return ret;
}

/**
 * grl_net_wc_set_log_level:
 * @self: a #GrlNetWc instance
 * @log_level: the libsoup log level to set [0,3]
 *
 * Setting the log level the logger feature is added into
 * the libsoup session.
 */
void
grl_net_wc_set_log_level (GrlNetWc *self,
                          guint log_level)
{
  SoupLogger *logger;

  g_return_if_fail (log_level <= 3);
  g_return_if_fail (GRL_IS_NET_WC (self));

  if (self->priv->log_level == log_level)
    return;

  soup_session_remove_feature_by_type (self->priv->session, SOUP_TYPE_LOGGER);

  logger = soup_logger_new ((SoupLoggerLogLevel) log_level, -1);
  soup_session_add_feature (self->priv->session, SOUP_SESSION_FEATURE (logger));
  g_object_unref (logger);

  self->priv->log_level = (SoupLoggerLogLevel) log_level;
}

/**
 * grl_net_wc_set_throttling:
 * @self: a #GrlNetWc instance
 * @throttling: the number of seconds to wait between requests
 *
 * Setting this property, the #GrlNetWc will queue all the requests and
 * will dispatch them with a pause between them of this value.
 */
void
grl_net_wc_set_throttling (GrlNetWc *self,
                           guint throttling)
{
  g_return_if_fail (GRL_IS_NET_WC (self));

  if (throttling > 0) {
    /* max conns per host = 1 */
    g_object_set (self->priv->session,
                  SOUP_SESSION_MAX_CONNS_PER_HOST, 1, NULL);
  } else {
    /* default value */
    g_object_set (self->priv->session,
                  SOUP_SESSION_MAX_CONNS_PER_HOST, 2, NULL);
  }

  self->priv->throttling = throttling;
}

/**
 * grl_net_wc_set_cache:
 * @self: a #GrlNetWc instance
 * @use_cache: if cache must be used or not
 *
 * Sets if cache must be used. Note that this will only work if caching is
 * supporting.  If sets %TRUE, a new cache will be created. If sets to %FALSE,
 * current cache is clean and removed.
 *
 * Since: 0.1.12
 **/
void
grl_net_wc_set_cache (GrlNetWc *self,
                      gboolean use_cache)
{
  g_return_if_fail (GRL_IS_NET_WC (self));

  if (use_cache && !cache_is_available (self))
    cache_up (self);
  else if (!use_cache && cache_is_available (self))
    cache_down (self);
}

/**
 * grl_net_wc_set_cache_size:
 * @self: a #GrlNetWc instance
 * @cache_size: size of cache (in Mb)
 *
 * Sets the new maximum size of cache, in Megabytes. Default value is 10. Using
 * 0 means no cache will be done.
 *
 * Since: 0.1.12
 **/
void
grl_net_wc_set_cache_size (GrlNetWc *self,
                           guint size)
{
  g_return_if_fail (GRL_IS_NET_WC (self));

  if (self->priv->cache_size == size)
    return;

  self->priv->cache_size = size;

  SoupSessionFeature *cache = soup_session_get_feature (self->priv->session, SOUP_TYPE_CACHE);
  if (!cache)
    return;

  soup_cache_set_max_size (SOUP_CACHE (cache), size * 1024 * 1024);
}

/**
 * grl_net_wc_flush_delayed_requests:
 * @self: a #GrlNetWc instance
 *
 * This method will flush all the pending request in the queue.
 */
void
grl_net_wc_flush_delayed_requests (GrlNetWc *self)
{
  GrlNetWcPrivate *priv = self->priv;
  struct request_clos *c;

  g_return_if_fail (GRL_IS_NET_WC (self));

  while ((c = g_queue_pop_head (priv->pending))) {
    if (c->cancellable)
      g_cancellable_cancel (c->cancellable);
    /* This will call the destroy notify, request_clos_destroy()  */
    g_source_remove (c->source_id);
  }

  g_get_current_time (&priv->last_request);
}