/*
 *
 *  PACrunner - Proxy configuration daemon
 *
 *  Copyright (C) 2010-2011  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>

#include "pacrunner.h"

struct pacrunner_proxy {
	gint refcount;

	char *interface;
	enum pacrunner_proxy_method method;
	char *url;
	char *script;
	GList **servers;
	GList **excludes;
	gboolean browser_only;
	GList *domains;
	void *jsctx;
};

struct proxy_domain {
	char *domain;
	int proto;
	union {
		struct in_addr ip4;
		struct in6_addr ip6;
	} addr;
	int mask;
};

static GList *proxy_list = NULL;
static pthread_mutex_t proxy_mutex;
static pthread_cond_t proxy_cond;
static int timeout_source = 0;
static gint proxy_updating = -1; /* -1 for 'never set', with timeout */

struct pacrunner_proxy *pacrunner_proxy_create(const char *interface)
{
	struct pacrunner_proxy *proxy;

	DBG("interface %s", interface);

	proxy = g_try_new0(struct pacrunner_proxy, 1);
	if (!proxy)
		return NULL;

	proxy->refcount = 1;

	proxy->interface = g_strdup(interface);
	proxy->method = PACRUNNER_PROXY_METHOD_UNKNOWN;

	DBG("proxy %p", proxy);

	return proxy;
}

struct pacrunner_proxy *pacrunner_proxy_ref(struct pacrunner_proxy *proxy)
{
	DBG("proxy %p", proxy);

	if (!proxy)
		return NULL;

	g_atomic_int_inc(&proxy->refcount);

	return proxy;
}

static void proxy_domain_destroy(gpointer data)
{
	struct proxy_domain *domain = data;
	g_return_if_fail(domain != NULL);

	g_free(domain->domain);
	g_free(domain);
}

static void reset_proxy(struct pacrunner_proxy *proxy)
{
	DBG("proxy %p", proxy);

	g_free(proxy->url);
	proxy->url = NULL;

	g_free(proxy->script);
	proxy->script = NULL;

	__pacrunner_manual_destroy_servers(proxy->servers);
	proxy->servers = NULL;

	__pacrunner_manual_destroy_excludes(proxy->excludes);
	proxy->excludes = NULL;
}

void pacrunner_proxy_unref(struct pacrunner_proxy *proxy)
{
	DBG("proxy %p", proxy);

	if (!proxy)
		return;

	if (!g_atomic_int_dec_and_test(&proxy->refcount))
		return;

	__pacrunner_js_clear_proxy(proxy);

	reset_proxy(proxy);

	g_list_free_full(proxy->domains, proxy_domain_destroy);
	proxy->domains = NULL;

	g_free(proxy->interface);
	g_free(proxy);
}

const char *pacrunner_proxy_get_interface(struct pacrunner_proxy *proxy)
{
	DBG("proxy %p", proxy);

	if (!proxy)
		return NULL;

	return proxy->interface;
}

const char *pacrunner_proxy_get_script(struct pacrunner_proxy *proxy)
{
	DBG("proxy %p", proxy);

	if (!proxy)
		return NULL;

	return proxy->script;
}

static gboolean check_browser_protocol(const char *url)
{
	static const char *browser_schemes[] = {
		"http://",
		"https://",
		"ftp://",
		"nntp://",
		"nntps://",
	};
	guint i;

	for (i = 0; i < G_N_ELEMENTS(browser_schemes); i++) {
		if (strncmp(browser_schemes[i], url,
				strlen(browser_schemes[i])) == 0)
			return TRUE;
	}

	return FALSE;
}

int pacrunner_proxy_set_domains(struct pacrunner_proxy *proxy, char **domains,
					gboolean browser_only)
{
	int len;
	char *slash, **domain;
	char ip[INET6_ADDRSTRLEN + 1];

	DBG("proxy %p domains %p browser-only %u", proxy,
					domains, browser_only);

	proxy->browser_only = browser_only;

	if (!proxy)
		return -EINVAL;

	g_list_free_full(proxy->domains, proxy_domain_destroy);
	proxy->domains = NULL;

	if (!domains)
		return 0;

	for (domain = domains; *domain; domain++) {
		struct proxy_domain *data;

		data = g_malloc0(sizeof(struct proxy_domain));

		DBG("proxy %p domain %s", proxy, *domain);

		slash = strchr(*domain, '/');
		if (!slash) {
			data->domain = g_strdup(*domain);
			data->proto = 0;

			proxy->domains = g_list_append(proxy->domains, data);
			continue;
		}

		len = slash - *domain;
		if (len > INET6_ADDRSTRLEN) {
			g_free(data);
			continue;
		}

		strncpy(ip, *domain, len);
		ip[len] = '\0';

		if (inet_pton(AF_INET, ip, &(data->addr.ip4)) == 1) {
			data->domain = NULL;
			data->proto = 4;

			errno = 0;
			data->mask = strtol(slash + 1, NULL, 10);
			if (errno || data->mask < 0 || data->mask > 32) {
				g_free(data);
				continue;
			}

			proxy->domains = g_list_append(proxy->domains, data);
		} else if (inet_pton(AF_INET6, ip, &(data->addr.ip6)) == 1) {
			data->domain = NULL;
			data->proto = 6;

			errno = 0;
			data->mask = strtol(slash + 1, NULL, 10);
			if (errno || data->mask < 0 || data->mask > 128) {
				g_free(data);
				continue;
			}

			proxy->domains = g_list_append(proxy->domains, data);
		} else {
			g_free(data);
			continue;
		}
	}

	return 0;
}

