/*
 * simplesasl.cpp - Simple SASL implementation
 * Copyright (C) 2003  Justin Karneges
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "simplesasl.h"

#include <tqhostaddress.h>
#include <tqstringlist.h>
#include <tqptrlist.h>
#include <tqvaluelist.h>
#include <tqca.h>
#include <stdlib.h>
#include "base64.h"

namespace XMPP
{

struct Prop
{
	TQCString var, val;
};

class PropList : public TQValueList<Prop>
{
public:
	PropList() : TQValueList<Prop>()
	{
	}

	void set(const TQCString &var, const TQCString &val)
	{
		Prop p;
		p.var = var;
		p.val = val;
		append(p);
	}

	TQCString get(const TQCString &var)
	{
		for(ConstIterator it = begin(); it != end(); ++it) {
			if((*it).var == var)
				return (*it).val;
		}
		return TQCString();
	}

	TQCString toString() const
	{
		TQCString str;
		bool first = true;
		for(ConstIterator it = begin(); it != end(); ++it) {
			if(!first)
				str += ',';
			str += (*it).var + "=\"" + (*it).val + '\"';
			first = false;
		}
		return str;
	}

	bool fromString(const TQCString &str)
	{
		PropList list;
		int at = 0;
		while(1) {
			int n = str.find('=', at);
			if(n == -1)
				break;
			TQCString var, val;
			var = str.mid(at, n-at);
			at = n + 1;
			if(str[at] == '\"') {
				++at;
				n = str.find('\"', at);
				if(n == -1)
					break;
				val = str.mid(at, n-at);
				at = n + 1;
			}
			else {
				n = str.find(',', at);
				if(n != -1) {
					val = str.mid(at, n-at);
					at = n;
				}
				else {
					val = str.mid(at);
					at = str.length()-1;
				}
			}
			Prop prop;
			prop.var = var;
			prop.val = val;
			list.append(prop);

			if(str[at] != ',')
				break;
			++at;
		}

		// integrity check
		if(list.varCount("nonce") != 1)
			return false;
		if(list.varCount("algorithm") != 1)
			return false;
		*this = list;
		return true;
	}

	int varCount(const TQCString &var)
	{
		int n = 0;
		for(ConstIterator it = begin(); it != end(); ++it) {
			if((*it).var == var)
				++n;
		}
		return n;
	}

	TQStringList getValues(const TQCString &var)
	{
		TQStringList list;
		for(ConstIterator it = begin(); it != end(); ++it) {
			if((*it).var == var)
				list += (*it).val;
		}
		return list;
	}
};

class SimpleSASLContext : public TQCA_SASLContext
{
public:
	// core props
	TQString service, host;

	// state
	int step;
	TQByteArray in_buf;
	TQString out_mech;
	TQByteArray out_buf;
	bool capable;
	bool allow_plain;
	int err;

	TQCA_SASLNeedParams need;
	TQCA_SASLNeedParams have;
	TQString user, authz, pass, realm;

	SimpleSASLContext()
	{
		reset();
	}

	~SimpleSASLContext()
	{
		reset();
	}

	void reset()
	{
		resetState();
		resetParams();
	}

	void resetState()
	{
		out_mech = TQString();
		out_buf.resize(0);
		err = -1;
	}

	void resetParams()
	{
		capable = true;
		need.user = false;
		need.authzid = false;
		need.pass = false;
		need.realm = false;
		have.user = false;
		have.authzid = false;
		have.pass = false;
		have.realm = false;
		user = TQString();
		authz = TQString();
		pass = TQString();
		realm = TQString();
	}

	void setCoreProps(const TQString &_service, const TQString &_host, TQCA_SASLHostPort *, TQCA_SASLHostPort *)
	{
		service = _service;
		host = _host;
	}

	void setSecurityProps(bool noPlain, bool, bool, bool, bool reqForward, bool reqCreds, bool reqMutual, int ssfMin, int, const TQString &, int)
	{
		if(reqForward || reqCreds || reqMutual || ssfMin > 0)
			capable = false;
		else
			capable = true;
		allow_plain = !noPlain;
	}

	int security() const
	{
		return 0;
	}

	int errorCond() const
	{
		return err;
	}

	bool clientStart(const TQStringList &mechlist)
	{
		bool haveMech = false;
		resetState();
		step = 0;

		for(TQStringList::ConstIterator it = mechlist.begin(); it != mechlist.end(); ++it) {
			if((*it) == "PLAIN" && allow_plain) {
				out_mech = "PLAIN";
				haveMech = true;
				break;
			}
			if((*it) == "DIGEST-MD5") {
				out_mech = "DIGEST-MD5";
				haveMech = true;
				break;
			}
		}
		if(!capable || !haveMech) {
			err = TQCA::SASL::NoMech;
			return false;
		}
		return true;
	}

	int clientFirstStep(bool)
	{
		return clientTryAgain();
	}

	bool serverStart(const TQString &, TQStringList *, const TQString &)
	{
		return false;
	}

	int serverFirstStep(const TQString &, const TQByteArray *)
	{
		return Error;
	}

	TQCA_SASLNeedParams clientParamsNeeded() const
	{
		return need;
	}

	void setClientParams(const TQString *_user, const TQString *_authzid, const TQString *_pass, const TQString *_realm)
	{
		if(_user) {
			user = *_user;
			need.user = false;
			have.user = true;
		}
		if(_authzid) {
			authz = *_authzid;
			need.authzid = false;
			have.authzid = true;
		}
		if(_pass) {
			pass = *_pass;
			need.pass = false;
			have.pass = true;
		}
		if(_realm) {
			realm = *_realm;
			need.realm = false;
			have.realm = true;
		}
	}

	TQString username() const
	{
		return TQString();
	}

	TQString authzid() const
	{
		return TQString();
	}

	int nextStep(const TQByteArray &in)
	{
		in_buf = in.copy();
		return tryAgain();
	}

	int tryAgain()
	{
		return clientTryAgain();
	}

	TQString mech() const
	{
		return out_mech;
	}

	const TQByteArray *clientInit() const
	{
		return out_mech == "PLAIN" ? &out_buf : 0;
	}

	TQByteArray result() const
	{
		return out_buf;
	}

	int clientTryAgain()
	{
		if( out_mech == "PLAIN" ) {
			if(step == 0) {
				// First, check if we have everything
				if(need.user || need.pass) {
					err = -1;
					return Error;
				}
				if(!have.user) {
					need.user = true;
				}
				if(!have.pass) {
					need.pass = true;
				}
				if(need.user || need.pass) {
					return NeedParams;
				}

				TQCString authz_ = authz.utf8();
				TQCString user_ = user.utf8();
				TQCString pass_ = pass.utf8();
				int l = 0;

				out_buf.resize(authz_.length() + 1 + user_.length() + 1 + pass_.length());
				memcpy(&out_buf[l], authz_.data(), authz_.length());
				l += authz_.length();
				out_buf[l] = '\0';
				l += 1;
				memcpy(&out_buf[l], user_.data(), user_.length());
				l += user_.length();
				out_buf[l] = '\0';
				l += 1;

				memcpy(&out_buf[l], pass_.data(), pass_.length());
				++step;
				return Continue;
			}

			out_buf.resize(0);
			return Success;
		}

		if( out_mech == "DIGEST-MD5" ) {
			if(step == 0) {
				++step;
				return Continue;
			}
			else if(step == 1) {
				// if we still need params, then the app has failed us!
				if(need.user || need.authzid || need.pass || need.realm) {
					err = -1;
					return Error;
				}

				// see if some params are needed
				if(!have.user)
					need.user = true;
				if(!have.authzid)
					need.authzid = true;
				if(!have.pass)
					need.pass = true;
				if(need.user || need.authzid || need.pass)
					return NeedParams;

				// get props
				TQCString cs(in_buf.data(), in_buf.size()+1);
				PropList in;
				if(!in.fromString(cs)) {
					err = TQCA::SASL::BadProto;
					return Error;
				}

				// make a cnonce
				TQByteArray a(32);
				for(int n = 0; n < (int)a.size(); ++n)
					a[n] = (char)(256.0*rand()/(RAND_MAX+1.0));
				TQCString cnonce = Base64::arrayToString(a).latin1();

				// make other variables
				realm = host;
				TQCString nonce = in.get("nonce");
				TQCString nc = "00000001";
				TQCString uri = service.utf8() + '/' + host.utf8();
				TQCString qop = "auth";

				// build 'response'
				TQCString X = user.utf8() + ':' + realm.utf8() + ':' + pass.utf8();
				TQByteArray Y = TQCA::MD5::hash(X);
				TQCString tmp = TQCString(":") + nonce + ':' + cnonce + ':' + authz.utf8();
				TQByteArray A1(Y.size() + tmp.length());
				memcpy(A1.data(), Y.data(), Y.size());
				memcpy(A1.data() + Y.size(), tmp.data(), tmp.length());
				TQCString A2 = "AUTHENTICATE:" + uri;
				TQCString HA1 = TQCA::MD5::hashToString(A1).latin1();
				TQCString HA2 = TQCA::MD5::hashToString(A2).latin1();
				TQCString KD = HA1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + HA2;
				TQCString Z = TQCA::MD5::hashToString(KD).latin1();

				// build output
				PropList out;
				out.set("username", user.utf8());
				out.set("realm", host.utf8());
				out.set("nonce", nonce);
				out.set("cnonce", cnonce);
				out.set("nc", nc);
				out.set("serv-type", service.utf8());
				out.set("host", host.utf8());
				out.set("digest-uri", uri);
				out.set("qop", qop);
				out.set("response", Z);
				out.set("charset", "utf-8");
				out.set("authzid", authz.utf8());
				TQCString s = out.toString();

				// done
				out_buf.resize(s.length());
				memcpy(out_buf.data(), s.data(), out_buf.size());
				++step;
				return Continue;
			}

			out_buf.resize(0);
			return Success;
		}

		err = TQCA::SASL::NoMech;
		return Error;
	}

	bool encode(const TQByteArray &a, TQByteArray *b)
	{
		*b = a.copy();
		return true;
	}

	bool decode(const TQByteArray &a, TQByteArray *b)
	{
		*b = a.copy();
		return true;
	}
};

class TQCASimpleSASL : public TQCAProvider
{
public:
	TQCASimpleSASL() {}
	~TQCASimpleSASL() {}

	void init()
	{
	}

	int qcaVersion() const
	{
		return TQCA_PLUGIN_VERSION;
	}

	int capabilities() const
	{
		return TQCA::CAP_SASL;
	}

	void *context(int cap)
	{
		if(cap == TQCA::CAP_SASL)
			return new SimpleSASLContext;
		return 0;
	}
};

TQCAProvider *createProviderSimpleSASL()
{
	return (new TQCASimpleSASL);
}

}
