/* 
	ctrlproxy: A modular IRC proxy
	(c) 2005-2006 Jelmer Vernooij <jelmer@nl.linux.org>
	
	Manual listen on ports

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU 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 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "internals.h"
#include "irc.h"
#include "listener.h"
#include "ssl.h"
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <glib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif

#include <netdb.h>

static GIConv iconv = (GIConv)-1;

static gboolean handle_client_receive(GIOChannel *c, GIOCondition condition, gpointer data) 
{
	struct line *l;
	struct listener *listener = data;
	GError *error = NULL;
	GIOStatus status;

	g_assert(c);

	while ((status = irc_recv_line(c, iconv, &error, &l)) == G_IO_STATUS_NORMAL) {
		g_assert(l);

		if (!l->args[0]){ 
			free_line(l);
			continue;
		}

		if(!listener->password) {
			log_network(LOG_WARNING, listener->network, "No password set, allowing client _without_ authentication!");
		}

		if(!g_strcasecmp(l->args[0], "PASS")) {
			char *desc;
			if (listener->password && strcmp(l->args[1], listener->password)) {
				log_network(LOG_WARNING, listener->network, "User tried to log in with incorrect password!");
				irc_sendf(c, iconv, NULL, ":%s %d %s :Password mismatch", get_my_hostname(), ERR_PASSWDMISMATCH, "*");
	
				free_line(l);
				return TRUE;
			}

			log_network (LOG_INFO, listener->network, "Client successfully authenticated");

			desc = g_io_channel_ip_get_description(c);
			client_init(listener->network, c, desc);

			free_line(l); 
			return FALSE;
		} else {
			irc_sendf(c, iconv, NULL, ":%s %d %s :You are not registered", get_my_hostname(), ERR_NOTREGISTERED, "*");
		}

		free_line(l);
	}

	if (status != G_IO_STATUS_AGAIN)
		return FALSE;
	
	return TRUE;
}

static gboolean handle_new_client(GIOChannel *c_server, GIOCondition condition, void *_listener)
{
	struct listener *listener = _listener;
	GIOChannel *c;
	int sock = accept(g_io_channel_unix_get_fd(c_server), NULL, 0);

	if (sock < 0) {
		log_global(LOG_WARNING, "Error accepting new connection: %s", strerror(errno));
		return TRUE;
	}

	c = g_io_channel_unix_new(sock);

	if (listener->ssl) {
#ifdef HAVE_GNUTLS
		c = ssl_wrap_iochannel(c, SSL_TYPE_SERVER, 
											 NULL, listener->ssl_credentials);
		g_assert(c != NULL);
#else
		log_global(LOG_WARNING, "SSL support not available, not listening for SSL connection");
#endif
	}

	g_io_channel_set_close_on_unref(c, TRUE);
	g_io_channel_set_encoding(c, NULL, NULL);
	g_io_channel_set_flags(c, G_IO_FLAG_NONBLOCK, NULL);
	g_io_add_watch(c, G_IO_IN, handle_client_receive, listener);

	g_io_channel_unref(c);

	listener->pending = g_list_append(listener->pending, c);

	return TRUE;
}

/* FIXME: Store in global struct somehow */
static GList *listeners = NULL;
static int autoport = 6667;
static gboolean auto_listener = FALSE;
static GKeyFile *keyfile = NULL;

static int next_autoport()
{
	return ++autoport;
}

gboolean start_listener(struct listener *l)
{
	int sock = -1;
	const int on = 1;
	struct addrinfo *res, *all_res;
	int error;
	struct addrinfo hints;
	struct listener_iochannel *lio;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;

	g_assert(!l->active);

	error = getaddrinfo(l->address, l->port, &hints, &all_res);
	if (error) {
		log_global(LOG_ERROR, "Can't get address for %s:%s", l->address?l->address:"", l->port);
		return FALSE;
	}

	for (res = all_res; res; res = res->ai_next) {
		GIOChannel *ioc;

		lio = g_new0(struct listener_iochannel, 1);

		if (getnameinfo(res->ai_addr, res->ai_addrlen, 
						lio->address, NI_MAXHOST, lio->port, NI_MAXSERV, 
						NI_NUMERICHOST | NI_NUMERICSERV) != 0) {
			strcpy(lio->address, "");
			strcpy(lio->port, "");
		}

		sock = socket(PF_INET, SOCK_STREAM, 0);
		if (sock < 0) {
			log_global(LOG_ERROR, "error creating socket: %s", strerror(errno));
			close(sock);
			g_free(lio);
			continue;
		}

		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	
		if (bind(sock, res->ai_addr, res->ai_addrlen) < 0) {
			/* Don't warn when binding to the same address using IPv4 
			 * /and/ ipv6. */
			if (!l->active || errno != EADDRINUSE)  {
				log_global(LOG_ERROR, "bind to %s:%s failed: %s", l->address, 
						   l->port, strerror(errno));
			}
			close(sock);
			g_free(lio);
			continue;
		}

		if (listen(sock, 5) < 0) {
			log_global(LOG_ERROR, "error listening on socket: %s", 
						strerror(errno));
			close(sock);
			g_free(lio);
			continue;
		}

		ioc = g_io_channel_unix_new(sock);

		if (ioc == NULL) {
			log_global(LOG_ERROR, 
						"Unable to create GIOChannel for server socket");
			close(sock);
			g_free(lio);
			continue;
		}

		g_io_channel_set_close_on_unref(ioc, TRUE);

		g_io_channel_set_encoding(ioc, NULL, NULL);
		lio->watch_id = g_io_add_watch(ioc, G_IO_IN, handle_new_client, l);
		g_io_channel_unref(ioc);
		l->incoming = g_list_append(l->incoming, lio);

		log_network( LOG_INFO, l->network, "Listening on %s:%s", 
					 lio->address, lio->port);
		l->active = TRUE;
	}

	freeaddrinfo(all_res);
	
	return l->active;
}

