/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Copyright (C) 2004 Imendio AB
 * Copyright (C) 2004 Josh Beam <josh@3ddrome.com>
 *
 * This program 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 program 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 program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <config.h>

#include <glib.h>
#include <string.h>

#ifndef G_OS_WIN32

#include <unistd.h>
#include <sys/socket.h>

#else  /* G_OS_WIN32 */

#include <winsock2.h>

#endif /* G_OS_WIN32 */

#include "lm-internals.h"
#include "lm-proxy.h"
#include "lm-utils.h"

struct _LmProxy {
	LmProxyType  type;
	gchar       *server;
	guint        port;
	gchar       *username;
	gchar       *password;
	guint        io_watch;

        gint         ref_count;
};

static void          proxy_free              (LmProxy       *proxy);
static gboolean      proxy_http_negotiate    (LmProxy       *proxy,
					      gint           fd, 
					      const gchar   *server,
					      guint          port);
static gboolean      proxy_negotiate         (LmProxy       *proxy,
					      gint           fd,
					      const gchar   *server,
					      guint          port);
static gboolean      proxy_http_read_cb      (GIOChannel    *source,
					      GIOCondition   condition,
					      gpointer       data);
static gboolean      proxy_read_cb           (GIOChannel    *source,
                                              GIOCondition   condition,
                                              gpointer       data);

static void
proxy_free (LmProxy *proxy)
{
	g_free (proxy->server);
	g_free (proxy->username);
	g_free (proxy->password);

	g_free (proxy);
}

static gboolean
proxy_http_negotiate (LmProxy *proxy, gint fd, const gchar *server, guint port)
{
	gchar *str;

	if (proxy->username && proxy->password) {
		gchar *tmp1;
		gchar *tmp2;

		tmp1 = g_strdup_printf ("%s:%s",
					proxy->username, 
					proxy->password);
		tmp2 = _lm_utils_base64_encode (tmp1);
		g_free (tmp1);

		str = g_strdup_printf ("CONNECT %s:%u HTTP/1.1\r\nHost: %s:%u\r\nProxy-Authorization: Basic %s\r\n\r\n",
				       server, port,
				       server, port,
				       tmp2);
		g_free (tmp2);
	} else {
		str = g_strdup_printf ("CONNECT %s:%u HTTP/1.1\r\nHost: %s:%u\r\n\r\n",
				       server, port, 
				       server, port);
	}

	send (fd, str, strlen (str), 0);
	g_free (str);
	return TRUE;
}

/* returns TRUE when connected through proxy */
static gboolean
proxy_http_read_cb (GIOChannel *source, GIOCondition condition, gpointer data)
{
	gchar          buf[512];
	gsize          bytes_read;
	GError        *error = NULL;

	g_io_channel_read_chars (source, buf, 512, &bytes_read, &error);

	if (bytes_read < 16) {
		return FALSE;
	}

	if (strncmp (buf, "HTTP/1.1 200", 12) != 0 &&
	    strncmp (buf, "HTTP/1.0 200", 12) != 0) {
		return FALSE;
	}

	if (strncmp (buf + (bytes_read - 4), "\r\n\r\n", 4) != 0) {
		return FALSE;
	}

	return TRUE;
}

static gboolean
proxy_read_cb (GIOChannel *source, GIOCondition condition, gpointer data)
{
	LmConnectData *connect_data;
	LmConnection  *connection;
	LmProxy       *proxy;
	gboolean       retval = FALSE;

	connect_data = (LmConnectData *) data;
	connection = connect_data->connection;
	proxy = lm_connection_get_proxy (connection);

	g_return_val_if_fail (proxy != NULL, FALSE);

	if (lm_connection_is_open (connection)) {
		return FALSE;
	}

	switch (lm_proxy_get_type (proxy)) {
	default:
	case LM_PROXY_TYPE_NONE:
		g_assert_not_reached ();
		break;
	case LM_PROXY_TYPE_HTTP:
		retval = proxy_http_read_cb (source, condition, data);
		break;
	}

	if (retval == TRUE) {
		g_source_remove (proxy->io_watch);
		_lm_socket_succeeded ((LmConnectData *) data);
	}

	return FALSE;
}

