/* $Id: commands.c 5078 2007-12-09 16:56:36Z morris $ */

/*
 *  Copyright (c) 2004-2007 Axel Andersson
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"

#include <string.h>
#include <wired/wired.h>

#include "banlist.h"
#include "clients.h"
#include "commands.h"
#include "main.h"
#include "servers.h"
#include "settings.h"
#include "tracker.h"
#include "version.h"

struct _wt_commands {
	const char					*name;

	/* minimum state required */
	wt_client_state_t			state;

	/* minimum number of arguments required */
	wi_uinteger_t				args;

	void						(*action)(wi_array_t *);
};
typedef struct _wt_commands		wt_commands_t;


static void						wt_parse_command(wi_string_t *);
static wi_uinteger_t			wt_command_index(wi_string_t *);

static void						wt_cmd_categories(wi_array_t *);
static void						wt_cmd_client(wi_array_t *);
static void						wt_cmd_hello(wi_array_t *);
static void						wt_cmd_register(wi_array_t *);
static void						wt_cmd_servers(wi_array_t *);


static wt_commands_t			wt_commands[] = {
	{ "CATEGORIES",
	  WT_CLIENT_STATE_SAID_HELLO,		0,		wt_cmd_categories },
	{ "CLIENT",
	  WT_CLIENT_STATE_SAID_HELLO,		1,		wt_cmd_client },
	{ "HELLO",
	  WT_CLIENT_STATE_CONNECTED,		0,		wt_cmd_hello },
	{ "REGISTER",
	  WT_CLIENT_STATE_SAID_HELLO,		5,		wt_cmd_register },
	{ "SERVERS",
	  WT_CLIENT_STATE_SAID_HELLO,		0,		wt_cmd_servers },
};


void wt_control_thread(wi_runtime_instance_t *argument) {
	wi_pool_t			*pool;
	wt_client_t			*client = argument;
	wi_string_t			*string;
	wi_socket_state_t	state;
	wi_uinteger_t		i = 0;
	
	pool = wi_pool_init(wi_pool_alloc());

	wt_client_set(client);

	while(client->state <= WT_CLIENT_STATE_SAID_HELLO) {
		do {
			state = wi_socket_wait(client->socket, 0.1);
		} while(state == WI_SOCKET_TIMEOUT && client->state <= WT_CLIENT_STATE_SAID_HELLO);

		if(client->state > WT_CLIENT_STATE_SAID_HELLO) {
			/* invalid state */
			break;
		}

		if(state == WI_SOCKET_ERROR) {
			if(wi_error_code() == EINTR) {
				/* got a signal */
				continue;
			} else {
				/* error in TCP communication */
				wi_log_err(WI_STR("Could not read from %@: %m"), client->ip);

				break;
			}
		}

		string = wi_socket_read_to_string(client->socket, 0.0, WI_STR(WT_MESSAGE_SEPARATOR_STR));

		if(!string || wi_string_length(string) == 0) {
			if(!string)
				wi_log_info(WI_STR("Could not read from %@: %m"), client->ip);
			
			break;
		}

		wt_parse_command(string);
		
		if(++i % 10 == 0)
			wi_pool_drain(pool);
	}

	wi_log_info(WI_STR("Disconnect from %@ after %.2fs"),
		client->ip,
		wi_time_interval() - client->connect_time);
	
	wi_release(pool);
}



#pragma mark -

static void wt_parse_command(wi_string_t *buffer) {
	wt_client_t		*client = wt_client();
	wi_string_t		*command;
	wi_array_t		*arguments;
	wi_uinteger_t	index;
	
	wi_parse_wired_command(buffer, &command, &arguments);
	
	index = wt_command_index(command);

	if(index == WI_NOT_FOUND) {
		wt_reply(501, WI_STR("Command Not Recognized"));

		return;
	}

	if(client->state < wt_commands[index].state)
		return;
	
	if(wi_array_count(arguments) < wt_commands[index].args) {
		wt_reply(503, WI_STR("Syntax Error"));

		return;
	}

	((*wt_commands[index].action) (arguments));
}



static wi_uinteger_t wt_command_index(wi_string_t *command) {
	const char		*cstring;
	wi_uinteger_t	i, min, max;
	int				cmp;

	cstring = wi_string_cstring(command);
	min = 0;
	max = WI_ARRAY_SIZE(wt_commands) - 1;

	do {
		i = (min + max) / 2;
		cmp = strcasecmp(cstring, wt_commands[i].name);

		if(cmp == 0)
			return i;
		else if(cmp < 0 && i > 0)
			max = i - 1;
		else if(cmp > 0)
			min = i + 1;
		else
			break;
	} while(min <= max);

	return WI_NOT_FOUND;
}