static int set_method(struct pacrunner_proxy *proxy,
					enum pacrunner_proxy_method method)
{
	DBG("proxy %p method %d", proxy, method);

	if (!proxy)
		return -EINVAL;

	if (proxy->method == method)
		return 0;

	proxy->method = method;

	reset_proxy(proxy);

	return 0;
}

int pacrunner_proxy_set_direct(struct pacrunner_proxy *proxy)
{
	DBG("proxy %p", proxy);

	if (!proxy)
		return -EINVAL;

	pthread_mutex_lock(&proxy_mutex);
	if (proxy_updating == -1) {
		proxy_updating = 0;
		pthread_cond_broadcast(&proxy_cond);
	}
	pthread_mutex_unlock(&proxy_mutex);

	return set_method(proxy, PACRUNNER_PROXY_METHOD_DIRECT);
}

int pacrunner_proxy_set_manual(struct pacrunner_proxy *proxy,
					char **servers, char **excludes)
{
	int err;

	DBG("proxy %p servers %p excludes %p", proxy, servers, excludes);

	if (!proxy)
		return -EINVAL;

	if (!servers)
		return -EINVAL;

	err = set_method(proxy, PACRUNNER_PROXY_METHOD_MANUAL);
	if (err < 0)
		return err;

	proxy->servers = __pacrunner_manual_parse_servers(servers);
	if (!proxy->servers)
		return -EINVAL;

	proxy->excludes = __pacrunner_manual_parse_excludes(excludes);

	pacrunner_proxy_enable(proxy);

	return 0;
}

static void download_callback(char *content, void *user_data)
{
	struct pacrunner_proxy *proxy = user_data;

	DBG("url %s content %p", proxy->url, content);

	if (!content) {
		pacrunner_error("Failed to retrieve PAC script");
		goto done;
	}

	g_free(proxy->script);
	proxy->script = content;

	pacrunner_proxy_enable(proxy);

done:
	pthread_mutex_lock(&proxy_mutex);
	proxy_updating--;
	pthread_cond_broadcast(&proxy_cond);
	pthread_mutex_unlock(&proxy_mutex);
	pacrunner_proxy_unref(proxy);
}

int pacrunner_proxy_set_auto(struct pacrunner_proxy *proxy,
					const char *url, const char *script)
{
	int err;

	DBG("proxy %p url %s script %p", proxy, url, script);

	if (!proxy)
		return -EINVAL;

	err = set_method(proxy, PACRUNNER_PROXY_METHOD_AUTO);
	if (err < 0)
		return err;

	g_free(proxy->url);
	proxy->url = g_strdup(url);

	if (!proxy->url) {
		g_free(proxy->script);
		proxy->script = g_strdup(script);
	} else {
		g_free(proxy->script);
		proxy->script = NULL;
	}

	if (proxy->script) {
		pacrunner_proxy_enable(proxy);
		return 0;
	}

	pacrunner_proxy_ref(proxy);

	pthread_mutex_lock(&proxy_mutex);
	err = __pacrunner_download_update(proxy->interface, proxy->url,
						download_callback, proxy);
	if (err < 0) {
		pacrunner_proxy_unref(proxy);
		if (proxy_updating == -1) {
			proxy_updating = 0;
			pthread_cond_broadcast(&proxy_cond);
		}
		pthread_mutex_unlock(&proxy_mutex);
		return err;
	}
	if (proxy_updating == -1)
		proxy_updating = 1;
	else
		proxy_updating++;
	pthread_mutex_unlock(&proxy_mutex);

	return 0;
}

