/*******************************************************************************
 * libproxy - A library for proxy configuration
 * Copyright (C) 2006 Nathaniel McCallum <nathaniel@natemccallum.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; either
 * 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 Street, Fifth Floor, Boston, MA  02110-1301 USA
 ******************************************************************************/

#include <cstdio>         // For fileno(), fread(), pclose(), popen(), sscanf()
#include <sys/select.h>   // For select()
#include <fcntl.h>        // For fcntl()
#include <errno.h>        // For errno stuff
#include <sys/types.h>    // For stat()
#include <sys/stat.h>     // For stat()
#include <unistd.h>       // For pipe(), close(), vfork(), dup(), execl(), _exit()
#include <signal.h>       // For kill()

#include "../extension_config.hpp"
using namespace libproxy;

#define BUFFERSIZE 10240

#define PROXY_MODE					"/system/proxy/mode"
#define PROXY_USE_AUTHENTICATION	"/system/http_proxy/use_authentication"
#define PROXY_AUTH_PASSWORD			"/system/http_proxy/authentication_password"
#define PROXY_AUTH_USER				"/system/http_proxy/authentication_user"
#define PROXY_AUTOCONFIG_URL		"/system/proxy/autoconfig_url"
#define PROXY_IGNORE_HOSTS			"/system/http_proxy/ignore_hosts"
#define PROXY_HTTP_HOST				"/system/http_proxy/host"
#define PROXY_HTTP_PORT				"/system/http_proxy/port"
#define PROXY_FTP_HOST				"/system/proxy/ftp_host"
#define PROXY_FTP_PORT				"/system/proxy/ftp_port"
#define PROXY_SECURE_HOST			"/system/proxy/secure_host"
#define PROXY_SECURE_PORT			"/system/proxy/secure_port"
#define PROXY_SOCKS_HOST			"/system/proxy/socks_host"
#define PROXY_SOCKS_PORT			"/system/proxy/socks_port"
#define PROXY_SAME_FOR_ALL          "/system/http_proxy/use_same_proxy"

static const char *all_keys[] = {
	PROXY_MODE,
	PROXY_USE_AUTHENTICATION,
	PROXY_AUTH_PASSWORD,
	PROXY_AUTH_USER,
	PROXY_AUTOCONFIG_URL,
	PROXY_IGNORE_HOSTS,
	PROXY_HTTP_HOST,
	PROXY_HTTP_PORT,
	PROXY_FTP_HOST,
	PROXY_FTP_PORT,
	PROXY_SECURE_HOST,
	PROXY_SECURE_PORT,
	PROXY_SOCKS_HOST,
	PROXY_SOCKS_PORT,
	PROXY_SAME_FOR_ALL,
	NULL
};

static int popen2(const char *program, FILE** read, FILE** write, pid_t* pid) {
	if (!read || !write || !pid || !program || !*program)
		return EINVAL;
	*read  = NULL;
	*write = NULL;
	*pid   = 0;

	// Open the pipes
	int rpipe[2];
	int wpipe[2];
	if (pipe(rpipe) < 0)
		return errno;
	if (pipe(wpipe) < 0) {
		close(rpipe[0]);
		close(rpipe[1]);
		return errno;
	}

	switch (*pid = vfork()) {
	case -1: // Error
		close(rpipe[0]);
		close(rpipe[1]);
		close(wpipe[0]);
		close(wpipe[1]);
		return errno;

	case 0: // Child
		close(STDIN_FILENO);  // Close stdin
		close(STDOUT_FILENO); // Close stdout

		// Dup the read end of the write pipe to stdin
		// Dup the write end of the read pipe to stdout
		if (dup2(wpipe[0], STDIN_FILENO)  != STDIN_FILENO)  _exit(1);
		if (dup2(rpipe[1], STDOUT_FILENO) != STDOUT_FILENO) _exit(2);

		// Close unneeded fds
		close(rpipe[0]);
		close(rpipe[1]);
		close(wpipe[0]);
		close(wpipe[1]);

		// Exec
		execl("/bin/sh", "sh", "-c", program, (char*) NULL);
		_exit(127);  // Whatever we do, don't return

	default: // Parent
		close(rpipe[1]);
		close(wpipe[0]);
		*read  = fdopen(rpipe[0], "r");
		*write = fdopen(wpipe[1], "w");
		if (*read == NULL || *write == NULL) {
			if (*read  != NULL) fclose(*read);
			if (*write != NULL) fclose(*write);
			return errno;
		}
		return 0;
	}
}

static inline uint16_t get_port(string &port)
{
	uint16_t retval;

	if (sscanf(port.c_str(), "%hu", &retval) != 1)
		retval = 0;

	return retval;	
}

class gnome_config_extension : public config_extension {
public:
	gnome_config_extension() {
		// Build the command
		int count;
		struct stat st;
		string cmd = LIBEXECDIR "/pxgconf";
		const char *pxgconf = getenv("PX_GCONF");

		if (pxgconf)
			cmd = string (pxgconf);

		if (stat(cmd.c_str(), &st))
			throw runtime_error ("Unable to open gconf helper!");

		for (count=0 ; all_keys[count] ; count++)
			cmd += string(" ", 1) + all_keys[count];

		// Get our pipes
		if (popen2(cmd.c_str(), &this->read, &this->write, &this->pid) != 0)
			throw runtime_error("Unable to run gconf helper!");

		// Read in our initial data
		this->read_data(count);

		// Set the read pipe to non-blocking
		if (fcntl(fileno(this->read), F_SETFL, O_NONBLOCK) == -1) {
			fclose(this->read);
			fclose(this->write);
			kill(this->pid, SIGTERM);
			throw runtime_error("Unable to set pipe to non-blocking!");
		}
	}

