/* Copyright (C) 2000-1 drscholl@users.sourceforge.net
   This is free software distributed under the terms of the
   GNU Public License.  See the file COPYING for details.

   $Id: config.c,v 1.90 2001/03/03 19:00:55 drscholl Exp $ */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include "opennap.h"
#include "debug.h"

typedef enum
{
    VAR_TYPE_INT,
    VAR_TYPE_STR,
    VAR_TYPE_BOOL,
    VAR_TYPE_LIST
}
VAR_TYPE;

#define CF_ONCE	1		/* may only be set in config file or command line */

struct config
{
    char   *name;
    VAR_TYPE type;
    unsigned long val;
    unsigned long def;		/* default value */
    u_int   flags;
};

#define UL (unsigned long)

static struct config Vars[] = {
#ifndef ROUTING_ONLY
    {"file_count_threshold", VAR_TYPE_INT, UL & File_Count_Threshold, 5000,
     0},
    {"max_results", VAR_TYPE_INT, UL & Max_Search_Results, 100, 0},
    {"max_shared", VAR_TYPE_INT, UL & Max_Shared, 5000, 0},
    {"stats_port", VAR_TYPE_INT, UL & Stats_Port, 8889, CF_ONCE},
    {"max_searches", VAR_TYPE_INT, UL & Max_Searches, 3, 0},
    {"report_name", VAR_TYPE_STR, UL & global.report_name, 0, 0},
    {"report_ip", VAR_TYPE_STR, UL & global.report_ip, 0, 0},
    {"report_port", VAR_TYPE_INT, UL & global.report_port, 0, 0},
    {"stat_server_host", VAR_TYPE_STR, UL & global.stat_server,
     UL "stats.napigator.com", 0},
    {"stat_server_pass", VAR_TYPE_STR, UL & global.stat_pass, UL "", 0},
    {"stat_server_port", VAR_TYPE_INT, UL & global.stat_server_port, 8890, 0},
    {"stat_server_user", VAR_TYPE_STR, UL & global.stat_user, UL "", 0},
#endif
    {"allow_share", VAR_TYPE_BOOL, ON_ALLOW_SHARE, 1, 0},
    {"auto_link", VAR_TYPE_BOOL, ON_AUTO_LINK, 0, 0},
    {"auto_register", VAR_TYPE_BOOL, ON_AUTO_REGISTER, 0, 0},
    {"client_queue_length", VAR_TYPE_INT, UL & Client_Queue_Length, 102400,
     0},
    {"compression_level", VAR_TYPE_INT, UL & Compression_Level, 1, CF_ONCE},
    {"eject_when_full", VAR_TYPE_BOOL, ON_EJECT_WHEN_FULL, 0, 0},
    {"flood_commands", VAR_TYPE_INT, UL & Flood_Commands, 0, 0},
    {"flood_time", VAR_TYPE_INT, UL & Flood_Time, 0, 0},
    {"ghost_kill", VAR_TYPE_BOOL, ON_GHOST_KILL, 1, 0},
    {"irc_channels", VAR_TYPE_BOOL, ON_IRC_CHANNELS, 1, 0},
    {"listen_addr", VAR_TYPE_STR, UL & Listen_Addr, UL "0.0.0.0", CF_ONCE},
    {"log_mode", VAR_TYPE_BOOL, ON_LOGLEVEL_CHANGE, 0, 0},
    {"login_interval", VAR_TYPE_INT, UL & Login_Interval, 0, 0},
    {"login_timeout", VAR_TYPE_INT, UL & Login_Timeout, 60, 0},
    {"max_browse_result", VAR_TYPE_INT, UL & Max_Browse_Result, 500, 0},
    {"max_channel_length", VAR_TYPE_INT, UL & Max_Channel_Length, 32, 0},
    {"max_client_string", VAR_TYPE_INT, UL & Max_Client_String, 32, 0},
    {"max_clones", VAR_TYPE_INT, UL & Max_Clones, 0, 0},
    {"max_command_length", VAR_TYPE_INT, UL & Max_Command_Length, 2048, 0},
    {"max_connections", VAR_TYPE_INT, UL & Max_Connections, FD_SETSIZE, 0},
    {"max_hotlist", VAR_TYPE_INT, UL & Max_Hotlist, 32, 0},
    {"max_ignore", VAR_TYPE_INT, UL & Max_Ignore, 32, 0},
    {"max_nick_length", VAR_TYPE_INT, UL & Max_Nick_Length, 19, 0},
    {"max_reason", VAR_TYPE_INT, UL & Max_Reason, 64, 0},
    {"max_time_delta", VAR_TYPE_INT, UL & Max_Time_Delta, 90, 0},
    {"max_topic", VAR_TYPE_INT, UL & Max_Topic, 64, 0},
    {"max_user_channels", VAR_TYPE_INT, UL & Max_User_Channels, 5, 0},
    {"min_read", VAR_TYPE_INT, UL & global.min_read, 0, 0},
    {"nick_expire", VAR_TYPE_INT, UL & Nick_Expire, 2678400 /* 31 days */ ,
     0},
    {"ping_interval", VAR_TYPE_INT, UL & Ping_Interval, 600, 0},
    {"register_interval", VAR_TYPE_INT, UL & Register_Interval, 0, 0},
    {"registered_only", VAR_TYPE_BOOL, ON_REGISTERED_ONLY, 0, 0},
    {"remote_browse", VAR_TYPE_BOOL, ON_REMOTE_BROWSE, 1, 0},
    {"restrict_registration", VAR_TYPE_BOOL, ON_RESTRICT_REGISTRATION, 0, 0},
    {"search_timeout", VAR_TYPE_INT, UL & Search_Timeout, 180, 0},
    {"server_alias", VAR_TYPE_STR, UL & Server_Alias, 0, CF_ONCE},
    {"server_chunk", VAR_TYPE_INT, UL & Server_Chunk, 0, 0},
    {"server_name", VAR_TYPE_STR, UL & Server_Name, 0, CF_ONCE},
    {"server_ports", VAR_TYPE_LIST, UL & Server_Ports, UL "8888", CF_ONCE},
    {"server_queue_length", VAR_TYPE_INT, UL & Server_Queue_Length, 1048576,
     0},
    {"stat_click", VAR_TYPE_INT, UL & global.stat_click, 60, 0},
    {"strict_channels", VAR_TYPE_BOOL, ON_STRICT_CHANNELS, 0, 0},
    {"user_db_interval", VAR_TYPE_INT, UL & User_Db_Interval, 1800, 0},
    {"usermode", VAR_TYPE_STR, UL & UserMode, UL "ALL", CF_ONCE},
    {"warn_time_delta", VAR_TYPE_INT, UL & Warn_Time_Delta, 30, 0},
    {"who_was_time", VAR_TYPE_INT, UL & Who_Was_Time, 300, 0},
#ifndef WIN32
    {"connection_hard_limit", VAR_TYPE_INT, UL & Connection_Hard_Limit,
     FD_SETSIZE, CF_ONCE},
    {"max_data_size", VAR_TYPE_INT, UL & Max_Data_Size, -1, CF_ONCE},
    {"max_rss_size", VAR_TYPE_INT, UL & Max_Rss_Size, -1, CF_ONCE},
    {"lock_memory", VAR_TYPE_BOOL, ON_LOCK_MEMORY, 0, CF_ONCE},
#endif
};