int pacrunner_proxy_enable(struct pacrunner_proxy *proxy)
{
	GList *list;

	DBG("proxy %p", proxy);

	if (!proxy)
		return -EINVAL;

	list = g_list_find(proxy_list, proxy);
	if (list)
		return -EEXIST;

	proxy = pacrunner_proxy_ref(proxy);
	if (!proxy)
		return -EIO;

	__pacrunner_js_set_proxy(proxy);

	pthread_mutex_lock(&proxy_mutex);
	if (proxy_updating == -1) {
		proxy_updating = 0;
		pthread_cond_broadcast(&proxy_cond);
	}
	proxy_list = g_list_append(proxy_list, proxy);
	pthread_mutex_unlock(&proxy_mutex);

	return 0;
}

int pacrunner_proxy_disable(struct pacrunner_proxy *proxy)
{
	GList *list;

	DBG("proxy %p", proxy);

	if (!proxy)
		return -EINVAL;

	list = g_list_find(proxy_list, proxy);
	if (!list)
		return -ENXIO;

	pthread_mutex_lock(&proxy_mutex);
	proxy_list = g_list_remove_link(proxy_list, list);
	g_list_free(list);
	pthread_mutex_unlock(&proxy_mutex);

	__pacrunner_js_clear_proxy(proxy);

	pacrunner_proxy_unref(proxy);

	return 0;
}

static int compare_legacy_ip_in_net(struct in_addr *host,
					struct proxy_domain *match)
{
	if (ntohl(host->s_addr ^ match->addr.ip4.s_addr) >> (32 - match->mask))
		return -1;

	return 0;
}

static int compare_ipv6_in_net(struct in6_addr *host,
					struct proxy_domain *match)
{
	int i, shift;

	for (i = 0; i < (match->mask)/8; i++) {
		if (host->s6_addr[i] != match->addr.ip6.s6_addr[i])
			return -1;
	}

	if ((match->mask) % 8) {
		/**
		 * If mask bits are not multiple of 8 , 1-7 bits are left
		 * to be compared.
		 */
		shift = 8 - (match->mask - (i*8));

		if ((host->s6_addr[i] >> shift) !=
			(match->addr.ip6.s6_addr[i] >> shift))
			return -1;
	}

	return 0;
}

static int compare_host_in_domain(const char *host, struct proxy_domain *match)
{
	size_t hlen = strlen(host);
	size_t dlen = strlen(match->domain);

	if ((hlen >= dlen) && (strcmp(host + (hlen - dlen),
						match->domain) == 0)) {
		if (hlen == dlen || host[hlen - dlen - 1] == '.')
			return 0;
	}

	return -1;
}

/**
 * A request for a "browser" protocol would match the following configs
 * order of preference (if they exist):
 * • Matching "Domains", BrowserOnly==TRUE
 * • Matching "Domains", BrowserOnly==FALSE
 * • Domains==NULL, BrowserOnly==TRUE
 * • Domains==NULL, BrowserOnly==FALSE
 *
 * A request for a non-browser protocol would match the following :
 * • Matching "Domains", BrowserOnly==FALSE
 * • Domains==NULL, BrowserOnly==FALSE (except if a config exists with
 * Matching "Domains", BrowserOnly==TRUE, in which case we need to
 * return NULL).
 **/