gboolean stop_listener(struct listener *l)
{

	while (l->incoming != NULL) {
		struct listener_iochannel *lio = l->incoming->data;

		g_source_remove(lio->watch_id);
		
		log_global(LOG_INFO, "Stopped listening at %s:%s", lio->address, 
					 lio->port);
		g_free(lio);

		l->incoming = g_list_remove(l->incoming, lio);
	}

	return TRUE;
}

void free_listener(struct listener *l)
{
	g_free(l->password);
	g_free(l->port);
	g_free(l->address);
	listeners = g_list_remove(listeners, l);
	g_free(l);
}

struct listener *listener_init(const char *address, const char *port)
{
	struct listener *l = g_new0(struct listener, 1);

	l->address = address?g_strdup(address):NULL;
	l->port = g_strdup(port);

	if (l->port == NULL) 
		l->port = g_strdup("6667");

	listeners = g_list_append(listeners, l);

	return l;
}

static void update_config(struct global *global, const char *path)
{
	GList *gl;
	char *filename;
	GKeyFile *kf; 
	GError *error = NULL;
	char *default_password;

	default_password = g_key_file_get_string(global->config->keyfile, "listener", "password", NULL);

	g_key_file_set_boolean(global->config->keyfile, "listener", "auto", auto_listener);

	g_key_file_set_integer(global->config->keyfile, "listener", "autoport", autoport);

	filename = g_build_filename(path, "listener", NULL);

	if (keyfile)
		keyfile = g_key_file_new();

	kf = keyfile;

	for (gl = listeners; gl; gl = gl->next) {
		struct listener *l = gl->data;
		char *tmp;

		if (!l->address) 
			tmp = g_strdup(l->port);
		else
			tmp = g_strdup_printf("%s:%s", l->address, l->port);

		if (l->password && !(default_password && strcmp(l->password, default_password) == 0)) 
			g_key_file_set_string(kf, tmp, "password", l->password);

		if (l->network) {
			g_assert(l->network->info.name != NULL);
			g_key_file_set_string(kf, tmp, "network", l->network->info.name);
		}

		g_key_file_set_boolean(kf, tmp, "ssl", l->ssl);

		g_free(tmp);
	}

	if (!g_key_file_save_to_file(kf, filename, &error)) {
		log_global(LOG_WARNING, "Unable to save to \"%s\": %s", filename, error->message);
	}
	
	g_free(filename);
}

static void auto_add_listener(struct network *n, void *private_data)
{
	GList *gl;
	char *port;
	struct listener *l;

	/* See if there is already a listener for n */
	for (gl = listeners; gl; gl = gl->next) {
		l = gl->data;

		if (l->network == n || l->network == NULL)
			return;
	}

	port = g_strdup_printf("%d", next_autoport());
	l = listener_init(NULL, port);
	l->network = n;
	start_listener(l);
}

