/*
 *  The contents of this file are subject to the Initial
 *  Developer's Public License Version 1.0 (the "License");
 *  you may not use this file except in compliance with the
 *  License. You may obtain a copy of the License at
 *  http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
 *
 *  Software distributed under the License is distributed AS IS,
 *  WITHOUT WARRANTY OF ANY KIND, either express or implied.
 *  See the License for the specific language governing rights
 *  and limitations under the License.
 *
 *  The Original Code was created by Alexander Peshkov
 *  for the Firebird Open Source RDBMS project.
 *
 *  Copyright (c) 2016 Alexander Peshkov <peshkoff@mail.ru>
 *  and all contributors signed below.
 *
 *  All Rights Reserved.
 *  Contributor(s): ______________________________________.
 */

#include "firebird.h"
#include "fb_exception.h"
#include "iberror.h"

#include "../jrd.h"
#include "../common/isc_f_proto.h"
#include "../common/db_alias.h"
#include "../common/isc_proto.h"
#include "../common/Auth.h"
#include "../common/classes/GetPlugins.h"


using namespace Jrd;
using namespace Firebird;


namespace {

class DummyCryptKey FB_FINAL :
    public Firebird::AutoIface<Firebird::ICryptKeyImpl<DummyCryptKey, Firebird::CheckStatusWrapper> >
{
public:
	// ICryptKey implementation
	void setSymmetric(Firebird::CheckStatusWrapper* status, const char* type, unsigned keyLength, const void* key)
	{ }

	void setAsymmetric(Firebird::CheckStatusWrapper* status, const char* type, unsigned encryptKeyLength,
		const void* encryptKey, unsigned decryptKeyLength, const void* decryptKey)
	{ }

	const void* getEncryptKey(unsigned* length)
	{
		*length = 0;
		return NULL;
	}

	const void* getDecryptKey(unsigned* length)
	{
		*length = 0;
		return NULL;
	}
};

class SBlock;

class CBlock FB_FINAL : public RefCntIface<IClientBlockImpl<CBlock, CheckStatusWrapper> >
{
public:
	CBlock(const string& p_login, const string& p_password)
		: login(p_login), password(p_password)
	{ }

	// Firebird::IClientBlock implementation
	int release()
	{
		return 1;
	}

	const char* getLogin()
	{
		return login.c_str();
	}

	const char* getPassword()
	{
		return password.c_str();
	}

	const unsigned char* getData(unsigned int* length)
	{
		*length = data.getCount();
		return data.begin();
	}

	void putData(CheckStatusWrapper* status, unsigned int length, const void* d);

	Firebird::ICryptKey* newKey(Firebird::CheckStatusWrapper* status)
	{
		return &dummyCryptKey;
	}

	Firebird::IAuthBlock* getAuthBlock(Firebird::CheckStatusWrapper*)
	{
		return NULL;
	}

private:
	const string& login;
	const string& password;
	DummyCryptKey dummyCryptKey;
	UCharBuffer data;

	friend class SBlock;
	SBlock* sBlock;
};

class SBlock FB_FINAL : public AutoIface<IServerBlockImpl<SBlock, CheckStatusWrapper> >
{
public:
	explicit SBlock(CBlock* par)
		: cBlock(par)
	{
		cBlock->sBlock = this;
	}

	// Firebird::IServerBlock implementation
	const char* getLogin()
	{
		return cBlock->getLogin();
	}

	const unsigned char* getData(unsigned int* length)
	{
		*length = data.getCount();
		return data.begin();
	}

	void putData(CheckStatusWrapper* status, unsigned int length, const void* d);

	Firebird::ICryptKey* newKey(Firebird::CheckStatusWrapper* status)
	{
		return &dummyCryptKey;
	}

private:
	DummyCryptKey dummyCryptKey;
	UCharBuffer data;

	friend class CBlock;
	CBlock* cBlock;
};

void CBlock::putData(CheckStatusWrapper* status, unsigned int length, const void* d)
{
	void* to = sBlock->data.getBuffer(length);
	memcpy(to, d, length);
}

void SBlock::putData(CheckStatusWrapper* status, unsigned int length, const void* d)
{
	void* to = cBlock->data.getBuffer(length);
	memcpy(to, d, length);
}

} // anonymous namespace


