/* GIO - GLib Input, Output and Streaming Library
 *
 * Copyright 2010 Red Hat, Inc.
 *
 * 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; 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
 * 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, see
 * <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <stdlib.h>

#include "gproxyresolvergnome.h"

#include <glib/gi18n-lib.h>
#include <gdesktop-enums.h>

#define GNOME_PROXY_SETTINGS_SCHEMA       "org.gnome.system.proxy"
#define GNOME_PROXY_MODE_KEY              "mode"
#define GNOME_PROXY_AUTOCONFIG_URL_KEY    "autoconfig-url"
#define GNOME_PROXY_IGNORE_HOSTS_KEY      "ignore-hosts"
#define GNOME_PROXY_USE_SAME_PROXY_KEY    "use-same-proxy"

#define GNOME_PROXY_HTTP_CHILD_SCHEMA     "http"
#define GNOME_PROXY_HTTP_HOST_KEY         "host"
#define GNOME_PROXY_HTTP_PORT_KEY         "port"
#define GNOME_PROXY_HTTP_USE_AUTH_KEY     "use-authentication"
#define GNOME_PROXY_HTTP_USER_KEY         "authentication-user"
#define GNOME_PROXY_HTTP_PASSWORD_KEY     "authentication-password"

#define GNOME_PROXY_HTTPS_CHILD_SCHEMA    "https"
#define GNOME_PROXY_HTTPS_HOST_KEY        "host"
#define GNOME_PROXY_HTTPS_PORT_KEY        "port"

#define GNOME_PROXY_FTP_CHILD_SCHEMA      "ftp"
#define GNOME_PROXY_FTP_HOST_KEY          "host"
#define GNOME_PROXY_FTP_PORT_KEY          "port"

#define GNOME_PROXY_SOCKS_CHILD_SCHEMA    "socks"
#define GNOME_PROXY_SOCKS_HOST_KEY        "host"
#define GNOME_PROXY_SOCKS_PORT_KEY        "port"

/* We have to has-a GSimpleProxyResolver rather than is-a one,
 * because a dynamic type cannot reimplement an interface that
 * its parent also implements... for some reason.
 */

struct _GProxyResolverGnome {
  GObject parent_instance;

  GProxyResolver *base_resolver;

  GSettings *proxy_settings;
  GSettings *http_settings;
  GSettings *https_settings;
  GSettings *ftp_settings;
  GSettings *socks_settings;
  gboolean need_update;

  GDesktopProxyMode mode;
  gchar *autoconfig_url;
  gboolean use_same_proxy;

  GDBusProxy *pacrunner;

  GMutex lock;
};

static GProxyResolverInterface *g_proxy_resolver_gnome_parent_iface;

static void g_proxy_resolver_gnome_iface_init (GProxyResolverInterface *iface);

G_DEFINE_DYNAMIC_TYPE_EXTENDED (GProxyResolverGnome,
				g_proxy_resolver_gnome,
				G_TYPE_OBJECT, 0,
				G_IMPLEMENT_INTERFACE_DYNAMIC (G_TYPE_PROXY_RESOLVER,
							       g_proxy_resolver_gnome_iface_init))

static void
g_proxy_resolver_gnome_class_finalize (GProxyResolverGnomeClass *klass)
{
}

static void
gsettings_changed (GSettings   *settings,
		   const gchar *key,
		   gpointer     user_data)
{
  GProxyResolverGnome *resolver = user_data;

  g_mutex_lock (&resolver->lock);
  resolver->need_update = TRUE;
  g_mutex_unlock (&resolver->lock);
}