static void load_config(struct global *global)
{
	char *filename = g_build_filename(global->config->config_dir, "listener", NULL);
	int i;
	char **groups;
	gsize size;
	GKeyFile *kf;
	char *default_password;

	default_password = g_key_file_get_string(global->config->keyfile, "listener", "password", NULL);
	if (g_key_file_has_key(global->config->keyfile, "listener", "auto", NULL))
		auto_listener = g_key_file_get_boolean(global->config->keyfile, "listener", "auto", NULL);

	if (g_key_file_has_key(global->config->keyfile, "listener", "autoport", NULL))
		autoport = g_key_file_get_integer(global->config->keyfile, "listener", "autoport", NULL);

	if (auto_listener)
		register_new_network_notify(global, auto_add_listener, NULL);

	keyfile = kf = g_key_file_new();

	if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_KEEP_COMMENTS, NULL)) {
		g_free(filename);
		return;
	}
		
	groups = g_key_file_get_groups(kf, &size);

	for (i = 0; i < size; i++)
	{
		struct listener *l;
		char *address, *port, *tmp;
		
		tmp = g_strdup(groups[i]);
		port = strrchr(tmp, ':');
		if (port) {
			address = tmp;
			*port = '\0';
			port++;
		} else {
			port = tmp;
			address = NULL;
		}
			
		l = listener_init(address, port);

		g_free(tmp);

		l->password = g_key_file_get_string(kf, groups[i], "password", NULL);
		if (!l->password)
			l->password = default_password;

		if (g_key_file_has_key(kf, groups[i], "ssl", NULL))
			l->ssl = g_key_file_get_boolean(kf, groups[i], "ssl", NULL);

#ifdef HAVE_GNUTLS
		if (l->ssl)
			l->ssl_credentials = ssl_create_server_credentials(global, kf, groups[i]);
#endif

		if (g_key_file_has_key(kf, groups[i], "network", NULL)) {

			tmp = g_key_file_get_string(kf, groups[i], "network", NULL);
			l->network = find_network(global, tmp);
			if (!l->network) {
				log_global(LOG_ERROR, "Unable to find network named \"%s\"", tmp);
			}
			g_free(tmp);
		}
			
		start_listener(l);
	}

	g_strfreev(groups);
	g_free(filename);
}

static void fini_plugin(void)
{
	GList *gl;
	for(gl = listeners; gl; gl = gl->next) {
		struct listener *l = gl->data;
		if (l->active) 
			stop_listener(l);
	}
}


void cmd_start_listener(admin_handle h, char **args, void *userdata)
{
	char *b, *p;
	struct listener *l;

	if (!args[1]) {
		admin_out(h, "No port specified");
		return;
	}

	if (!args[2]) {
		admin_out(h, "No password specified");
		return;
	}

	b = g_strdup(args[1]);
	if ((p = strchr(b, ':'))) {
		*p = '\0';
		p++;
	} else {
		p = b;
		b = NULL;
	}

	l = listener_init(b, p);

	if (b) g_free(b); else g_free(p);

	l->password = g_strdup(args[2]);

	if (args[3]) {
		l->network = find_network(admin_get_global(h), args[3]);
		if (l->network == NULL) {
			admin_out(h, "No such network `%s'", args[3]);
			free_listener(l);
			return;
		}
	}

	start_listener(l);
}

void cmd_stop_listener(admin_handle h, char **args, void *userdata)
{
	GList *gl;
	char *b, *p;
	int i = 0;

	if (!args[0]) {
		admin_out(h, "No port specified");
		return;
	}

	b = g_strdup(args[1]);
	if ((p = strchr(b, ':'))) {
		*p = '\0';
		p++;
	} else {
		p = b;
		b = NULL;
	}

	for (gl = listeners; gl; gl = gl->next) {
		struct listener *l = gl->data;

		if (b && !l->address)
			continue;

		if (b && l->address && strcmp(b, l->address) != 0)
			continue;

		if (strcmp(p, l->port) != 0)
			continue;

		stop_listener(l);
		free_listener(l);
		i++;
	}

	if (b) g_free(b); else g_free(p);

	admin_out(h, "%d listeners stopped", i);
}

void cmd_list_listener(admin_handle h, char **args, void *userdata)
{
	GList *gl;

	for (gl = listeners; gl; gl = gl->next) {
		struct listener *l = gl->data;

		admin_out(h, "%s:%s%s%s%s", l->address?l->address:"", l->port, l->network?" (":"", l->network?l->network->info.name:"", l->network?")":"");
	}
}

const static struct admin_command listener_commands[] = {
	{ "STARTLISTENER", cmd_start_listener },
	{ "STOPLISTENER", cmd_stop_listener },
	{ "LISTLISTENER", cmd_list_listener },
	{ NULL }
};

static gboolean init_plugin(void)
{
	int i;
	register_load_config_notify(load_config);
	register_save_config_notify(update_config);

	for (i = 0; listener_commands[i].name; i++)
		register_admin_command(&listener_commands[i]);

	atexit(fini_plugin);
	return TRUE;
}

struct plugin_ops plugin = {
	.name = "listener",
	.version = 0,
	.init = init_plugin,
};


syntax highlighted by Code2HTML, v. 0.9.1