namespace EDS {

bool validatePassword(thread_db* tdbb, const PathName& file, ClumpletWriter& dpb)
{
	// Peliminary checks - should we really validate password ourself
	if (!dpb.find(isc_dpb_user_name))		// check for user name presence
		return false;
	if (ISC_check_if_remote(file, false))	// check for remote connection
		return false;
	UserId* usr = tdbb->getAttachment()->att_user;
	if (!usr)
		return false;
	if (!usr->usr_auth_block.hasData())		// check for embedded attachment
		return false;

	Arg::Gds loginError(isc_login_error);

	// Build list of client/server plugins
	RefPtr<const Config> config;
	PathName list;
	expandDatabaseName(file, list /* unused value */, &config);
	PathName serverList = config->getPlugins(IPluginManager::TYPE_AUTH_SERVER);
	PathName clientList = config->getPlugins(IPluginManager::TYPE_AUTH_CLIENT);
	Auth::ParsedList::mergeLists(list, serverList, clientList);

	if (!list.hasData())
	{
		Arg::Gds noPlugins(isc_random);
		noPlugins << "No matching client/server authentication plugins configured for execute statement in embedded datasource";
		iscLogStatus(NULL, noPlugins.value());

#ifdef DEV_BUILD
		loginError << noPlugins;
#endif
		loginError.raise();
	}

	// Analyze DPB
	string login;
	if (dpb.find(isc_dpb_user_name))
	{
		dpb.getString(login);
		fb_utils::dpbItemUpper(login);
	}
	string password;
	if (dpb.find(isc_dpb_password))
		dpb.getString(password);

	// Try each plugin from the merged list
	for (GetPlugins<IClient> client(IPluginManager::TYPE_AUTH_CLIENT, config, list.c_str());
		client.hasData(); client.next())
	{
		GetPlugins<IServer> server(IPluginManager::TYPE_AUTH_SERVER, config, client.name());
		if (!server.hasData())
			continue;

		CBlock cBlock(login, password);
		SBlock sBlock(&cBlock);
		Auth::WriterImplementation writer;
		writer.setPlugin(client.name());

		const int MAXLIMIT = 100;
		for (int limit = 0; limit < MAXLIMIT; ++limit)		// avoid endless AUTH_MORE_DATA loop
		{
			LocalStatus ls;
			CheckStatusWrapper s(&ls);
			ISC_STATUS code = isc_login;

			switch (client.plugin()->authenticate(&s, &cBlock))
			{
			case IAuth::AUTH_SUCCESS:
			case IAuth::AUTH_MORE_DATA:
				break;

			case IAuth::AUTH_CONTINUE:
				limit = MAXLIMIT;
				continue;

			case IAuth::AUTH_FAILED:
				if (s.getState() & Firebird::IStatus::STATE_ERRORS)
				{
					iscLogStatus("Authentication failed, client plugin:", &s);
					code = isc_login_error;
				}
				(Arg::Gds(code)
#ifdef DEV_BUILD
								 << Arg::StatusVector(&s)
#endif
								 ).raise();
				break;	// compiler silencer
			}

			switch (server.plugin()->authenticate(&s, &sBlock, &writer))
			{
			case IAuth::AUTH_SUCCESS:
				// remove isc_dpb_user_name cause it has precedence over isc_dpb_auth_block
				dpb.deleteWithTag(isc_dpb_user_name);
				// isc_dpb_password makes no sense w/o isc_dpb_user_name
				dpb.deleteWithTag(isc_dpb_password);
				// save built by this routine isc_dpb_auth_block
				writer.store(&dpb, isc_dpb_auth_block);
				return true;

			case IAuth::AUTH_CONTINUE:
				limit = MAXLIMIT;
				continue;

			case IAuth::AUTH_MORE_DATA:
				break;

			case IAuth::AUTH_FAILED:
				if (s.getState() & Firebird::IStatus::STATE_ERRORS)
				{
					iscLogStatus("Authentication faled, server plugin:", &s);
					code = isc_login_error;
				}
				(Arg::Gds(code)
#ifdef DEV_BUILD
								 << Arg::StatusVector(&s)
#endif
								 ).raise();
				break;	// compiler silencer
			}
		}
	}

	Arg::Gds(isc_login).raise();
	return false;		// warning silencer
}

} // namespace EDS