static void
g_proxy_resolver_gnome_finalize (GObject *object)
{
  GProxyResolverGnome *resolver = G_PROXY_RESOLVER_GNOME (object);

  if (resolver->proxy_settings)
    {
      g_signal_handlers_disconnect_by_func (resolver->proxy_settings,
					    (gpointer)gsettings_changed,
					    resolver);
      g_object_unref (resolver->proxy_settings);

      g_signal_handlers_disconnect_by_func (resolver->http_settings,
					    (gpointer)gsettings_changed,
					    resolver);
      g_object_unref (resolver->http_settings);

      g_signal_handlers_disconnect_by_func (resolver->https_settings,
					    (gpointer)gsettings_changed,
					    resolver);
      g_object_unref (resolver->https_settings);

      g_signal_handlers_disconnect_by_func (resolver->ftp_settings,
					    (gpointer)gsettings_changed,
					    resolver);
      g_object_unref (resolver->ftp_settings);

      g_signal_handlers_disconnect_by_func (resolver->socks_settings,
					    (gpointer)gsettings_changed,
					    resolver);
      g_object_unref (resolver->socks_settings);
    }

  g_clear_object (&resolver->base_resolver);
  g_clear_object (&resolver->pacrunner);

  g_free (resolver->autoconfig_url);

  g_mutex_clear (&resolver->lock);

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

static void
g_proxy_resolver_gnome_init (GProxyResolverGnome *resolver)
{
  g_mutex_init (&resolver->lock);

  resolver->base_resolver = g_simple_proxy_resolver_new (NULL, NULL);

  resolver->proxy_settings = g_settings_new (GNOME_PROXY_SETTINGS_SCHEMA);
  g_signal_connect (resolver->proxy_settings, "changed",
		    G_CALLBACK (gsettings_changed), resolver);
  resolver->http_settings = g_settings_get_child (resolver->proxy_settings,
                                                  GNOME_PROXY_HTTP_CHILD_SCHEMA);
  g_signal_connect (resolver->http_settings, "changed",
		    G_CALLBACK (gsettings_changed), resolver);
  resolver->https_settings = g_settings_get_child (resolver->proxy_settings,
                                                   GNOME_PROXY_HTTPS_CHILD_SCHEMA);
  g_signal_connect (resolver->https_settings, "changed",
		    G_CALLBACK (gsettings_changed), resolver);
  resolver->ftp_settings = g_settings_get_child (resolver->proxy_settings,
                                                 GNOME_PROXY_FTP_CHILD_SCHEMA);
  g_signal_connect (resolver->ftp_settings, "changed",
		    G_CALLBACK (gsettings_changed), resolver);
  resolver->socks_settings = g_settings_get_child (resolver->proxy_settings,
                                                   GNOME_PROXY_SOCKS_CHILD_SCHEMA);
  g_signal_connect (resolver->socks_settings, "changed",
		    G_CALLBACK (gsettings_changed), resolver);

  resolver->need_update = TRUE;
}

/* called with lock held */
static void
update_settings (GProxyResolverGnome *resolver)
{
  GSimpleProxyResolver *simple = G_SIMPLE_PROXY_RESOLVER (resolver->base_resolver);
  gchar **ignore_hosts;
  gchar *host, *http_proxy, *proxy;
  guint port;

  resolver->need_update = FALSE;

  g_free (resolver->autoconfig_url);
  g_simple_proxy_resolver_set_default_proxy (simple, NULL);
  g_simple_proxy_resolver_set_ignore_hosts (simple, NULL);
  g_simple_proxy_resolver_set_uri_proxy (simple, "http", NULL);
  g_simple_proxy_resolver_set_uri_proxy (simple, "https", NULL);
  g_simple_proxy_resolver_set_uri_proxy (simple, "ftp", NULL);

  resolver->mode =
    g_settings_get_enum (resolver->proxy_settings, GNOME_PROXY_MODE_KEY);
  resolver->autoconfig_url =
    g_settings_get_string (resolver->proxy_settings, GNOME_PROXY_AUTOCONFIG_URL_KEY);

  if (resolver->mode == G_DESKTOP_PROXY_MODE_AUTO && !resolver->pacrunner)
    {
      GError *error = NULL;
      resolver->pacrunner =
	g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
				       G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
				       G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
				       NULL,
				       "org.gtk.GLib.PACRunner",
				       "/org/gtk/GLib/PACRunner",
				       "org.gtk.GLib.PACRunner",
				       NULL, &error);
      if (error)
	{
	  g_warning ("Could not start proxy autoconfiguration helper:"
		     "\n    %s\nProxy autoconfiguration will not work",
		     error->message);
	}
    }
  else if (resolver->mode != G_DESKTOP_PROXY_MODE_AUTO && resolver->pacrunner)
    {
      g_object_unref (resolver->pacrunner);
      resolver->pacrunner = NULL;
    }

  ignore_hosts =
    g_settings_get_strv (resolver->proxy_settings, GNOME_PROXY_IGNORE_HOSTS_KEY);
  g_simple_proxy_resolver_set_ignore_hosts (simple, ignore_hosts);
  g_strfreev (ignore_hosts);

  if (resolver->mode == G_DESKTOP_PROXY_MODE_AUTO)
    {
      /* We use the base_resolver to handle ignore_hosts in the AUTO case,
       * so we have to set a non-"direct://" default proxy so we can distinguish
       * the two cases.
       */
       g_simple_proxy_resolver_set_default_proxy (simple, "use-proxy:");
    }

  if (resolver->mode != G_DESKTOP_PROXY_MODE_MANUAL)
    return;

  host = g_settings_get_string (resolver->http_settings, GNOME_PROXY_HTTP_HOST_KEY);
  port = g_settings_get_int (resolver->http_settings, GNOME_PROXY_HTTP_PORT_KEY);
  if (host && *host)
    {
      if (g_settings_get_boolean (resolver->http_settings, GNOME_PROXY_HTTP_USE_AUTH_KEY))
	{
	  gchar *user, *password;
	  gchar *enc_user, *enc_password;

	  user = g_settings_get_string (resolver->http_settings, GNOME_PROXY_HTTP_USER_KEY);
	  enc_user = g_uri_escape_string (user, NULL, TRUE);
	  g_free (user);
	  password = g_settings_get_string (resolver->http_settings, GNOME_PROXY_HTTP_PASSWORD_KEY);
	  enc_password = g_uri_escape_string (password, NULL, TRUE);
	  g_free (password);

	  http_proxy = g_strdup_printf ("http://%s:%s@%s:%u",
					enc_user, enc_password,
					host, port);
	  g_free (enc_user);
	  g_free (enc_password);
	}
      else
	http_proxy = g_strdup_printf ("http://%s:%u", host, port);

      g_simple_proxy_resolver_set_uri_proxy (simple, "http", http_proxy);
      if (g_settings_get_boolean (resolver->proxy_settings, GNOME_PROXY_USE_SAME_PROXY_KEY))
	g_simple_proxy_resolver_set_default_proxy (simple, http_proxy);
    }
  else
    http_proxy = NULL;
  g_free (host);

  host = g_settings_get_string (resolver->https_settings, GNOME_PROXY_HTTPS_HOST_KEY);
  port = g_settings_get_int (resolver->https_settings, GNOME_PROXY_HTTPS_PORT_KEY);
  if (host && *host)
    {
      proxy = g_strdup_printf ("http://%s:%u", host, port);
      g_simple_proxy_resolver_set_uri_proxy (simple, "https", proxy);
      g_free (proxy);
    }
  else if (http_proxy)
    g_simple_proxy_resolver_set_uri_proxy (simple, "https", http_proxy);
  g_free (host);

  host = g_settings_get_string (resolver->socks_settings, GNOME_PROXY_SOCKS_HOST_KEY);
  port = g_settings_get_int (resolver->socks_settings, GNOME_PROXY_SOCKS_PORT_KEY);
  if (host && *host)
    {
      proxy = g_strdup_printf ("socks://%s:%u", host, port);
      g_simple_proxy_resolver_set_default_proxy (simple, proxy);
      g_free (proxy);
    }
  g_free (host);

  g_free (http_proxy);

  host = g_settings_get_string (resolver->ftp_settings, GNOME_PROXY_FTP_HOST_KEY);
  port = g_settings_get_int (resolver->ftp_settings, GNOME_PROXY_FTP_PORT_KEY);
  if (host && *host)
    {
      proxy = g_strdup_printf ("ftp://%s:%u", host, port);
      g_simple_proxy_resolver_set_uri_proxy (simple, "ftp", proxy);
      g_free (proxy);
    }
  g_free (host);
}