static int Vars_Size = sizeof (Vars) / sizeof (struct config);

static void
set_int_var (struct config *v, int val)
{
    ASSERT (v->type == VAR_TYPE_INT);
    *(int *) v->val = val;
}

static void
set_str_var (struct config *v, const char *s)
{
    char  **ptr;

    ASSERT (v->type == VAR_TYPE_STR);
    ptr = (char **) v->val;
    if (*ptr)
	FREE (*ptr);
    *ptr = STRDUP (s);
}

static void
set_list_var (struct config *v, const char *s)
{
    int     ac, i;
    char   *av[32];
    LIST   *tmpList, *list = 0;


    ASSERT (v->type == VAR_TYPE_LIST);
    strncpy (Buf, s, sizeof (Buf) - 1);
    Buf[sizeof (Buf) - 1] = 0;
    ac = split_line (av, FIELDS (av), Buf);
    for (i = 0; i < ac; i++)
    {
	tmpList = CALLOC (1, sizeof (LIST));
	tmpList->data = STRDUP (av[i]);
	tmpList->next = list;
	list = tmpList;
    }
    list_free (*(LIST **) v->val, free_pointer);
    *(LIST **) v->val = list;
}

static void
set_bool_var (struct config *v, int on)
{
    ASSERT (v->type == VAR_TYPE_BOOL);
    if (on)
	Server_Flags |= v->val;
    else
	Server_Flags &= ~v->val;
}