char *pacrunner_proxy_lookup(const char *url, const char *host)
{
	GList *l, *list;
	int protocol = 0;
	struct in_addr ip4_addr;
	struct in6_addr ip6_addr;
	gboolean request_is_browser;
	struct pacrunner_proxy *proxy = NULL;

	/* Four classes of 'match' */
	struct pacrunner_proxy *alldomains_browseronly = NULL;
	struct pacrunner_proxy *alldomains_allprotos = NULL;
	struct pacrunner_proxy *domainmatch_browseronly = NULL;
	struct pacrunner_proxy *domainmatch_allprotos = NULL;

	DBG("url %s host %s", url, host);

	pthread_mutex_lock(&proxy_mutex);
	while (proxy_updating)
		pthread_cond_wait(&proxy_cond, &proxy_mutex);

	if (!proxy_list) {
		pthread_mutex_unlock(&proxy_mutex);
		return NULL;
	}

	if (inet_pton(AF_INET, host, &ip4_addr) == 1) {
		protocol = 4;
	} else if (inet_pton(AF_INET6, host, &ip6_addr) == 1) {
		protocol = 6;
	} else if (host[0] == '[') {
		char ip[INET6_ADDRSTRLEN + 1];
		int len = strlen(host);

		if (len < INET6_ADDRSTRLEN + 2 && host[len - 1] == ']') {
			strncpy(ip, host + 1, len - 2);
			ip[len - 2] = '\0';

			if (inet_pton(AF_INET6, ip, &ip6_addr) == 1)
				protocol = 6;
		}
	}

	request_is_browser = check_browser_protocol(url);

	for (list = g_list_first(proxy_list); list; list = g_list_next(list)) {
		proxy = list->data;

		if (!proxy->domains) {
			if (proxy->browser_only && !alldomains_browseronly)
				alldomains_browseronly = proxy;
			else if (!proxy->browser_only && !alldomains_allprotos)
				alldomains_allprotos = proxy;
			continue;
		}

		for (l = g_list_first(proxy->domains); l; l = g_list_next(l)) {
			struct proxy_domain *data = l->data;

			if (data->proto != protocol)
				continue;

			switch (protocol) {
			case 4:
				if (compare_legacy_ip_in_net(&ip4_addr,
								data) == 0) {
					DBG("match proxy %p Legacy IP range %s",
					    proxy, data->domain);
					goto matches;
				}
				break;
			case 6:
				if (compare_ipv6_in_net(&ip6_addr,
							data) == 0) {
					DBG("match proxy %p IPv6 range %s",
					    proxy, data->domain);
					goto matches;
				}
				break;
			default:
				if (compare_host_in_domain(host, data) == 0) {
					DBG("match proxy %p DNS domain %s",
					    proxy, data->domain);
					goto matches;
				}
				break;
			}
		}
		/* No match */
		continue;

	matches:
		if (proxy->browser_only == request_is_browser) {
			goto found;
		} else if (proxy->browser_only) {
			/* A non-browser request will return DIRECT instead of
			 * falling back to alldomains_* if this exists.
			 */
			if (!domainmatch_browseronly)
				domainmatch_browseronly = proxy;
		} else {
			/* We might fall back to this, for a browser request */
			if (!domainmatch_allprotos)
				domainmatch_allprotos = proxy;
		}
	}

	if (request_is_browser) {
		/* We'll have bailed out immediately if we found a domain match
		 * with proxy->browser_only==TRUE. Fallbacks in order of prefe-
		 * rence.
		 */
		proxy = domainmatch_allprotos;
		if (!proxy)
			proxy = alldomains_browseronly;
		if (!proxy)
			proxy = alldomains_allprotos;
	} else {
		if (!domainmatch_browseronly)
			proxy = alldomains_allprotos;
		else
			proxy = NULL;
	}

 found:
	pthread_mutex_unlock(&proxy_mutex);

	if (!proxy)
		return NULL;

	switch (proxy->method) {
	case PACRUNNER_PROXY_METHOD_UNKNOWN:
	case PACRUNNER_PROXY_METHOD_DIRECT:
		break;
	case PACRUNNER_PROXY_METHOD_MANUAL:
		return __pacrunner_manual_execute(url, host, proxy->servers,
						  proxy->excludes);
	case PACRUNNER_PROXY_METHOD_AUTO:
		return __pacrunner_js_execute(proxy, url, host);
	}

	return NULL;
}

static gboolean proxy_config_timeout(gpointer user_data)
{
	DBG("");

	/* If ConnMan/NetworkManager/whatever hasn't given us a config within
	   a reasonable length of time, start responding 'DIRECT'. */
	if (proxy_updating == -1) {
		proxy_updating = 0;
		pthread_cond_broadcast(&proxy_cond);
	}
	return FALSE;
}

void __pacrunner_proxy_set_jsctx(struct pacrunner_proxy *proxy, void *jsctx)
{
	proxy->jsctx = jsctx;
}

void *__pacrunner_proxy_get_jsctx(struct pacrunner_proxy *proxy)
{
	return proxy->jsctx;
}

int __pacrunner_proxy_init(void)
{
	DBG("");

	pthread_mutex_init(&proxy_mutex, NULL);
	pthread_cond_init(&proxy_cond, NULL);

	timeout_source = g_timeout_add_seconds(5, proxy_config_timeout, NULL);

	return 0;
}

void __pacrunner_proxy_cleanup(void)
{
	GList *list;

	DBG("");

	for (list = g_list_first(proxy_list); list; list = g_list_next(list)) {
		struct pacrunner_proxy *proxy = list->data;

		DBG("proxy %p", proxy);

		if (proxy)
			pacrunner_proxy_unref(proxy);
	}

	pthread_mutex_destroy(&proxy_mutex);
	pthread_cond_destroy(&proxy_cond);

	g_list_free(proxy_list);
	proxy_list = NULL;

	if (timeout_source) {
		g_source_remove(timeout_source);
		timeout_source = 0;
	}
}
