/* PureAdmin
 * Copyright (C) 2003 Isak Savo
 *
 *  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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*
 * Functions for communication with PureFTPd server
 * Some parts is taken from the pure-ftpwho utility distributed with PureFTPd,
 * copyright is in these cases held by their respective author(s)
 *
 * Copyright (C) 2003 Isak Savo
 */
#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/mman.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#include "srv_comm.h"
#include "helper.h"
#include "cfg.h"
#include "globals.h"


GStaticMutex alist_mutex = G_STATIC_MUTEX_INIT;
GList *cur_activities = NULL; 

/* Generates a unique id from the provided hash string. In most cases,
 * this will be the same as returned from g_str_hash, but this function
 * also guarantees that the id is unique in the provided list of activities.
 *
 * Theoreticly this could lead to a eternal loop, but it shouldn't
 * in practice! (Unless there are a couple of billion entries in the list)
 */
static
guint _list_generate_unique_id (const gchar *hashstr, GList *list)
{
	GList *node;
	gboolean already_exists;
	guint current;

	/* This should in most case generate a number that is unique */
	current = g_str_hash (hashstr);
	do
	{
		already_exists = FALSE;
		node = g_list_first (list);
		while (node && already_exists == FALSE)
		{
			guint id = ((PActivity *) node->data)->id;

			if (id > current)
				/* The list is sorted ascending by ID, so this shortcut is valid */
				return current;

			if (id == current)
				already_exists = TRUE;
			node = g_list_next (node);
		}
		if (G_UNLIKELY (already_exists))
			current += g_random_int();
	} while (already_exists);
	
	return current;
}

/* Parses a line separated by pipes ('|'), and returns an array of strings containing all the fields */
static
gchar **ftpwho_parse_line (gchar *line)
{
	gchar **retval;
	gchar *field;
	gboolean done = FALSE;
	gint array_count, i, j, line_length;
	
	if (!line || !strstr (line, "|"))
		return g_strsplit ("", "|", -1);
	if (G_LIKELY (strstr (line, "\\|") == NULL))
		return g_strsplit (line, "|", -1);

	/* We need to handle an escaped pipe symbol in the username */
	retval = g_new0 (gchar *, 13);
	i = 0;
	line_length = strlen (line);
	field = g_new0 (gchar, line_length);
	for (array_count = 0; array_count < 12 && i < line_length; array_count++)
	{
		done = FALSE;
		j = 0;
		while (!done && i < line_length)
		{
			if (line[i] == '\\' && line[i + 1] == '|') {
				field[j++] = '|';
				i++;
			}
			else if (line[i] == '|')
				done = TRUE;
			else 
				field[j++] = line[i];
			i++;
		}
		retval[array_count] = g_strdup (field);
		memset (field, '\0', j);
	}
	g_free (field);

	return retval;
}

/* This function is used to find out/set whether the srv_com function
 * has fetched *any* data from the ftp server. This is used to display an
 * initial "please wait, fetching info..." message to the user
 *
 * Arguments:
 *     set - If TRUE, the init_status will be set to TRUE, otherwise it will be left untouched.
 *
 * Returns:
 *     The init status (TRUE if activities has been fetched at least once, otherwise FALSE)
 */
gboolean has_initialized (gboolean set)
{
	static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
	static gboolean init_status = FALSE;
	gboolean rv = FALSE;
	
	g_static_mutex_lock (&mutex);
	if (set)
		init_status = TRUE;
	rv = init_status;
	g_static_mutex_unlock (&mutex);

	return rv;
}

static
gint cmp_activities_by_id (const PActivity *a1, const PActivity *a2)
{
	if (G_UNLIKELY (a1 == NULL || a2 == NULL))
		return 0;

	if (a1->id < a2->id)
		return 1;
	if (a1->id == a2->id)
		return 0;
	return -1;
}

	
static
gint lst_find_user (gconstpointer listdata, gconstpointer searchdata)
{
	gchar *haystack = (listdata ? ((POnlineUser *) listdata)->username : NULL);
	gchar *needle = (searchdata ? ((PActivity *) searchdata)->username : NULL);
	gboolean namematch, hostmatch;

	namematch = hostmatch = FALSE;
	if (G_UNLIKELY (!haystack || !needle ||
	    *haystack == 0 || *needle == 0))
		return 0;

	if (g_str_equal (haystack, needle))
		namematch = TRUE;

	haystack = ((POnlineUser *) listdata)->remote_addr;
	needle = ((PActivity *) searchdata)->remote_addr;

	if (g_str_equal (needle, haystack))
		hostmatch = TRUE;

   
	if (hostmatch && namematch)
		return 0;		/* Absolute match */
	else if (namematch)		/* These are actually not relevant, but return <0 or > 0 anyway */
		return -1;
	else
		return 1;
}

/* Main worker function of this file. This function is responsible for fetching data from pure-ftpwho, parse it
 * and then store it in a GList of activities. Note: This function is running in a separate thread than the main
 * program, and extra care must be taken when accessing shared data structures:
 *
 * GList *cur_activities: Protected by static mutex alist_mutex
 */