static int
set_var (const char *var, const char *val, int init)
{
    int     i, n;
    char   *ptr;

    for (i = 0; i < Vars_Size; i++)
    {
	if (!strcmp (Vars[i].name, var))
	{
	    if (!init && (Vars[i].flags & CF_ONCE))
	    {
		log ("set_var: %s may not be reset/only set in the config file", Vars[i].name);
		return -1;
	    }
	    if (Vars[i].type == VAR_TYPE_INT)
	    {
		n = strtol (val, &ptr, 10);
		if (*ptr)
		{
		    log ("set_var: invalid integer value: %s", val);
		    return -1;
		}
		set_int_var (&Vars[i], n);
	    }
	    else if (Vars[i].type == VAR_TYPE_STR)
		set_str_var (&Vars[i], val);
	    else if (Vars[i].type == VAR_TYPE_BOOL)
	    {
		if (!strcasecmp ("yes", val) || !strcasecmp ("on", val))
		    n = 1;
		else if (!strcasecmp ("no", val) || !strcasecmp ("off", val))
		    n = 0;
		else
		{
		    n = strtol (val, &ptr, 10);
		    if (*ptr)
		    {
			log ("set_var: invalid boolean value: %s", val);
			return -1;
		    }
		}
		set_bool_var (&Vars[i], n);
	    }
	    else if (Vars[i].type == VAR_TYPE_LIST)
		set_list_var (&Vars[i], val);
	    else
	    {
		ASSERT (0);
	    }
	    return 0;
	}
    }
    log ("set_var(): unknown variable %s", var);
    return -1;
}

static char *
get_str_var (char *name)
{
    int     ac;

    for (ac = 0; ac < Vars_Size; ac++)
    {
	if (!strcasecmp (name, Vars[ac].name)
	    && Vars[ac].type == VAR_TYPE_STR)
	    return *(char **) Vars[ac].val;
    }
    return NULL;
}

int
config (int init)
{
    FILE   *f;
    char   *ptr, *var, *val;
    int     line = 0;
    char    path[_POSIX_PATH_MAX];

    snprintf (path, sizeof (path), "%s/config", Config_Dir);

    if ((f = fopen (path, "r")))
    {
	log ("config: reading %s", path);
	while (fgets (Buf, sizeof (Buf), f))
	{
	    line++;
	    ptr = Buf;
	    var = next_arg (&ptr);
	    if (!var || *var == '#')
		continue;
	    val = next_arg (&ptr);
	    if (!val)
	    {
		log("config: error in %s:%d: missing value", path, line);
		continue;
	    }
	    if (set_var (var, val, init) != 0)
		log ("config: error in %s, line %d", path, line);
	}
	fclose (f);
    }
    else if (errno != ENOENT)
    {
	logerr ("config", path);
	return -1;
    }
    if (init)
	config_user_level (get_str_var ("usermode"));
    return 0;
}

static void
query_var (CONNECTION * con, struct config *v)
{
    if (v->type == VAR_TYPE_INT)
	send_cmd (con, MSG_SERVER_NOSUCH, "%s = %d", v->name,
		  *(int *) v->val);
    else if (v->type == VAR_TYPE_BOOL)
    {
	send_cmd (con, MSG_SERVER_NOSUCH, "%s = %s", v->name,
		  (Server_Flags & v->val) ? "on" : "off");
    }
    else if (v->type == VAR_TYPE_LIST)
    {
	char    buf[1024];
	LIST   *tmpList = 0;

	buf[0] = 0;
	for (tmpList = *(LIST **) v->val; tmpList; tmpList = tmpList->next)
	    snprintf (buf + strlen (buf), sizeof (buf) - strlen (buf),
		      "%s ", (char *) tmpList->data);
	send_cmd (con, MSG_SERVER_NOSUCH, "%s = %s", v->name, buf);
    }
    else
    {
	ASSERT (v->type == VAR_TYPE_STR);
	send_cmd (con, MSG_SERVER_NOSUCH, "%s = %s", v->name,
		  *(char **) v->val);
    }
}

/* 810 [ <var> [ <value> ] ] */
HANDLER (server_config)
{
    char   *av[2];
    int     ac;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    /* only local users should be able to config the server.  this is still
     * problematic as currently user levels are shared across all servers
     * meaning an Elite from another server could still log in and alter the
     * settings.
     */
    CHECK_USER_CLASS ("server_config");

    /* allow mods+ to query the config values, only elites can set them */
    if (con->user->level < LEVEL_MODERATOR)
    {
	permission_denied (con);
	return;
    }

    ac = split_line (av, FIELDS (av), pkt);
    if (ac == 0)
    {
	/* user requests all config variables */
	for (ac = 0; ac < Vars_Size; ac++)
	    query_var (con, &Vars[ac]);
    }
    else if (ac == 1)
    {
	/* user requests the value of a specific variable */
	for (ac = 0; ac < Vars_Size; ac++)
	    if (!strcasecmp (av[0], Vars[ac].name))
	    {
		query_var (con, &Vars[ac]);
		return;
	    }
	send_cmd (con, MSG_SERVER_NOSUCH, "no such variable %s", pkt);
    }
    else
    {
	if (con->user->level < LEVEL_ELITE)
	{
	    permission_denied (con);
	    return;
	}
	/* user changes the value of a specific variable */
	if (set_var (av[0], av[1], 0) != 0)
	{
	    send_cmd (con, MSG_SERVER_NOSUCH, "error setting variable %s",
		      av[0]);
	}
	else
	    notify_mods (CHANGELOG_MODE, "%s set %s to %s",
			 con->user->nick, av[0], av[1]);
    }
}