gboolean
proxy_negotiate (LmProxy *proxy, gint fd, const gchar *server, guint port)
{
	g_return_val_if_fail (proxy != NULL, FALSE);

	switch (proxy->type) {
	case LM_PROXY_TYPE_NONE:
		return TRUE;
	case LM_PROXY_TYPE_HTTP:
		return proxy_http_negotiate (proxy, fd, server, port);
	default:
		g_assert_not_reached ();
	}

	return FALSE;
}

gboolean
_lm_proxy_connect_cb (GIOChannel *source, GIOCondition condition, gpointer data)
{
	LmConnection  *connection;
	LmConnectData *connect_data;
	LmProxy       *proxy;
	int            error;
	socklen_t      len;

	connect_data = (LmConnectData *) data;
	connection = connect_data->connection;
	proxy = lm_connection_get_proxy (connection);

	g_return_val_if_fail (proxy != NULL, FALSE);

	if (condition == G_IO_ERR) {
		len = sizeof (error);
		_lm_sock_get_error (connect_data->fd, &error, &len);
		_lm_socket_failed_with_error (connect_data, error);
		return FALSE;
	} else if (condition == G_IO_OUT) {
		if (!proxy_negotiate (lm_connection_get_proxy (connection), connect_data->fd, lm_connection_get_server (connection), lm_connection_get_port (connection))) {
			_lm_socket_failed (connect_data);
			return FALSE;
		}
			
		proxy->io_watch = g_io_add_watch (connect_data->io_channel,
						  G_IO_IN|G_IO_ERR,
						  (GIOFunc) proxy_read_cb,
						  connect_data);
	} else {
		g_assert_not_reached ();
	}

	return FALSE;
}

/**
 * lm_proxy_new
 * @type: the type of the new proxy
 * 
 * Creates a new Proxy. Used #lm_connection_set_proxy to make a connection 
 * user this proxy.
 * 
 * Return value: a newly create proxy
 **/
LmProxy * 
lm_proxy_new (LmProxyType type)
{
	LmProxy *proxy;

	proxy = g_new0 (LmProxy, 1);
	
	proxy->ref_count = 1;
	proxy->type = type;

	switch (proxy->type) {
	case LM_PROXY_TYPE_HTTP:
		proxy->port = 8000;
		break;
	default:
		proxy->port = 0;
	}

	return proxy;
}

/**
 * lm_proxy_new_with_server
 * @type: the type of the new proxy
 * @server: the proxy server
 * @port: the proxy server port
 * 
 * Creates a new Proxy. Use #lm_connection_set_proxy to make a connection 
 * user this proxy.
 * 
 * Return value: a newly create proxy
 **/
LmProxy *
lm_proxy_new_with_server (LmProxyType  type,
			  const gchar *server,
			  guint        port)
{
	LmProxy *proxy;

	proxy = lm_proxy_new (type);
	lm_proxy_set_server (proxy, server);
	lm_proxy_set_port (proxy, port);

	return proxy;
}

/**
 * lm_proxy_get_type
 * @proxy: an #LmProxy
 * 
 * Fetches the proxy type
 * 
 * Return value: the type 
 **/
LmProxyType
lm_proxy_get_type (LmProxy *proxy)
{
	g_return_val_if_fail (proxy != NULL, LM_PROXY_TYPE_NONE);

	return proxy->type;
}

/**
 * lm_proxy_set_type
 * @proxy: an #LmProxy
 * @type: an LmProxyType
 *
 * Sets the proxy type for @proxy to @type. 
 **/
void
lm_proxy_set_type (LmProxy *proxy, LmProxyType type)
{
	g_return_if_fail (proxy != NULL);

	proxy->type = type;
}

