/*
 * WebIRC / CGI:IRC Support
 * (C) Copyright 2006-.. Bram Matthys (Syzop) and the UnrealIRCd team
 *
 * 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 1, 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 "unrealircd.h"

/* Types */
typedef struct _configitem_webirc ConfigItem_webirc;

typedef enum {
	WEBIRC_PASS=1, WEBIRC_WEBIRC=2
} WEBIRCType;

struct _configitem_webirc {
	ConfigItem_webirc *prev, *next;
	ConfigFlag flag;
	ConfigItem_mask *mask;
	WEBIRCType type;
	anAuthStruct *auth;
};

/* Module header */
ModuleHeader MOD_HEADER(webirc)
= {
	"webirc",
	"$Id$",
	"WebIRC/CGI:IRC Support",
	"3.2-b8-1",
	NULL 
};

/* Global variables */
ModDataInfo *webirc_md = NULL;
ConfigItem_webirc *conf_webirc = NULL;

/* Forward declarations */
CMD_FUNC(m_webirc);
int webirc_check_init(aClient *cptr, char *sockn, size_t size);
int webirc_local_pass(aClient *sptr, char *password);
int webirc_config_test(ConfigFile *, ConfigEntry *, int, int *);
int webirc_config_run(ConfigFile *, ConfigEntry *, int);
void webirc_free_conf(void);
void delete_webircblock(ConfigItem_webirc *e);
void webirc_md_free(ModData *md);

#define IsWEBIRC(x)     (moddata_client(x, webirc_md).l)
#define SetWEBIRC(x)	do { moddata_client(x, webirc_md).l = 1; } while(0)
#define ClearWEBIRC(x)	do { moddata_client(x, webirc_md).l = 0; } while(0)

#define MSG_WEBIRC "WEBIRC"

MOD_TEST(webirc)
{
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, webirc_config_test);
	return MOD_SUCCESS;
}

/** Called upon module init */
MOD_INIT(webirc)
{
	ModDataInfo mreq;

	MARK_AS_OFFICIAL_MODULE(modinfo);
	
	memset(&mreq, 0, sizeof(mreq));
	mreq.name = "webirc";
	mreq.type = MODDATATYPE_CLIENT;
	mreq.free = webirc_md_free;
	webirc_md = ModDataAdd(modinfo->handle, mreq);
	if (!webirc_md)
	{
		config_error("could not register webirc moddata");
		return MOD_FAILED;
	}

	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, webirc_config_run);
	HookAdd(modinfo->handle, HOOKTYPE_CHECK_INIT, 0, webirc_check_init);
	HookAdd(modinfo->handle, HOOKTYPE_LOCAL_PASS, 0, webirc_local_pass);

	CommandAdd(modinfo->handle, MSG_WEBIRC, m_webirc, MAXPARA, M_UNREGISTERED);
		
	return MOD_SUCCESS;
}

/** Called upon module load */
MOD_LOAD(webirc)
{
	return MOD_SUCCESS;
}

/** Called upon unload */
MOD_UNLOAD(webirc)
{
	webirc_free_conf();
	return MOD_SUCCESS;
}

void webirc_free_conf(void)
{
	ConfigItem_webirc *webirc_ptr, *next;

	for (webirc_ptr = conf_webirc; webirc_ptr; webirc_ptr = next)
	{
		next = webirc_ptr->next;
		delete_webircblock(webirc_ptr);
	}
	conf_webirc = NULL;
}

void delete_webircblock(ConfigItem_webirc *e)
{
	unreal_delete_masks(e->mask);
	if (e->auth)
		Auth_DeleteAuthStruct(e->auth);
	DelListItem(e, conf_webirc);
	MyFree(e);
}