void
free_config (void)
{
    int     i;

    for (i = 0; i < Vars_Size; i++)
	if (Vars[i].type == VAR_TYPE_STR && *(char **) Vars[i].val)
	    FREE (*(char **) Vars[i].val);
	else if (Vars[i].type == VAR_TYPE_LIST)
	    list_free (*(LIST **) Vars[i].val, free_pointer);
}

/* load the default settings of the server */
void
config_defaults (void)
{
    int     i;

    for (i = 0; i < Vars_Size; i++)
    {
	if (Vars[i].def)
	{
	    if (Vars[i].type == VAR_TYPE_STR)
		set_str_var (&Vars[i], (char *) Vars[i].def);
	    else if (Vars[i].type == VAR_TYPE_INT)
		set_int_var (&Vars[i], Vars[i].def);
	    else if (Vars[i].type == VAR_TYPE_LIST)
		set_list_var (&Vars[i], (char *) Vars[i].def);
	    else if (Vars[i].type == VAR_TYPE_BOOL)
		set_bool_var (&Vars[i], Vars[i].def);
#if DEBUG
	    else
		ASSERT (0);
#endif
	}
    }
}

/* 800 [ :<user> ] <var>
   reset `var' to its default value */
HANDLER (server_reconfig)
{
    int     i;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    CHECK_USER_CLASS ("server_reconfig");
    ASSERT (validate_user (con->user));
    if (con->user->level < LEVEL_ELITE)
    {
	permission_denied (con);
	return;
    }
    for (i = 0; i < Vars_Size; i++)
	if (!strcmp (pkt, Vars[i].name))
	{
	    if (!(Vars[i].flags & CF_ONCE))
	    {
		send_cmd (con, MSG_SERVER_NOSUCH,
			  "reconfig failed: %s may not be changed",
			  Vars[i].name);
	    }
	    else if (Vars[i].def)
	    {
		if (Vars[i].type == VAR_TYPE_STR)
		    set_str_var (&Vars[i], (char *) Vars[i].def);
		else if (Vars[i].type == VAR_TYPE_INT)
		    set_int_var (&Vars[i], Vars[i].def);
		else if (Vars[i].type == VAR_TYPE_BOOL)
		    set_bool_var (&Vars[i], Vars[i].def);
		else if (Vars[i].type == VAR_TYPE_BOOL)
		    set_list_var (&Vars[i], (char *) Vars[i].def);
		notify_mods (CHANGELOG_MODE, "%s reset %s",
			     con->user->nick, Vars[i].name);
	    }
	    else
	    {
		send_cmd (con, MSG_SERVER_NOSUCH, "no default value for %s",
			  pkt);
	    }
	    return;
	}
    send_cmd (con, MSG_SERVER_NOSUCH, "no such variable %s", pkt);
}

static void
nick_check (server_auth_t * auth, void *unused)
{
    USER   *user;
    USERDB *userdb;

    (void) unused;
    if (auth->alias)
    {
	user = hash_lookup (Users, auth->alias);
	if (user)
	{
	    kill_user_internal (0, user, Server_Name, 0,
				"you may not use this nickname");
	}
	/* if the nick is registered, drop it now */
	userdb = hash_lookup (User_Db, auth->alias);
	if (userdb)
	{
	    log ("nick_check: nuking account %s", userdb->nick);
	    hash_remove (User_Db, userdb->nick);
	}
    }
}

/* 10116 [ :user ] [server]
 * reload configuration file
 */
HANDLER (rehash)
{
    USER   *sender;

    (void) len;
    if (pop_user (con, &pkt, &sender))
	return;
    if (sender->level < LEVEL_ELITE)
    {
	permission_denied (con);
	return;
    }
    notify_mods (SERVERLOG_MODE, "%s reloaded configuration on %s",
		 sender->nick, pkt && *pkt ? pkt : Server_Name);
    if (!pkt || !*pkt || !strcasecmp (Server_Name, pkt))
    {
	config (0);
	/* since the motd is stored in memory, reread it */
	motd_close ();
	motd_init ();
#ifndef ROUTING_ONLY
	/* reread filter file */
	load_filter ();
	load_block ();
#endif
	load_server_auth ();

	/* since the servers file may have changed, ensure that there is
	 * no nickname that matches an alias for a server.
	 */
	list_foreach (Server_Auth, (list_callback_t) nick_check, 0);
    }
    /* pass the message even if this is the server we are reloading so that
     * everyone sees the message
     */
    pass_message_args (con, tag, ":%s %s", sender->nick,
		       pkt && *pkt ? pkt : Server_Name);
}