static
void update_activities (void)
{
	gchar *c_stdout, *c_stderr;
	gchar **lines, **entries;
	gchar *cmd;
	GList *tmp_alist = NULL;
	gint exit_status, lineno, count;
	gboolean ret;
   
	/* FIXME: (Maybe) make the log handler thread safe so that we can call pur_log_*() from this function */
	c_stdout = c_stderr = NULL;
	cmd = g_strdup_printf ("%s -s %s", cfg.cmd_ftpwho, cfg.resolve_hostnames ? "" : "-H");
	
	ret = g_spawn_command_line_sync (cmd,
					 &c_stdout,
					 NULL,
					 &exit_status,
					 NULL);
	if (!ret)
	{
		//pur_log_wrn ("Unable to execute child: [%s]", cmd);
		g_free (cmd);
		return;
	}
	g_free (cmd);

	/* Parse output */
	c_stdout = g_strchomp (c_stdout);
   
	lines = g_strsplit (c_stdout, "\n", 0);
	g_free (c_stdout);
	lineno = 0;
	while (lines[lineno])
	{
		PActivity *activity;

		entries = ftpwho_parse_line (lines[lineno]);
		count = arr_count (entries);
		//pur_log_dbg ("ftpwho_parse_line: %d entries returned", count);
		if (count < 11)
		{
			//pur_log_wrn ("Line [%s]: only %d entries found!", lines[lineno], count);
			lineno++;
			g_strfreev (entries);
			continue;
		}
		/* Add new found entries to global list */
		activity = g_new0 (PActivity, 1);
      
		activity->pid = atoi (entries[0]);
		activity->username = g_strdup (entries[1]);
		activity->online_since = atoi (entries[2]);
		
		if (strncmp (entries[3], "IDLE", 4) == 0)
			activity->state = FTPWHO_STATE_IDLE;
		else if (strncmp (entries[3], " DL ", 4) == 0)
			activity->state = FTPWHO_STATE_DOWNLOAD;
		else if (strncmp (entries[3], " UL ", 4) == 0)
			activity->state = FTPWHO_STATE_UPLOAD;

		activity->filename = g_strdup (entries[4]);
		activity->remote_addr = g_strdup (entries[5]);
		activity->download_current_size = atoi (entries[8]);
		activity->download_total_size = atoi (entries[9]);
		activity->percentage = atoi (entries[10]);
		activity->speed = atoi (entries[11]);
		gchar *hash_string = g_strconcat (activity->username, activity->remote_addr, activity->filename, entries[3], NULL);
		activity->id = _list_generate_unique_id (hash_string, tmp_alist);
		tmp_alist = g_list_insert_sorted (tmp_alist, activity, (GCompareFunc) cmp_activities_by_id);
		//tmp_alist = g_list_append (tmp_alist, activity);

		g_strfreev (entries);
		lineno++;
	}
	g_strfreev (lines);
	/* Clear the real (old) list in replace it with the newly fetched list */
	g_static_mutex_lock (&alist_mutex);
	g_list_foreach (cur_activities, (GFunc) pactivity_free, NULL);
	g_list_free (cur_activities);
	cur_activities = tmp_alist;
	g_static_mutex_unlock (&alist_mutex);
	
	has_initialized (TRUE);
}


/** GLOBAL FUNCTIONS **/

void srv_terminate (void)
{
	GList *node = g_list_first (cur_activities);
	if (!node)
		return;
	do
	{
		g_free (node->data);
	} while ((node = node->next));

	g_list_free (cur_activities);
	cur_activities = NULL;
	return;
}

PActivity *pactivity_dup (PActivity *a)
{
	PActivity *rv;

	rv = (PActivity *) g_memdup (a, sizeof (PActivity));
	rv->username = g_strdup (a->username);
	rv->remote_addr = g_strdup (a->remote_addr);
	rv->filename = g_strdup (a->filename);
	
	return rv;
}

void pactivity_free (PActivity *a)
{
	if (!a)
		return;
	g_free (a->username);
	g_free (a->remote_addr);
	g_free (a->filename);
	g_free (a);
}

void free_activity_list (GList *l)
{
	GList *node = g_list_first (l);
	
	while (node) {
		pactivity_free ((PActivity *) node->data);
		node = g_list_next(node);
	}
	g_list_free (l);
}

void pouser_free (POnlineUser *u)
{
	if (!u)
		return;
	g_free (u->username);
	g_free (u->remote_addr);
	g_free (u);
}

void free_pouser_list (GList *l)
{
	GList *node = g_list_first (l);
	
	while (node) {
		pouser_free ((POnlineUser *) node->data);
		node = g_list_next(node);
	}
	g_list_free (l);
}

/* Returns the current activities on the server. The returned value is a GList of PActivity and should
 * be free'd when no longer used. See also: free_activity_list */
