/*
 * InspIRCd -- Internet Relay Chat Daemon
 *
 *   Copyright (C) 2009 Robin Burchell <robin+git@viroteck.net>
 *   Copyright (C) 2008 Pippijn van Steenhoven <pip88nl@gmail.com>
 *   Copyright (C) 2008 Craig Edwards <craigedwards@brainbox.cc>
 *   Copyright (C) 2007 Carsten Valdemar Munk <carsten.munk+inspircd@gmail.com>
 *
 * This file is part of InspIRCd.  InspIRCd 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, version 2.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */


#include "inspircd.h"
#include "users.h"
#include "channels.h"
#include "modules.h"

#include <ldap.h>

#ifdef _WIN32
# pragma comment(lib, "ldap.lib")
# pragma comment(lib, "lber.lib")
#endif

/* $ModDesc: Adds the ability to authenticate opers via LDAP */
/* $LinkerFlags: -lldap */

struct RAIILDAPString
{
	char *str;

	RAIILDAPString(char *Str)
		: str(Str)
	{
	}

	~RAIILDAPString()
	{
		ldap_memfree(str);
	}

	operator char*()
	{
		return str;
	}

	operator std::string()
	{
		return str;
	}
};

class ModuleLDAPAuth : public Module
{
	std::string base;
	std::string ldapserver;
	std::string username;
	std::string password;
	std::string attribute;
	int searchscope;
	LDAP *conn;

	bool HandleOper(LocalUser* user, const std::string& opername, const std::string& inputpass)
	{
		OperIndex::iterator it = ServerInstance->Config->oper_blocks.find(opername);
		if (it == ServerInstance->Config->oper_blocks.end())
			return false;

		ConfigTag* tag = it->second->oper_block;
		if (!tag)
			return false;

		std::string acceptedhosts = tag->getString("host");
		std::string hostname = user->ident + "@" + user->host;
		if (!InspIRCd::MatchMask(acceptedhosts, hostname, user->GetIPString()))
			return false;

		if (!LookupOper(opername, inputpass))
			return false;

		user->Oper(it->second);
		return true;
	}

public:
	ModuleLDAPAuth()
		: conn(NULL)
	{
	}

	void init() CXX11_OVERRIDE
	{
		Implementation eventlist[] = { I_OnRehash, I_OnPreCommand };
		ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
		OnRehash(NULL);
	}

	~ModuleLDAPAuth()
	{
		if (conn)
			ldap_unbind_ext(conn, NULL, NULL);
	}

	void OnRehash(User* user) CXX11_OVERRIDE
	{
		ConfigTag* tag = ServerInstance->Config->ConfValue("ldapoper");

		base 			= tag->getString("baserdn");
		ldapserver		= tag->getString("server");
		std::string scope	= tag->getString("searchscope");
		username		= tag->getString("binddn");
		password		= tag->getString("bindauth");
		attribute		= tag->getString("attribute");

		if (scope == "base")
			searchscope = LDAP_SCOPE_BASE;
		else if (scope == "onelevel")
			searchscope = LDAP_SCOPE_ONELEVEL;
		else searchscope = LDAP_SCOPE_SUBTREE;

		Connect();
	}

	bool Connect()
	{
		if (conn != NULL)
			ldap_unbind_ext(conn, NULL, NULL);
		int res, v = LDAP_VERSION3;
		res = ldap_initialize(&conn, ldapserver.c_str());
		if (res != LDAP_SUCCESS)
		{
			conn = NULL;
			return false;
		}

		res = ldap_set_option(conn, LDAP_OPT_PROTOCOL_VERSION, (void *)&v);
		if (res != LDAP_SUCCESS)
		{
			ldap_unbind_ext(conn, NULL, NULL);
			conn = NULL;
			return false;
		}
		return true;
	}

	ModResult OnPreCommand(std::string& command, std::vector<std::string>& parameters, LocalUser* user, bool validated, const std::string& original_line) CXX11_OVERRIDE
	{
		if (validated && command == "OPER" && parameters.size() >= 2)
		{
			if (HandleOper(user, parameters[0], parameters[1]))
				return MOD_RES_DENY;
		}
		return MOD_RES_PASSTHRU;
	}

	bool LookupOper(const std::string& opername, const std::string& opassword)
	{
		if (conn == NULL)
			if (!Connect())
				return false;

		int res;
		char* authpass = strdup(password.c_str());
		// bind anonymously if no bind DN and authentication are given in the config
		struct berval cred;
		cred.bv_val = authpass;
		cred.bv_len = password.length();

		if ((res = ldap_sasl_bind_s(conn, username.c_str(), LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL)) != LDAP_SUCCESS)
		{
			if (res == LDAP_SERVER_DOWN)
			{
				// Attempt to reconnect if the connection dropped
				ServerInstance->SNO->WriteToSnoMask('a', "LDAP server has gone away - reconnecting...");
				Connect();
				res = ldap_sasl_bind_s(conn, username.c_str(), LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL);
			}

			if (res != LDAP_SUCCESS)
			{
				free(authpass);
				ldap_unbind_ext(conn, NULL, NULL);
				conn = NULL;
				return false;
			}
		}
		free(authpass);

		LDAPMessage *msg, *entry;
		std::string what = attribute + "=" + opername;
		if ((res = ldap_search_ext_s(conn, base.c_str(), searchscope, what.c_str(), NULL, 0, NULL, NULL, NULL, 0, &msg)) != LDAP_SUCCESS)
		{
			return false;
		}
		if (ldap_count_entries(conn, msg) > 1)
		{
			ldap_msgfree(msg);
			return false;
		}
		if ((entry = ldap_first_entry(conn, msg)) == NULL)
		{
			ldap_msgfree(msg);
			return false;
		}
		authpass = strdup(opassword.c_str());
		cred.bv_val = authpass;
		cred.bv_len = opassword.length();
		RAIILDAPString DN(ldap_get_dn(conn, entry));
		if ((res = ldap_sasl_bind_s(conn, DN, LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL)) == LDAP_SUCCESS)
		{
			free(authpass);
			ldap_msgfree(msg);
			return true;
		}
		else
		{
			free(authpass);
			ldap_msgfree(msg);
			return false;
		}
	}

	Version GetVersion() CXX11_OVERRIDE
	{
		return Version("Adds the ability to authenticate opers via LDAP", VF_VENDOR);
	}

};

MODULE_INIT(ModuleLDAPAuth)