#pragma mark -

/*
	CATEGORIES
*/

static void wt_cmd_categories(wi_array_t *arguments) {
	wi_file_t		*file;
	wi_string_t		*string;

	file = wi_file_for_reading(wt_settings.categories);
	
	if(!file) {
		wt_reply(500, WI_STR("Command Failed"));
		wi_log_err(WI_STR("Could not open %@: %m"), wt_settings.categories);

		return;
	}

	while((string = wi_file_read_config_line(file)))
		wt_reply(710, WI_STR("%@"), string);

	wt_reply(711, WI_STR("Done"));
}



/*
	CLIENT <application-version>
*/

static void wt_cmd_client(wi_array_t *arguments) {
	wt_client_t		*client = wt_client();

	client->version = wi_retain(WI_ARRAY(arguments, 0));
}



/*
	HELLO
*/

void wt_cmd_hello(wi_array_t *arguments) {
	wt_client_t		*client = wt_client();

	if(client->state != WT_CLIENT_STATE_CONNECTED)
		return;

	if(wt_ip_is_banned(client->ip)) {
		wt_reply(511, WI_STR("Banned"));
		wi_log_err(WI_STR("Connection from %@ denied, host is banned"),
			client->ip);

		client->state = WT_CLIENT_STATE_DISCONNECTED;

		return;
	}

	wt_reply(200, WI_STR("%#@%c%#@%c%#@%c%#@%c%#@"),
			 wt_server_version_string,		WT_FIELD_SEPARATOR,
			 wt_protocol_version_string,	WT_FIELD_SEPARATOR,
			 wt_settings.name,				WT_FIELD_SEPARATOR,
			 wt_settings.description,		WT_FIELD_SEPARATOR,
			 wi_date_iso8601_string(wt_start_date));
	
	client->state = WT_CLIENT_STATE_SAID_HELLO;
}



/*
	REGISTER <category> <url> <name> <bandwidth> <description>
*/