int webirc_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	ConfigEntry	*cep, *cepp;
	int errors = 0;
	char has_mask = 0; /* mandatory */
	char has_password = 0; /* mandatory */
	char has_type = 0; /* optional (used for dup checking) */
	WEBIRCType webirc_type = WEBIRC_WEBIRC; /* the default */

	if (type != CONFIG_MAIN)
		return 0;
	
	if (!ce || !ce->ce_varname)
		return 0;
	
	if (!strcmp(ce->ce_varname, "cgiirc"))
	{
		config_error("%s:%i: the cgiirc block has been renamed to webirc and "
		             "the syntax has changed in UnrealIRCd 3.4.x",
		             ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
		need_34_upgrade = 1;
		*errs = 1;
		return -1;
	}

	if (strcmp(ce->ce_varname, "webirc"))
		return 0; /* not interested in non-webirc stuff.. */

	/* Now actually go parse the webirc { } block */
	for (cep = ce->ce_entries; cep; cep = cep->ce_next)
	{
		if (!cep->ce_varname)
		{
			config_error_blank(cep->ce_fileptr->cf_filename, cep->ce_varlinenum, "webirc");
			errors++;
			continue;
		}
		if (!cep->ce_vardata)
		{
			config_error_empty(cep->ce_fileptr->cf_filename, cep->ce_varlinenum,
				"webirc", cep->ce_varname);
			errors++;
			continue;
		}
		if (!strcmp(cep->ce_varname, "mask"))
		{
			if (cep->ce_vardata || cep->ce_entries)
				has_mask = 1;
		}
		else if (!strcmp(cep->ce_varname, "password"))
		{
			if (has_password)
			{
				config_warn_duplicate(cep->ce_fileptr->cf_filename, 
					cep->ce_varlinenum, "webirc::password");
				continue;
			}
			has_password = 1;
			if (Auth_CheckError(cep) < 0)
				errors++;
		}
		else if (!strcmp(cep->ce_varname, "type"))
		{
			if (has_type)
			{
				config_warn_duplicate(cep->ce_fileptr->cf_filename,
					cep->ce_varlinenum, "webirc::type");
			}
			has_type = 1;
			if (!strcmp(cep->ce_vardata, "webirc"))
				webirc_type = WEBIRC_WEBIRC;
			else if (!strcmp(cep->ce_vardata, "old"))
				webirc_type = WEBIRC_PASS;
			else
			{
				config_error("%s:%i: unknown webirc::type '%s', should be either 'webirc' or 'old'",
					cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_vardata);
				errors++;
			}
		}
		else
		{
			config_error_unknown(cep->ce_fileptr->cf_filename, cep->ce_varlinenum,
				"webirc", cep->ce_varname);
			errors++;
		}
	}
	if (!has_mask)
	{
		config_error_missing(ce->ce_fileptr->cf_filename, ce->ce_varlinenum,
			"webirc::mask");
		errors++;
	}

	if (!has_password && (webirc_type == WEBIRC_WEBIRC))
	{
		config_error_missing(ce->ce_fileptr->cf_filename, ce->ce_varlinenum,
			"webirc::password");
		errors++;
	}
	
	if (has_password && (webirc_type == WEBIRC_PASS))
	{
		config_error("%s:%i: webirc block has type set to 'old' but has a password set. "
		             "Passwords are not used with type 'old'. Either remove the password or "
		             "use the 'webirc' method instead.",
		             ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
		errors++;
	}

	*errs = errors;
	return errors ? -1 : 1;
}

int webirc_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
	ConfigEntry *cep;
	ConfigEntry *cepp;
	ConfigItem_webirc *webirc = NULL;
	
	if (type != CONFIG_MAIN)
		return 0;
	
	if (!ce || !ce->ce_varname || strcmp(ce->ce_varname, "webirc"))
		return 0; /* not interested */

	webirc = (ConfigItem_webirc *) MyMallocEx(sizeof(ConfigItem_webirc));
	webirc->type = WEBIRC_WEBIRC; /* default */

	for (cep = ce->ce_entries; cep; cep = cep->ce_next)
	{
		if (!strcmp(cep->ce_varname, "mask"))
			unreal_add_masks(&webirc->mask, cep);
		else if (!strcmp(cep->ce_varname, "password"))
			webirc->auth = Auth_ConvertConf2AuthStruct(cep);
		else if (!strcmp(cep->ce_varname, "type"))
		{
			if (!strcmp(cep->ce_vardata, "webirc"))
				webirc->type = WEBIRC_WEBIRC;
			else if (!strcmp(cep->ce_vardata, "old"))
				webirc->type = WEBIRC_PASS;
			else
				abort();
		}
	}
	
	AddListItem(webirc, conf_webirc);
	
	return 0;
}

void webirc_md_free(ModData *md)
{
	/* we have nothing to free actually, but we must set to zero */
	md->l = 0;
}

ConfigItem_webirc *Find_webirc(aClient *sptr, WEBIRCType type)
{
	ConfigItem_webirc *e;

	for (e = conf_webirc; e; e = e->next)
	{
		if ((e->type == type) && unreal_mask_match(sptr, e->mask))
			return e;
	}

	return NULL;
}