static gboolean
g_proxy_resolver_gnome_is_supported (GProxyResolver *object)
{
  const char *session;

  if (g_getenv ("GNOME_DESKTOP_SESSION_ID"))
    return TRUE;

  session = g_getenv ("DESKTOP_SESSION");
  if (session == NULL)
    return FALSE;

  return g_str_has_prefix (session, "gnome") ||
         strcmp (session, "ubuntu") == 0;
}

static inline gchar **
make_proxies (const gchar *proxy)
{
  gchar **proxies;

  proxies = g_new (gchar *, 2);
  proxies[0] = g_strdup (proxy);
  proxies[1] = NULL;

  return proxies;
}

/* Threadsafely determines what to do with @uri; returns %FALSE if an
 * error occurs, %TRUE and an array of proxies if the mode is NONE or
 * MANUAL, or if @uri is covered by ignore-hosts, or %TRUE and a
 * (transfer-full) pacrunner and autoconfig url if the mode is AUTOMATIC.
 */
static gboolean
g_proxy_resolver_gnome_lookup_internal (GProxyResolverGnome   *resolver,
					const gchar           *uri,
					gchar               ***out_proxies,
					GDBusProxy           **out_pacrunner,
					gchar                **out_autoconfig_url,
					GCancellable          *cancellable,
					GError               **error)
{
  gchar **proxies = NULL;

  *out_proxies = NULL;
  *out_pacrunner = NULL;
  *out_autoconfig_url = NULL;

  g_mutex_lock (&resolver->lock);
  if (resolver->need_update)
    update_settings (resolver);

  proxies = g_proxy_resolver_lookup (resolver->base_resolver,
				     uri, cancellable, error);
  if (!proxies)
    goto done;

  /* Parent class does ignore-host handling */
  if (!strcmp (proxies[0], "direct://") && !proxies[1])
    goto done;

  if (resolver->pacrunner)
    {
      g_clear_pointer (&proxies, g_strfreev);
      *out_pacrunner = g_object_ref (resolver->pacrunner);
      *out_autoconfig_url = g_strdup (resolver->autoconfig_url);
      goto done;
    }

 done:
  g_mutex_unlock (&resolver->lock);

  if (proxies)
    {
      *out_proxies = proxies;
      return TRUE;
    }
  else if (*out_pacrunner)
    return TRUE;
  else
    return FALSE;
}