static void wt_cmd_register(wi_array_t *arguments) {
	wt_client_t					*client = wt_client();
	wi_enumerator_t				*enumerator;
	wi_array_t					*array;
	wi_address_t				*address, *hostaddress;
	wi_url_t					*url;
	wi_string_t					*hostname;
	wt_server_t					*server;
	wi_boolean_t				failed = false, passed;
	uint32_t					bandwidth;

	url			= wi_autorelease(wi_url_init_with_string(wi_url_alloc(), WI_ARRAY(arguments, 1)));
	hostname	= wi_url_host(url);
	address		= wi_socket_address(client->socket);
	
	if(!wi_url_is_valid(url)) {
		/* invalid URL */
		if(wt_settings.strictlookup) {
			wt_reply(503, WI_STR("Syntax Error"));
			wi_log_warn(WI_STR("Register from %@ as \"%@\" URL %@ aborted: %s"),
				client->ip, WI_ARRAY(arguments, 2), WI_ARRAY(arguments, 1),
				"Invalid URL");
			
			return;
		}
		
		failed = true;
		goto failed;
	}

	if(wi_ip_version(hostname) > 0) {
		/* hostname is numeric, compare with source address */
		if(!wi_is_equal(hostname, client->ip)) {
			/* IP mismatch */
			if(wt_settings.strictlookup) {
				wt_reply(530, WI_STR("Address Mismatch"));
				wi_log_warn(WI_STR("Register from %@ as \"%@\" URL %@ denied: %s"),
					client->ip, WI_ARRAY(arguments, 2), WI_ARRAY(arguments, 1),
					"IP mismatch");

				return;
			}

			failed = true;
			goto failed;
		}
	} else {
		/* hostname is symbolic */
		if(wt_settings.lookup) {
			/* look up and compare with source address */
			passed = false;
			array = wi_host_addresses(wi_host_with_string(hostname));
			
			if(array) {
				enumerator = wi_array_data_enumerator(array);
				
				while((hostaddress = wi_enumerator_next_data(enumerator))) {
					if(wi_is_equal(hostaddress, address)) {
						passed = true;
						
						break;
					}
				}
			}
			
			if(!passed) {
				/* lookup failed */
				if(wt_settings.strictlookup) {
					wt_reply(531, WI_STR("Address Mismatch"));
					wi_log_warn(WI_STR("Register from %@ as \"%@\" URL %@ denied: %s"),
						client->ip, WI_ARRAY(arguments, 2), WI_ARRAY(arguments, 1),
						"Lookup failed");
					
					return;
				}
				
				failed = true;
				goto failed;
			}
		}

		if(wt_settings.reverselookup) {
			/* reverse look up and compare to hostname */
			if(!wi_is_equal(wi_address_hostname(address), hostname)) {
				/* reverse lookup failed */
				if(wt_settings.strictlookup) {
					wt_reply(531, WI_STR("Address Mismatch"));
					wi_log_warn(WI_STR("Register from %@ as \"%@\" URL % denied: %@"),
						client->ip, WI_ARRAY(arguments, 2), WI_ARRAY(arguments, 1),
						"Reverse lookup failed");

					return;
				}

				failed = true;
				goto failed;
			}
		}
	}

failed:
	/* get bandwidth */
	bandwidth = wi_string_uint32(WI_ARRAY(arguments, 3));

	/* bandwidth too low? */
	if(wt_settings.minbandwidth > 0 && bandwidth < wt_settings.minbandwidth) {
		wt_reply(516, WI_STR("Permission Denied"));
		wi_log_warn(WI_STR("Register from %@ as \"%@\" URL %@ denied: Bandwidth %.0f Kbps considered too low"),
			client->ip, WI_ARRAY(arguments, 2), WI_ARRAY(arguments, 1), bandwidth / 128.0);

		return;
	}

	/* bandwidth too high? */
	if(wt_settings.maxbandwidth > 0 && bandwidth > wt_settings.maxbandwidth) {
		wt_reply(516, WI_STR("Permission Denied"));
		wi_log_warn(WI_STR("Register from %@ as \"%@\" URL %@ denied: Bandwidth %.0f Kbps considered too high"),
			client->ip, WI_ARRAY(arguments, 2), WI_ARRAY(arguments, 1), bandwidth / 128.0);

		return;
	}

	/* is there an existing server from this host? */
	server = wt_servers_server_with_ip(client->ip);

	if(server) {
		if(server->port == wi_url_port(url)) {
			/* remove existing server in preparation for re-registration */
			wt_servers_remove_stats_for_server(server);
			wt_servers_remove_server(server);
		} else {
			/* multiple servers from the same IP allowed? */
			if(!wt_settings.allowmultiple) {
				wt_reply(530, WI_STR("Address Registered"));
				wi_log_warn(WI_STR("Register from %@ as \"%@\" URL %@ denied: %s"),
					client->ip, WI_ARRAY(arguments, 2), WI_ARRAY(arguments, 1),
					"A server from the same address is already registered");
				
				return;
			}
		}
	}
	
	/* rewrite URL if host verification failed */
	if(failed) {
		wi_url_set_scheme(url, WI_STR("wired"));
		wi_url_set_host(url, client->ip);

		if(wi_string_length(WI_ARRAY(arguments, 1)) == 0)
			wi_log_info(WI_STR("Rewriting URL to %@"), wi_url_string(url));
		else
			wi_log_info(WI_STR("Rewriting URL from %@ to %@"), WI_ARRAY(arguments, 1), wi_url_string(url));
	}

	/* create new server */
	server					= wt_server_init(wt_server_alloc());
	server->key				= wi_retain(wi_string_sha1(wi_autorelease(wi_string_init_random_string_with_length(wi_string_alloc(), 1024))));
	server->port			= wi_url_port(url);
	server->bandwidth		= bandwidth;
	server->register_time	= wi_time_interval();
	server->update_time		= 0.0;
	server->ip				= wi_retain(client->ip);
	server->category		= wt_servers_category_is_valid(WI_ARRAY(arguments, 0))
		? wi_retain(WI_ARRAY(arguments, 0))
		: wi_string_init(wi_string_alloc());
	server->name			= wi_retain(WI_ARRAY(arguments, 2));
	server->description		= wi_retain(WI_ARRAY(arguments, 4));
	server->url				= wi_copy(wi_url_string(url));

	wt_servers_add_server(server);
	wt_servers_add_stats_for_server(server);
	
	/* reply 700 */
	wt_reply(700, WI_STR("%@"), server->key);
	wi_log_info(WI_STR("Registered \"%@\" with URL %@"), server->name, server->url);
	wt_servers_write_file();
	wi_release(server);
}



/*
	SERVERS
*/

static void wt_cmd_servers(wi_array_t *arguments) {
	/* reply all servers */
	wt_servers_reply_server_list();

	/* update status */
	wi_lock_lock(wt_status_lock);
	wt_total_clients++;
	wt_write_status(true);
	wi_lock_unlock(wt_status_lock);
}


syntax highlighted by Code2HTML, v. 0.9.1