#define WEBIRC_STRING     "WEBIRC_"
#define WEBIRC_STRINGLEN  (sizeof(WEBIRC_STRING)-1)

/* Does the CGI:IRC host spoofing work */
int dowebirc(aClient *cptr, char *ip, char *host)
{
	char scratch[64];
	char *sockhost;

	if (IsWEBIRC(cptr))
		return exit_client(cptr, cptr, &me, "Double CGI:IRC request (already identified)");

	if (host && !strcmp(ip, host))
		host = NULL; /* host did not resolve, make it NULL */

	/* STEP 1: Update cptr->local->ip
	   inet_pton() returns 1 on success, 0 on bad input, -1 on bad AF */
	if ((inet_pton(AF_INET, ip, scratch) != 1) &&
	    (inet_pton(AF_INET6, ip, scratch) != 1))
	{
		/* then we have an invalid IP */
		return exit_client(cptr, cptr, &me, "Invalid IP address");
	}

	/* STEP 2: Update GetIP() */
	safefree(cptr->ip);
	cptr->ip = strdup(ip);
		
	/* STEP 3: Update cptr->local->hostp */
	/* (free old) */
	if (cptr->local->hostp)
	{
		unreal_free_hostent(cptr->local->hostp);
		cptr->local->hostp = NULL;
	}
	/* (create new) */
	if (host && verify_hostname(host))
		cptr->local->hostp = unreal_create_hostent(host, cptr->ip);

	/* STEP 4: Update sockhost
	   Make sure that if this any IPv4 address is _not_ prefixed with
	   "::ffff:" by using Inet_ia2p().
	 */
	// Hmm I ignored above warning. May be bad during transition period.
	strlcpy(cptr->local->sockhost, cptr->ip, sizeof(cptr->local->sockhost));

	SetWEBIRC(cptr);

	/* Check (g)zlines right now; these are normally checked upon accept(),
	 * but since we know the IP only now after PASS/WEBIRC, we have to check
	 * here again...
	 */
	return check_banned(cptr);
}

/* WEBIRC <pass> "cgiirc" <hostname> <ip> */
CMD_FUNC(m_webirc)
{
	char *ip, *host, *password;
	size_t ourlen;
	ConfigItem_webirc *e;

	if ((parc < 5) || BadPtr(parv[4]))
	{
		sendto_one(cptr, err_str(ERR_NEEDMOREPARAMS), me.name, "*", "WEBIRC");
		return -1;
	}

	password = parv[1];
	host = !DONT_RESOLVE ? parv[3] : parv[4];
	ip = parv[4];

	/* Check if allowed host */
	e = Find_webirc(sptr, WEBIRC_WEBIRC);
	if (!e)
		return exit_client(cptr, sptr, &me, "CGI:IRC -- No access");

	/* Check password */
	if (Auth_Check(sptr, e->auth, password) == -1)
		return exit_client(cptr, sptr, &me, "CGI:IRC -- Invalid password");

	/* And do our job.. */
	return dowebirc(cptr, ip, host);
}


int webirc_check_init(aClient *cptr, char *sockn, size_t size)
{
	if (IsWEBIRC(cptr))
	{
		strlcpy(sockn, GetIP(cptr), size); /* use already set value */
		return 0;
	}
	
	return 1; /* nothing to do */
}

int webirc_local_pass(aClient *sptr, char *password)
{
	if (!strncmp(password, WEBIRC_STRING, WEBIRC_STRINGLEN))
	{
		char *ip, *host;
		ConfigItem_webirc *e;

		e = Find_webirc(sptr, WEBIRC_PASS);
		if (e)
		{
			/* Ok now we got that sorted out, proceed:
			 * Syntax: WEBIRC_<ip>_<resolvedhostname>
			 * The <resolvedhostname> has been checked ip->host AND host->ip by CGI:IRC itself
			 * already so we trust it.
			 */
			ip = password + WEBIRC_STRINGLEN;
			host = strchr(ip, '_');
			if (!host)
				return exit_client(sptr, sptr, &me, "Invalid CGI:IRC IP received");
			*host++ = '\0';
		
			return dowebirc(sptr, ip, host);
		}
		/* falltrough if not in webirc block.. */
	}

	return 0; /* not webirc */
}