	~gnome_config_extension() {
		fclose(this->read);
		fclose(this->write);
		kill(this->pid, SIGTERM);
	}

	url get_config(url dest) throw (runtime_error) {
		// Check for changes in the config
		fd_set rfds;
		struct timeval timeout = { 0, 0 };
		FD_ZERO(&rfds);
		FD_SET(fileno(this->read), &rfds);
		if (select(fileno(this->read)+1, &rfds, NULL, NULL, &timeout) > 0)
			this->read_data();

		// Mode is wpad:// or pac+http://...
		if (this->data[PROXY_MODE] == "auto") {
			string pac = this->data[PROXY_AUTOCONFIG_URL];
			return url::is_valid(pac) ? url(string("pac+") + pac) : url("wpad://");
		}

		// Mode is http://... or socks://...
		else if (this->data[PROXY_MODE] == "manual") {
			string type, host, port;
			bool       auth = this->data[PROXY_USE_AUTHENTICATION] == "true";
			string username = url::encode(this->data[PROXY_AUTH_USER], url::ALLOWED_IN_USERINFO_ELEMENT);
			string password = url::encode(this->data[PROXY_AUTH_PASSWORD], url::ALLOWED_IN_USERINFO_ELEMENT);
			bool same_proxy = this->data[PROXY_SAME_FOR_ALL] == "true"; 

			// If socks is set use it (except when same_proxy is set)
			if (!same_proxy) {
				type = "socks";
				host = this->data[PROXY_SOCKS_HOST];
				port = this->data[PROXY_SOCKS_PORT];
			}
			
			if (host == "" || get_port(port) == 0) {
				// Get the per-scheme proxy settings
				if (dest.get_scheme() == "http") {
						type = "http";
						host = this->data[PROXY_HTTP_HOST];
						port = this->data[PROXY_HTTP_PORT];
				}
				else if (dest.get_scheme() == "https") {
						// It is expected that the configured server is an
						// HTTP server that support CONNECT method.
						type = "http";
						host = this->data[PROXY_SECURE_HOST];
						port = this->data[PROXY_SECURE_PORT];
				}
				else if (dest.get_scheme() == "ftp") {
						// It is expected that the configured server is an
						// HTTP server that handles proxying FTP URLs 
						// (e.g. request with header "Host: ftp://ftp.host.org")
						type = "http";
						host = this->data[PROXY_FTP_HOST];
						port = this->data[PROXY_FTP_PORT];
				}

				// If no proxy is set and we have the same_proxy option
				// enabled try socks at the end only.
				if (same_proxy && (host == "" || get_port(port) == 0)) {
					type = "socks";
					host = this->data[PROXY_SOCKS_HOST];
					port = this->data[PROXY_SOCKS_PORT];
				}
			}


			// If host and port were found, build config url
			if (host != "" && get_port(port) != 0) {
				string tmp = type + "://";
				if (auth)
					tmp += username + ":" + password + "@";
				tmp += host + ":" + port;
				return url(tmp);
			}
		}

		// Mode is direct://
		return url("direct://");
	}

	string get_ignore(url) {
		return this->data[PROXY_IGNORE_HOSTS];
	}

	bool set_creds(url /*proxy*/, string username, string password) {
		string auth = PROXY_USE_AUTHENTICATION "\ttrue\n";
		string user = string(PROXY_AUTH_USER "\t") + username + "\n";
		string pass = string(PROXY_AUTH_PASSWORD "\t") + password + "\n";

		return (fwrite(auth.c_str(), 1, auth.size(), this->write) == auth.size() &&
			fwrite(user.c_str(), 1, user.size(), this->write) == user.size() &&
			fwrite(pass.c_str(), 1, pass.size(), this->write) == pass.size());
	}

private:
	FILE* read;
	FILE* write;
	pid_t pid;
	map<string, string> data;

	bool read_data(int num=-1) {
		if (num == 0)    return true;
		if (!this->read) return false; // We need the pipe to be open

		for (char l[BUFFERSIZE] ; num != 0 && fgets(l, BUFFERSIZE, this->read) != NULL ; ) {
			string line = l;
			line        = line.substr(0, line.rfind('\n'));
			string key  = line.substr(0, line.find("\t"));
			string val  = line.substr(line.find("\t")+1);
			this->data[key] = val;
			if (num > 0) num--;
		}

		return (num <= 0);
	}
};

static base_extension** gnome_config_extension_init() {
	base_extension** retval = new base_extension*[2];
	retval[1] = NULL;
	try {
		retval[0] = new gnome_config_extension();
		return retval;
	}
	catch (runtime_error) {
		delete[] retval;
		return NULL;
	}
}

static bool gnome_config_extension_test() {
	return (getenv("GNOME_DESKTOP_SESSION_ID")
			|| (getenv("DESKTOP_SESSION")
				&& string(getenv("DESKTOP_SESSION")) == "gnome"));
}

MM_MODULE_INIT(gnome_config_extension,
			   gnome_config_extension_init,
			   gnome_config_extension_test,
			   NULL, NULL);