/**
 * lm_proxy_get_server:
 * @proxy: an #LmProxy
 * 
 * Fetches the server address that @proxy is using.
 * 
 * Return value: the proxy server address
 **/
const gchar *
lm_proxy_get_server (LmProxy *proxy)
{
	g_return_val_if_fail (proxy != NULL, NULL);
	
	return proxy->server;
}

/**
 * lm_proxy_set_server:
 * @proxy: an #LmProxy
 * @server: Address of the proxy server
 * 
 * Sets the server address for @proxy to @server. 
 **/
void
lm_proxy_set_server (LmProxy *proxy, const gchar *server)
{
	g_return_if_fail (proxy != NULL);
	g_return_if_fail (server != NULL);
	
	g_free (proxy->server);
	proxy->server = _lm_utils_hostname_to_punycode (server);
}

/**
 * lm_proxy_get_port:
 * @proxy: an #LmProxy
 * 
 * Fetches the port that @proxy is using.
 * 
 * Return value: The port 
 **/
guint
lm_proxy_get_port (LmProxy *proxy)
{
	g_return_val_if_fail (proxy != NULL, 0);

	return proxy->port;
}

/**
 * lm_proxy_set_port:
 * @proxy: an #LmProxy
 * @port: proxy server port
 * 
 * Sets the server port that @proxy will be using.
 **/
void
lm_proxy_set_port (LmProxy *proxy, guint port)
{
	g_return_if_fail (proxy != NULL);
	
	proxy->port = port;
}

/**
 * lm_proxy_get_username:
 * @proxy: an #LmProxy
 * 
 * Fetches the username that @proxy is using.
 * 
 * Return value: the username
 **/
const gchar *
lm_proxy_get_username (LmProxy *proxy)
{
	g_return_val_if_fail (proxy != NULL, NULL);
	
	return proxy->username;
}

/**
 * lm_proxy_set_username:
 * @proxy: an #LmProxy
 * @username: Username
 * 
 * Sets the username for @proxy to @username or %NULL to unset.  
 **/
void
lm_proxy_set_username (LmProxy *proxy, const gchar *username)
{
	g_return_if_fail (proxy != NULL);
	
	g_free (proxy->username);
	
	if (username) {
		proxy->username = g_strdup (username);
	} else {
		proxy->username = NULL;
	}
}
/**
 * lm_proxy_get_password:
 * @proxy: an #LmProxy
 * 
 * Fetches the password that @proxy is using.
 * 
 * Return value: the proxy password
 **/
const gchar *
lm_proxy_get_password (LmProxy *proxy)
{
	g_return_val_if_fail (proxy != NULL, NULL);
	
	return proxy->password;
}

/**
 * lm_proxy_set_password:
 * @proxy: an #LmProxy
 * @password: Password
 * 
 * Sets the password for @proxy to @password or %NULL to unset. 
 **/
void
lm_proxy_set_password (LmProxy *proxy, const gchar *password)
{
	g_return_if_fail (proxy != NULL);
	
	g_free (proxy->password);

	if (password) {
		proxy->password = g_strdup (password);
	} else {
		proxy->password = NULL;
	}
}

/**
 * lm_proxy_ref:
 * @proxy: an #LmProxy
 * 
 * Adds a reference to @proxy.
 * 
 * Return value: the proxy
 **/
LmProxy *
lm_proxy_ref (LmProxy *proxy)
{
	g_return_val_if_fail (proxy != NULL, NULL);
	
	proxy->ref_count++;
	return proxy;
}

/**
 * lm_proxy_unref
 * @proxy: an #LmProxy
 * 
 * Removes a reference from @proxy. When no more references are present
 * @proxy is freed.
 **/
void
lm_proxy_unref (LmProxy *proxy)
{
	g_return_if_fail (proxy != NULL);
	
	proxy->ref_count--;

	if (proxy->ref_count == 0) {
		proxy_free (proxy);
        }
}