GList *srv_get_activities (void)
{
	GList *retval = NULL, *node;
	PActivity *n_data;

	g_static_mutex_lock (&alist_mutex);
	node = cur_activities;
	while (node)
	{
		n_data = pactivity_dup ((PActivity *) node->data);
		retval = g_list_append (retval, n_data);
		node = g_list_next (node);
	}
	g_static_mutex_unlock (&alist_mutex);

	return retval;
}

gboolean srv_has_activities (void)
{
	gboolean rv = FALSE;
	
	g_static_mutex_lock (&alist_mutex);
	if (cur_activities)
		rv = TRUE;
	g_static_mutex_unlock (&alist_mutex);

	return rv;
}

gboolean srv_try_get_activities (GError **err)
{
	gchar *output, *err_output, *cmd;
	gint ret;
	gint exit_status;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);
	if (!cfg.cmd_ftpwho)
	{
		g_set_error (err,
			     PUREADMIN_SRV_ERROR,
			     PA_SRV_ERROR_COMMAND_NOT_FOUND,
			     _("Command 'pure-ftpwho' not found. Perhaps PureFTPd isn't installed"));
		return FALSE;
	}

	cmd = g_strdup_printf ("%s -s -H", cfg.cmd_ftpwho);
	ret = g_spawn_command_line_sync (cmd,
					 &output,
					 &err_output,
					 &exit_status,
					 NULL);
	pur_log_dbg ("Running command: [%s]", cmd);
	if (!ret)
	{
		g_set_error (err,
			     PUREADMIN_SRV_ERROR,
			     PA_SRV_ERROR_COMMAND_NOT_FOUND,
			     _("Command 'pure-ftpwho' not found. Perhaps PureFTPd isn't installed"));
		g_free (cmd);
		g_free (output);
		return FALSE;
	}
	g_free (cmd);
	exit_status = (signed char) WEXITSTATUS(exit_status);
	if (exit_status)
	{
		if (strstr (output, "root"))
			g_set_error (err,
				     PUREADMIN_SRV_ERROR,
				     PA_SRV_ERROR_PERMISSION_DENIED,
				     _("Permission denied"));
		else
		{
			if (exit_status == -1)
				g_set_error (err,
				     PUREADMIN_SRV_ERROR,
				     PA_SRV_ERROR_FAILED,
				     _("Resource temporarily unavailable."));
			else
				g_set_error (err,
				     PUREADMIN_SRV_ERROR,
				     PA_SRV_ERROR_FAILED,
				     _("Pure-ftpwho exited abnormally: %d. Output was: '%s' and '%s' "),
				     exit_status, output, err_output);
		}
		g_free (output);
		g_free (err_output);
		return FALSE;
	}
	g_free (output);
	return TRUE;
}

G_GNUC_NORETURN gpointer srv_activity_thread (gpointer data)
{
	while (1)
	{
		update_activities ();
		g_usleep (3000000);
	}
}

/* Returns the PActivity that has the provided id number.
 * The returned value should be free'd using pactivity_free when no longer used */
PActivity *srv_get_activity_with_id (guint id)
{
	GList *node = NULL;
	PActivity *act = NULL, *rv;
	gboolean found = FALSE;
	g_static_mutex_lock (&alist_mutex);
	node = g_list_first (cur_activities);
	while (node && !found)
	{
		act = (PActivity *) node->data;
		if (act->id == id)
			found = TRUE;
		node = g_list_next (node);
	}
	rv = pactivity_dup (act);
	g_static_mutex_unlock (&alist_mutex);
	if (found)
		return rv;
	else
		return NULL;
}

GList *srv_get_pids_for_user_host (const gchar *user, const gchar *host)
{
	GList *node = NULL;
	GList *ret = NULL;
	PActivity *act;
   
	if (!user || !host ||
	    *user == '\0' || *host == '\0')
		return NULL;
	g_static_mutex_lock (&alist_mutex);
	node = g_list_first (cur_activities);
	while (node)
	{
		act = (PActivity *) node->data;
		if (g_str_equal (act->username, user) && 
		    g_str_equal (act->remote_addr, host))
		{
			ret = g_list_append (ret, GINT_TO_POINTER (act->pid));
		}
		node = g_list_next (node);
	}
	g_static_mutex_unlock (&alist_mutex);

	return ret;
}

GList *srv_get_connected_users (GList *activities)
{
	GList *retval = NULL, *node;
	POnlineUser *user = NULL;

	g_static_mutex_lock (&alist_mutex);
	activities = g_list_first (activities);
	while (activities)
	{
		if ((node = g_list_find_custom (retval, activities->data, (GCompareFunc) lst_find_user)))
			((POnlineUser *) node->data)->num_connections++;
		else
		{
			user = g_new0 (POnlineUser, 1);
			user->username = g_strdup (((PActivity *) activities->data)->username);
			user->remote_addr = g_strdup (((PActivity *) activities->data)->remote_addr);
			user->num_connections = 1;
			
			retval = g_list_append (retval, user);
			
		}
		activities = g_list_next (activities);
	}
	g_static_mutex_unlock (&alist_mutex);
	return g_list_first (retval);
}


syntax highlighted by Code2HTML, v. 0.9.1