static gchar **
g_proxy_resolver_gnome_lookup (GProxyResolver  *proxy_resolver,
			       const gchar     *uri,
			       GCancellable    *cancellable,
			       GError         **error)
{
  GProxyResolverGnome *resolver = G_PROXY_RESOLVER_GNOME (proxy_resolver);
  GDBusProxy *pacrunner;
  gchar **proxies, *autoconfig_url;

  if (!g_proxy_resolver_gnome_lookup_internal (resolver, uri,
					       &proxies, &pacrunner, &autoconfig_url,
					       cancellable, error))
    return NULL;

  if (pacrunner)
    {
      GVariant *vproxies;

      vproxies = g_dbus_proxy_call_sync (pacrunner,
					 "Lookup",
					 g_variant_new ("(ss)",
							autoconfig_url,
							uri),
					 G_DBUS_CALL_FLAGS_NONE,
					 -1,
					 cancellable, error);
      if (vproxies)
	{
	  g_variant_get (vproxies, "(^as)", &proxies);
	  g_variant_unref (vproxies);
	}
      else
	proxies = NULL;

      g_object_unref (pacrunner);
      g_free (autoconfig_url);
    }

  return proxies;
}

static void
got_autoconfig_proxies (GObject      *source,
			GAsyncResult *result,
			gpointer      user_data)
{
  GTask *task = user_data;
  GVariant *vproxies;
  char **proxies;
  GError *error = NULL;

  vproxies = g_dbus_proxy_call_finish (G_DBUS_PROXY (source),
				       result, &error);
  if (vproxies)
    {
      g_variant_get (vproxies, "(^as)", &proxies);
      g_task_return_pointer (task, proxies, (GDestroyNotify)g_strfreev);
      g_variant_unref (vproxies);
    }
  else
    g_task_return_error (task, error);
  g_object_unref (task);
}

static void
g_proxy_resolver_gnome_lookup_async (GProxyResolver      *proxy_resolver,
				     const gchar         *uri,
				     GCancellable        *cancellable,
				     GAsyncReadyCallback  callback,
				     gpointer             user_data)
{
  GProxyResolverGnome *resolver = G_PROXY_RESOLVER_GNOME (proxy_resolver);
  GTask *task;
  char **proxies, *autoconfig_url;
  GDBusProxy *pacrunner;
  GError *error = NULL;

  task = g_task_new (resolver, cancellable, callback, user_data);
  g_task_set_source_tag (task, g_proxy_resolver_gnome_lookup_async);

   if (!g_proxy_resolver_gnome_lookup_internal (resolver, uri,
						&proxies, &pacrunner, &autoconfig_url,
						cancellable, &error))
     {
       g_task_return_error (task, error);
       g_object_unref (task);
       return;
     }
   else if (proxies)
     {
       g_task_return_pointer (task, proxies, (GDestroyNotify)g_strfreev);
       g_object_unref (task);
       return;
     }

   g_dbus_proxy_call (pacrunner,
		      "Lookup",
		      g_variant_new ("(ss)",
				     autoconfig_url,
				     uri),
		      G_DBUS_CALL_FLAGS_NONE,
		      -1,
		      cancellable,
		      got_autoconfig_proxies,
		      task);
   g_object_unref (pacrunner);
   g_free (autoconfig_url);
}

static gchar **
g_proxy_resolver_gnome_lookup_finish (GProxyResolver  *resolver,
				      GAsyncResult    *result,
				      GError         **error)
{
  g_return_val_if_fail (g_task_is_valid (result, resolver), NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}

static void
g_proxy_resolver_gnome_class_init (GProxyResolverGnomeClass *resolver_class)
{
  GObjectClass *object_class;
  
  object_class = G_OBJECT_CLASS (resolver_class);
  object_class->finalize = g_proxy_resolver_gnome_finalize;
}

static void
g_proxy_resolver_gnome_iface_init (GProxyResolverInterface *iface)
{
  g_proxy_resolver_gnome_parent_iface = g_type_interface_peek_parent (iface);

  iface->is_supported = g_proxy_resolver_gnome_is_supported;
  iface->lookup = g_proxy_resolver_gnome_lookup;
  iface->lookup_async = g_proxy_resolver_gnome_lookup_async;
  iface->lookup_finish = g_proxy_resolver_gnome_lookup_finish;
}

void
g_proxy_resolver_gnome_register (GIOModule *module)
{
  g_proxy_resolver_gnome_register_type (G_TYPE_MODULE (module));
  g_io_extension_point_implement (G_PROXY_RESOLVER_EXTENSION_POINT_NAME,
				  g_proxy_resolver_gnome_get_type(),
				  "gnome",
				  80);
}
