/*
 * Copyright (C) 2003-2007  Justin Karneges <justin@affinix.com>
 * Copyright (C) 2004,2005  Brad Hards <bradh@frogmouth.net>
 *
 * 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 "qca_securelayer.h"

#include "qcaprovider.h"
#include "qca_safeobj.h"
#include "qca_safetimer.h"

#include <QPointer>
#include <QMetaMethod>

namespace QCA {

Provider::Context *getContext(const QString &type, const QString &provider);

enum ResetMode
{
	ResetSession        = 0,
	ResetSessionAndData = 1,
	ResetAll            = 2
};

//----------------------------------------------------------------------------
// LayerTracker
//----------------------------------------------------------------------------
class LayerTracker
{
private:
	struct Item
	{
		int plain;
		qint64 encoded;
	};

	int p;
	QList<Item> list;

public:
	LayerTracker()
	{
		p = 0;
	}

	void reset()
	{
		p = 0;
		list.clear();
	}

	void addPlain(int plain)
	{
		p += plain;
	}

	void specifyEncoded(int encoded, int plain)
	{
		// can't specify more bytes than we have
		if(plain > p)
			plain = p;
		p -= plain;
		Item i;
		i.plain = plain;
		i.encoded = encoded;
		list += i;
	}

	int finished(qint64 encoded)
	{
		int plain = 0;
		for(QList<Item>::Iterator it = list.begin(); it != list.end();)
		{
			Item &i = *it;

			// not enough?
			if(encoded < i.encoded)
			{
				i.encoded -= encoded;
				break;
			}

			encoded -= i.encoded;
			plain += i.plain;
			it = list.erase(it);
		}
		return plain;
	}
};

//----------------------------------------------------------------------------
// SecureLayer
//----------------------------------------------------------------------------
SecureLayer::SecureLayer(QObject *parent)
:QObject(parent)
{
}

bool SecureLayer::isClosable() const
{
	return false;
}

void SecureLayer::close()
{
}

QByteArray SecureLayer::readUnprocessed()
{
	return QByteArray();
}

//----------------------------------------------------------------------------
// TLSSession
//----------------------------------------------------------------------------
TLSSession::TLSSession()
{
}

TLSSession::TLSSession(const TLSSession &from)
:Algorithm(from)
{
}

TLSSession::~TLSSession()
{
}

TLSSession & TLSSession::operator=(const TLSSession &from)
{
	Algorithm::operator=(from);
	return *this;
}

bool TLSSession::isNull() const
{
	return (!context() ? true : false);
}

//----------------------------------------------------------------------------
// TLS
//----------------------------------------------------------------------------
class TLS::Private : public QObject
{
	Q_OBJECT
public:
	enum
	{
		OpStart,
		OpUpdate
	};

	enum State
	{
		Inactive,
		Initializing,
		Handshaking,
		Connected,
		Closing
	};

	class Action
	{
	public:
		enum Type
		{
			ReadyRead,
			ReadyReadOutgoing,
			Handshaken,
			Close,
			CheckPeerCertificate,
			CertificateRequested,
			HostNameReceived
		};

		int type;

		Action(int _type) : type(_type)
		{
		}
	};

	TLS *q;
	TLSContext *c;
	TLS::Mode mode;

	// signal connected flags
	bool connect_hostNameReceived;
	bool connect_certificateRequested;
	bool connect_peerCertificateAvailable;
	bool connect_handshaken;

	// persistent settings (survives ResetSessionAndData)
	CertificateChain localCert;
	PrivateKey localKey;
	CertificateCollection trusted;
	bool con_ssfMode;
	int con_minSSF, con_maxSSF;
	QStringList con_cipherSuites;
	bool tryCompress;
	int packet_mtu;
	QList<CertificateInfoOrdered> issuerList;
	TLSSession session;

	// session
	State state;
	bool blocked;
	bool server;
	QString host;
	TLSContext::SessionInfo sessionInfo;
	SafeTimer actionTrigger;
	int op;
	QList<Action> actionQueue;
	bool need_update;
	bool maybe_input;
	bool emitted_hostNameReceived;
	bool emitted_certificateRequested;
	bool emitted_peerCertificateAvailable;

	// data (survives ResetSession)
	CertificateChain peerCert;
	Validity peerValidity;
	bool hostMismatch;
	Error errorCode;

	// stream i/o
	QByteArray in, out;
	QByteArray to_net, from_net;
	QByteArray unprocessed;
	int out_pending;
	int to_net_encoded;
	LayerTracker layer;

	// datagram i/o
	QList<QByteArray> packet_in, packet_out;
	QList<QByteArray> packet_to_net, packet_from_net;
	int packet_out_pending; // packet count
	QList<int> packet_to_net_encoded;

	Private(TLS *_q, TLS::Mode _mode) : QObject(_q), q(_q), mode(_mode), actionTrigger(this)
	{
		// c is 0 during initial reset, so we don't redundantly reset it
		c = nullptr;
		connect_hostNameReceived = false;
		connect_certificateRequested = false;
		connect_peerCertificateAvailable = false;
		connect_handshaken = false;
		server = false;

		connect(&actionTrigger, &SafeTimer::timeout, this, &Private::doNextAction);
		actionTrigger.setSingleShot(true);

		reset(ResetAll);

		c = static_cast<TLSContext *>(q->context());

		// parent the context to us, so that moveToThread works
		c->setParent(this);

		connect(c, &TLSContext::resultsReady, this, &Private::tls_resultsReady);
		connect(c, &TLSContext::dtlsTimeout, this, &Private::tls_dtlsTimeout);
	}

	~Private() override
	{
		// context is owned by Algorithm, unparent so we don't double-delete
		c->setParent(nullptr);
	}

	void reset(ResetMode mode)
	{
		if(c)
			c->reset();

		// if we reset while in client mode, then clear this list
		//   (it should only persist when used for server mode)
		if(!server)
			issuerList.clear();

		state = Inactive;
		blocked = false;
		server = false;
		host = QString();
		sessionInfo = TLSContext::SessionInfo();
		actionTrigger.stop();
		op = -1;
		actionQueue.clear();
		need_update = false;
		maybe_input = false;
		emitted_hostNameReceived = false;
		emitted_certificateRequested = false;
		emitted_peerCertificateAvailable = false;

		out.clear();
		out_pending = 0;
		packet_out.clear();
		packet_out_pending = 0;

		if(mode >= ResetSessionAndData)
		{
			peerCert = CertificateChain();
			peerValidity = ErrorValidityUnknown;
			hostMismatch = false;
			errorCode = (TLS::Error)-1;

			in.clear();
			to_net.clear();
			from_net.clear();
			unprocessed.clear();
			to_net_encoded = 0;
			layer.reset();

			packet_in.clear();
			packet_to_net.clear();
			packet_from_net.clear();
			packet_to_net_encoded.clear();
		}

		if(mode >= ResetAll)
		{
			localCert = CertificateChain();
			localKey = PrivateKey();
			trusted = CertificateCollection();
			con_ssfMode = true;
			con_minSSF = 128;
			con_maxSSF = -1;
			con_cipherSuites = QStringList();
			tryCompress = false;
			packet_mtu = -1;
			issuerList.clear();
			session = TLSSession();
		}
	}

	void start(bool serverMode)
	{
		state = Initializing;
		server = serverMode;

		c->setup(serverMode, host, tryCompress);

		if(con_ssfMode)
			c->setConstraints(con_minSSF, con_maxSSF);
		else
			c->setConstraints(con_cipherSuites);

		c->setCertificate(localCert, localKey);
		c->setTrustedCertificates(trusted);
		if(serverMode)
			c->setIssuerList(issuerList);
		if(!session.isNull())
		{
			TLSSessionContext *sc = static_cast<TLSSessionContext*>(session.context());
			c->setSessionId(*sc);
		}
		c->setMTU(packet_mtu);

		QCA_logTextMessage(QStringLiteral("tls[%1]: c->start()").arg(q->objectName()), Logger::Information);
		op = OpStart;
		c->start();
	}

	void close()
	{
		QCA_logTextMessage(QStringLiteral("tls[%1]: close").arg(q->objectName()), Logger::Information);

		if(state != Connected)
			return;

		state = Closing;
		c->shutdown();
	}

	void continueAfterStep()
	{
		QCA_logTextMessage(QStringLiteral("tls[%1]: continueAfterStep").arg(q->objectName()), Logger::Information);

		if(!blocked)
			return;

		blocked = false;
		update();
	}

	void processNextAction()
	{
		if(actionQueue.isEmpty())
		{
			if(need_update)
			{
				QCA_logTextMessage(QStringLiteral("tls[%1]: need_update").arg(q->objectName()), Logger::Information);
				update();
			}
			return;
		}

		const Action a = actionQueue.takeFirst();

		// set up for the next one, if necessary
		if(!actionQueue.isEmpty() || need_update)
		{
			if(!actionTrigger.isActive())
				actionTrigger.start();
		}

		if(a.type == Action::ReadyRead)
		{
			emit q->readyRead();
		}
		else if(a.type == Action::ReadyReadOutgoing)
		{
			emit q->readyReadOutgoing();
		}
		else if(a.type == Action::Handshaken)
		{
			state = Connected;

			// write any app data waiting during handshake
			if(!out.isEmpty())
			{
				need_update = true;
				if(!actionTrigger.isActive())
					actionTrigger.start();
			}

			QCA_logTextMessage(QStringLiteral("tls[%1]: handshaken").arg(q->objectName()), Logger::Information);

			if(connect_handshaken)
			{
				blocked = true;
				emit q->handshaken();
			}
		}
		else if(a.type == Action::Close)
		{
			unprocessed = c->unprocessed();
			reset(ResetSession);
			emit q->closed();
		}
		else if(a.type == Action::CheckPeerCertificate)
		{
			peerCert = c->peerCertificateChain();
			if(!peerCert.isEmpty())
			{
				peerValidity = c->peerCertificateValidity();
				if(peerValidity == ValidityGood && !host.isEmpty() && !peerCert.primary().matchesHostName(host))
					hostMismatch = true;
			}

			if(connect_peerCertificateAvailable)
			{
				blocked = true;
				emitted_peerCertificateAvailable = true;
				emit q->peerCertificateAvailable();
			}
		}
		else if(a.type == Action::CertificateRequested)
		{
			issuerList = c->issuerList();
			if(connect_certificateRequested)
			{
				blocked = true;
				emitted_certificateRequested = true;
				emit q->certificateRequested();
			}
		}
		else if(a.type == Action::HostNameReceived)
		{
			if(connect_hostNameReceived)
			{
				blocked = true;
				emitted_hostNameReceived = true;
				emit q->hostNameReceived();
			}
		}
	}

	void update()
	{
		QCA_logTextMessage(QStringLiteral("tls[%1]: update").arg(q->objectName()), Logger::Information);

		if(blocked)
		{
			QCA_logTextMessage(QStringLiteral("tls[%1]: ignoring update while blocked").arg(q->objectName()), Logger::Information);
			return;
		}

		if(!actionQueue.isEmpty())
		{
			QCA_logTextMessage(QStringLiteral("tls[%1]: ignoring update while processing actions").arg(q->objectName()), Logger::Information);
			need_update = true;
			return;
		}

		// only allow one operation at a time
		if(op != -1)
		{
			QCA_logTextMessage(QStringLiteral("tls[%1]: ignoring update while operation active").arg(q->objectName()), Logger::Information);
			need_update = true;
			return;
		}

		need_update = false;

		QByteArray arg_from_net, arg_from_app;

		if(state == Handshaking)
		{
			// during handshake, only send from_net (no app data)

			if(mode == TLS::Stream)
			{
				arg_from_net = from_net;
				from_net.clear();
			}
			else
			{
				// note: there may not be a packet
				if(!packet_from_net.isEmpty())
					arg_from_net = packet_from_net.takeFirst();
			}
		}
		else
		{
			if(mode == TLS::Stream)
			{
				if(!from_net.isEmpty())
				{
					arg_from_net = from_net;
					from_net.clear();
				}

				if(!out.isEmpty())
				{
					out_pending += out.size();
					arg_from_app = out;
					out.clear();
				}
			}
			else
			{
				if(!packet_from_net.isEmpty())
					arg_from_net = packet_from_net.takeFirst();

				if(!packet_out.isEmpty())
				{
					arg_from_app = packet_out.takeFirst();
					++packet_out_pending;
				}
			}
		}

		if(arg_from_net.isEmpty() && arg_from_app.isEmpty() && !maybe_input)
		{
			QCA_logTextMessage(QStringLiteral("tls[%1]: ignoring update: no output and no expected input").arg(q->objectName()), Logger::Information);
			return;
		}

		// clear this flag
		maybe_input = false;

		QCA_logTextMessage(QStringLiteral("tls[%1]: c->update").arg(q->objectName()), Logger::Information);
		op = OpUpdate;
		c->update(arg_from_net, arg_from_app);
	}

	void start_finished()
	{
		const bool ok = c->result() == TLSContext::Success;
		if(!ok)
		{
			reset(ResetSession);
			errorCode = TLS::ErrorInit;
			emit q->error();
			return;
		}

		state = Handshaking;

		// immediately update so we can get the first packet to send
		maybe_input = true;
		update();
	}

	void update_finished()
	{
		const TLSContext::Result r = c->result();
		if(r == TLSContext::Error)
		{
			if(state == Handshaking || state == Closing)
			{
				reset(ResetSession);
				errorCode = ErrorHandshake;
			}
			else
			{
				reset(ResetSession);
				errorCode = ErrorCrypt;
			}

			emit q->error();
			return;
		}

		const QByteArray c_to_net = c->to_net();
		if(!c_to_net.isEmpty())
		{
			QCA_logTextMessage(QStringLiteral("tls[%1]: to_net %2").arg(q->objectName(), QString::number(c_to_net.size())), Logger::Information);
		}

		if(state == Closing)
		{
			if(mode == TLS::Stream)
				to_net += c_to_net;
			else
				packet_to_net += c_to_net;

			if(!c_to_net.isEmpty())
				actionQueue += Action(Action::ReadyReadOutgoing);

			if(r == TLSContext::Success)
				actionQueue += Action(Action::Close);

			processNextAction();
			return;
		}
		else if(state == Handshaking)
		{
			if(mode == TLS::Stream)
				to_net += c_to_net;
			else
				packet_to_net += c_to_net;

			if(!c_to_net.isEmpty())
				actionQueue += Action(Action::ReadyReadOutgoing);

			bool clientHello = false;
			bool serverHello = false;
			if(server)
				clientHello = c->clientHelloReceived();
			else
				serverHello = c->serverHelloReceived();

			// client specifies a host?
			if(!emitted_hostNameReceived && clientHello)
			{
				host = c->hostName();
				if(!host.isEmpty())
					actionQueue += Action(Action::HostNameReceived);
			}

			// successful handshake or server hello means there might be a peer cert
			if(!emitted_peerCertificateAvailable && (r == TLSContext::Success || (!server && serverHello)))
				actionQueue += Action(Action::CheckPeerCertificate);

			// server requests a cert from us?
			if(!emitted_certificateRequested && (serverHello && c->certificateRequested()))
				actionQueue += Action(Action::CertificateRequested);

			if(r == TLSContext::Success)
			{
				sessionInfo = c->sessionInfo();
				if(sessionInfo.id)
				{
					TLSSessionContext *sc = static_cast<TLSSessionContext*>(sessionInfo.id->clone());
					session.change(sc);
				}

				actionQueue += Action(Action::Handshaken);
			}

			processNextAction();
			return;
		}
		else // Connected
		{
			const QByteArray c_to_app = c->to_app();
			if(!c_to_app.isEmpty())
			{
				QCA_logTextMessage(QStringLiteral("tls[%1]: to_app %2").arg(q->objectName(), QString::number(c_to_app.size())), Logger::Information);
			}

			const bool eof = c->eof();
			int enc = -1;
			if(!c_to_net.isEmpty())
				enc = c->encoded();

			bool io_pending = false;
			if(mode == TLS::Stream)
			{
				if(!c_to_net.isEmpty())
					out_pending -= enc;

				if(out_pending > 0)
				{
					maybe_input = true;
					io_pending = true;
				}

				if(!out.isEmpty())
					io_pending = true;
			}
			else
			{
				if(!c_to_net.isEmpty())
					--packet_out_pending;

				if(packet_out_pending > 0)
				{
					maybe_input = true;
					io_pending = true;
				}

				if(!packet_out.isEmpty())
					io_pending = true;
			}

			if(mode == TLS::Stream)
			{
				to_net += c_to_net;
				in += c_to_app;
				to_net_encoded += enc;
			}
			else
			{
				packet_to_net += c_to_net;
				packet_in += c_to_app;
			}

			if(!c_to_net.isEmpty())
				actionQueue += Action(Action::ReadyReadOutgoing);

			if(!c_to_app.isEmpty())
				actionQueue += Action(Action::ReadyRead);

			if(eof)
			{
				close();
				maybe_input = true;
			}

			if(eof || io_pending)
			{
				QCA_logTextMessage(QStringLiteral("tls[%1]: eof || io_pending").arg(q->objectName()), Logger::Information);
				update();
			}

			processNextAction();
			return;
		}
	}

private Q_SLOTS:
	void tls_resultsReady()
	{
		QCA_logTextMessage(QStringLiteral("tls[%1]: c->resultsReady()").arg(q->objectName()), Logger::Information);

		Q_ASSERT(op != -1);

		int last_op = op;
		op = -1;

		if(last_op == OpStart)
			start_finished();
		else // OpUpdate
			update_finished();
	}

	void tls_dtlsTimeout()
	{
		QCA_logTextMessage(QStringLiteral("tls[%1]: c->dtlsTimeout()").arg(q->objectName()), Logger::Information);

		maybe_input = true;
		update();
	}

	void doNextAction()
	{
		processNextAction();
	}
};

TLS::TLS(QObject *parent, const QString &provider)
:SecureLayer(parent), Algorithm(QStringLiteral("tls"), provider)
{
	d = new Private(this, TLS::Stream);
}

TLS::TLS(Mode mode, QObject *parent, const QString &provider)
:SecureLayer(parent), Algorithm(mode == Stream ? QStringLiteral("tls") : QStringLiteral("dtls"), provider)
{
	d = new Private(this, mode);
}

TLS::~TLS()
{
	delete d;
}

void TLS::reset()
{
	d->reset(ResetAll);
}

QStringList TLS::supportedCipherSuites(const Version &version) const // clazy:exclude=function-args-by-value TODO make it remove the & when we break ABI
{
	return d->c->supportedCipherSuites(version);
}

void TLS::setCertificate(const CertificateChain &cert, const PrivateKey &key)
{
	d->localCert = cert;
	d->localKey = key;
	if(d->state != TLS::Private::Inactive)
		d->c->setCertificate(cert, key);
}

void TLS::setCertificate(const KeyBundle &kb)
{
	setCertificate(kb.certificateChain(), kb.privateKey());
}

CertificateCollection TLS::trustedCertificates() const
{
	return d->trusted;
}

void TLS::setTrustedCertificates(const CertificateCollection &trusted)
{
	d->trusted = trusted;
	if(d->state != TLS::Private::Inactive)
		d->c->setTrustedCertificates(trusted);
}

void TLS::setConstraints(SecurityLevel s)
{
	int min = 128;
	switch(s)
	{
		case SL_None:
			min = 0;
			break;
		case SL_Integrity:
			min = 1;
			break;
		case SL_Export:
			min = 40;
			break;
		case SL_Baseline:
			min = 128;
			break;
		case SL_High:
			min = 129;
			break;
		case SL_Highest:
			min = qMax(129, d->c->maxSSF());
			break;
	}

	d->con_ssfMode = true;
	d->con_minSSF = min;
	d->con_maxSSF = -1;

	if(d->state != TLS::Private::Inactive)
		d->c->setConstraints(d->con_minSSF, d->con_maxSSF);
}

void TLS::setConstraints(int minSSF, int maxSSF)
{
	d->con_ssfMode = true;
	d->con_minSSF = minSSF;
	d->con_maxSSF = maxSSF;

	if(d->state != TLS::Private::Inactive)
		d->c->setConstraints(d->con_minSSF, d->con_maxSSF);
}

void TLS::setConstraints(const QStringList &cipherSuiteList)
{
	d->con_ssfMode = false;
	d->con_cipherSuites = cipherSuiteList;

	if(d->state != TLS::Private::Inactive)
		d->c->setConstraints(d->con_cipherSuites);
}

QList<CertificateInfoOrdered> TLS::issuerList() const
{
	return d->issuerList;
}

void TLS::setIssuerList(const QList<CertificateInfoOrdered> &issuers)
{
	d->issuerList = issuers;
	if(d->state != TLS::Private::Inactive)
		d->c->setIssuerList(issuers);
}

void TLS::setSession(const TLSSession &session)
{
	d->session = session;
}

bool TLS::canCompress() const
{
	return d->c->canCompress();
}

bool TLS::canSetHostName() const
{
	return d->c->canSetHostName();
}

bool TLS::compressionEnabled() const
{
	return d->tryCompress;
}

void TLS::setCompressionEnabled(bool b)
{
	d->tryCompress = b;
}

void TLS::startClient(const QString &host)
{
	d->reset(ResetSessionAndData);
	d->host = host;
	d->issuerList.clear();

	// client mode
	d->start(false);
}

void TLS::startServer()
{
	d->reset(ResetSessionAndData);

	// server mode
	d->start(true);
}

void TLS::continueAfterStep()
{
	d->continueAfterStep();
}

bool TLS::isHandshaken() const
{
	if(d->state == TLS::Private::Connected || d->state == TLS::Private::Closing)
		return true;
	else
		return false;
}

bool TLS::isCompressed() const
{
	return d->sessionInfo.isCompressed;
}

TLS::Version TLS::version() const
{
	return d->sessionInfo.version;
}

QString TLS::cipherSuite() const
{
	return d->sessionInfo.cipherSuite;
}

int TLS::cipherBits() const
{
	return d->sessionInfo.cipherBits;
}

int TLS::cipherMaxBits() const
{
	return d->sessionInfo.cipherMaxBits;
}

TLSSession TLS::session() const
{
	return d->session;
}

TLS::Error TLS::errorCode() const
{
	return d->errorCode;
}

TLS::IdentityResult TLS::peerIdentityResult() const
{
	if(d->peerCert.isEmpty())
		return NoCertificate;

	if(d->peerValidity != ValidityGood)
		return InvalidCertificate;

	if(d->hostMismatch)
		return HostMismatch;

	return Valid;
}

Validity TLS::peerCertificateValidity() const
{
	return d->peerValidity;
}

CertificateChain TLS::localCertificateChain() const
{
	return d->localCert;
}

PrivateKey TLS::localPrivateKey() const
{
	return d->localKey;
}

CertificateChain TLS::peerCertificateChain() const
{
	return d->peerCert;
}

bool TLS::isClosable() const
{
	return true;
}

int TLS::bytesAvailable() const
{
	if(d->mode == Stream)
		return d->in.size();
	else
		return 0;
}

int TLS::bytesOutgoingAvailable() const
{
	if(d->mode == Stream)
		return d->to_net.size();
	else
		return 0;
}

void TLS::close()
{
	d->close();
	d->update();
}

void TLS::write(const QByteArray &a)
{
	if(d->mode == Stream)
	{
		d->out.append(a);
		d->layer.addPlain(a.size());
	}
	else
		d->packet_out.append(a);
	QCA_logTextMessage(QStringLiteral("tls[%1]: write").arg(objectName()), Logger::Information);
	d->update();
}

QByteArray TLS::read()
{
	if(d->mode == Stream)
	{
		const QByteArray a = d->in;
		d->in.clear();
		return a;
	}
	else
	{
		if(!d->packet_in.isEmpty())
			return d->packet_in.takeFirst();
		else
			return QByteArray();
	}
}

void TLS::writeIncoming(const QByteArray &a)
{
	if(d->mode == Stream)
		d->from_net.append(a);
	else
		d->packet_from_net.append(a);
	QCA_logTextMessage(QStringLiteral("tls[%1]: writeIncoming %2").arg(objectName(), QString::number(a.size())), Logger::Information);
	d->update();
}

QByteArray TLS::readOutgoing(int *plainBytes)
{
	if(d->mode == Stream)
	{
		const QByteArray a = d->to_net;
		d->to_net.clear();
		if(plainBytes)
			*plainBytes = d->to_net_encoded;
		d->layer.specifyEncoded(a.size(), d->to_net_encoded);
		d->to_net_encoded = 0;
		return a;
	}
	else
	{
		if(!d->packet_to_net.isEmpty())
		{
			const QByteArray a = d->packet_to_net.takeFirst();
			const int x = d->packet_to_net_encoded.takeFirst();
			if(plainBytes)
				*plainBytes = x;
			return a;
		}
		else
		{
			if(plainBytes)
				*plainBytes = 0;
			return QByteArray();
		}
	}
}

QByteArray TLS::readUnprocessed()
{
	if(d->mode == Stream)
	{
		const QByteArray a = d->unprocessed;
		d->unprocessed.clear();
		return a;
	}
	else
		return QByteArray();
}

int TLS::convertBytesWritten(qint64 bytes)
{
	return d->layer.finished(bytes);
}

int TLS::packetsAvailable() const
{
	return d->packet_in.count();
}

int TLS::packetsOutgoingAvailable() const
{
	return d->packet_to_net.count();
}

int TLS::packetMTU() const
{
	return d->packet_mtu;
}

void TLS::setPacketMTU(int size) const
{
	d->packet_mtu = size;
	if(d->state != TLS::Private::Inactive)
		d->c->setMTU(size);
}

void TLS::connectNotify(const QMetaMethod &signal)
{
	if(signal == QMetaMethod::fromSignal(&TLS::hostNameReceived))
		d->connect_hostNameReceived = true;
	else if(signal == QMetaMethod::fromSignal(&TLS::certificateRequested))
		d->connect_certificateRequested = true;
	else if(signal == QMetaMethod::fromSignal(&TLS::peerCertificateAvailable))
		d->connect_peerCertificateAvailable = true;
	else if(signal == QMetaMethod::fromSignal(&TLS::handshaken))
		d->connect_handshaken = true;
}

void TLS::disconnectNotify(const QMetaMethod &signal)
{
	if(signal == QMetaMethod::fromSignal(&TLS::hostNameReceived))
		d->connect_hostNameReceived = false;
	else if(signal == QMetaMethod::fromSignal(&TLS::certificateRequested))
		d->connect_certificateRequested = false;
	else if(signal == QMetaMethod::fromSignal(&TLS::peerCertificateAvailable))
		d->connect_peerCertificateAvailable = false;
	else if(signal == QMetaMethod::fromSignal(&TLS::handshaken))
		d->connect_handshaken = false;
}

//----------------------------------------------------------------------------
// SASL::Params
//----------------------------------------------------------------------------
class SASL::Params::Private
{
public:
	bool needUsername, canSendAuthzid, needPassword, canSendRealm;
};

SASL::Params::Params()
:d(new Private)
{
}

SASL::Params::Params(bool user, bool authzid, bool pass, bool realm)
:d(new Private)
{
	d->needUsername = user;
	d->canSendAuthzid = authzid;
	d->needPassword = pass;
	d->canSendRealm = realm;
}

SASL::Params::Params(const SASL::Params &from)
:d(new Private(*from.d))
{
}

SASL::Params::~Params()
{
	delete d;
}

SASL::Params & SASL::Params::operator=(const SASL::Params &from)
{
	*d = *from.d;
	return *this;
}

bool SASL::Params::needUsername() const
{
	return d->needUsername;
}

bool SASL::Params::canSendAuthzid() const
{
	return d->canSendAuthzid;
}

bool SASL::Params::needPassword() const
{
	return d->needPassword;
}

bool SASL::Params::canSendRealm() const
{
	return d->canSendRealm;
}

//----------------------------------------------------------------------------
// SASL
//----------------------------------------------------------------------------
/*
  These don't map, but I don't think it matters much..
    SASL_TRYAGAIN  (-8)  transient failure (e.g., weak key)
    SASL_BADMAC    (-9)  integrity check failed
      -- client only codes --
    SASL_WRONGMECH (-11) mechanism doesn't support requested feature
    SASL_NEWSECRET (-12) new secret needed
      -- server only codes --
    SASL_TRANS     (-17) One time use of a plaintext password will
                         enable requested mechanism for user
    SASL_PWLOCK    (-21) password locked
    SASL_NOCHANGE  (-22) requested change was not needed
*/

class SASL::Private : public QObject
{
	Q_OBJECT
public:
	enum
	{
		OpStart,
		OpServerFirstStep,
		OpNextStep,
		OpTryAgain,
		OpUpdate
	};

	class Action
	{
	public:
		enum Type
		{
			ClientStarted,
			NextStep,
			Authenticated,
			ReadyRead,
			ReadyReadOutgoing
		};

		int type;
		QByteArray stepData;
		bool haveInit;

		Action(int _type) : type(_type)
		{
		}

		Action(int _type, const QByteArray &_stepData) : type(_type), stepData(_stepData)
		{
		}

		Action(int _type, bool _haveInit, const QByteArray &_stepData) : type(_type), stepData(_stepData), haveInit(_haveInit)
		{
		}
	};

	SASL *q;
	SASLContext *c;

	// persistent settings (survives ResetSessionAndData)
	AuthFlags auth_flags;
	int ssfmin, ssfmax;
	QString ext_authid;
	int ext_ssf;
	bool localSet, remoteSet;
	SASLContext::HostPort local, remote;
	bool set_username, set_authzid, set_password, set_realm;
	QString username, authzid, realm;
	SecureArray password;

	// session
	bool server;
	QStringList mechlist;
	QString server_realm;
	bool allowClientSendFirst;
	bool disableServerSendLast;
	SafeTimer actionTrigger;
	int op;
	QList<Action> actionQueue;
	bool need_update;
	bool first;
	bool authed;

	// data (survives ResetSession)
	QString mech; // selected mech
	Error errorCode;

	// stream i/o
	QByteArray in, out;
	QByteArray to_net, from_net;
	int out_pending;
	int to_net_encoded;
	LayerTracker layer;

	Private(SASL *_q) : QObject(_q), q(_q), actionTrigger(this)
	{
		c = nullptr;
		set_username = false;
		set_authzid = false;
		set_password = false;
		set_realm = false;

        connect(&actionTrigger, &SafeTimer::timeout, this, &Private::doNextAction);
		actionTrigger.setSingleShot(true);

		reset(ResetAll);

		c = static_cast<SASLContext *>(q->context());

		// parent the context to us, so that moveToThread works
		c->setParent(this);

		connect(c, &SASLContext::resultsReady, this, &Private::sasl_resultsReady);
	}

	~Private() override
	{
		// context is owned by Algorithm, unparent so we don't double-delete
		c->setParent(nullptr);
	}

	void reset(ResetMode mode)
	{
		if(c)
			c->reset();

		server = false;
		mechlist.clear();
		server_realm = QString();
		allowClientSendFirst = false;
		disableServerSendLast = true;
		actionTrigger.stop();
		op = -1;
		actionQueue.clear();
		need_update = false;
		first = false;
		authed = false;

		out.clear();
		out_pending = 0;

		if(mode >= ResetSessionAndData)
		{
			mech = QString();
			errorCode = (SASL::Error)-1;

			in.clear();
			to_net.clear();
			from_net.clear();
			to_net_encoded = 0;
			layer.reset();
		}

		if(mode >= ResetAll)
		{
			auth_flags = SASL::AuthFlagsNone;
			ssfmin = 0;
			ssfmax = 0;
			ext_authid = QString();
			ext_ssf = 0;
			localSet = false;
			remoteSet = false;
			local = SASLContext::HostPort();
			remote = SASLContext::HostPort();

			set_username = false;
			username = QString();
			set_authzid = false;
			authzid = QString();
			set_password = false;
			password = SecureArray();
			set_realm = false;
			realm = QString();
		}
	}

	void setup(const QString &service, const QString &host)
	{
		c->setup(service, host, localSet ? &local : nullptr, remoteSet ? &remote : nullptr, ext_authid, ext_ssf);
		c->setConstraints(auth_flags, ssfmin, ssfmax);

		QString *p_username = nullptr;
		QString *p_authzid = nullptr;
		SecureArray *p_password = nullptr;
		QString *p_realm = nullptr;

		if(set_username)
			p_username = &username;
		if(set_authzid)
			p_authzid = &authzid;
		if(set_password)
			p_password = &password;
		if(set_realm)
			p_realm = &realm;

		c->setClientParams(p_username, p_authzid, p_password, p_realm);
	}

	void start()
	{
		op = OpStart;
		first = true;

		if(server)
		{
			QCA_logTextMessage(QStringLiteral("sasl[%1]: c->startServer()").arg(q->objectName()), Logger::Information);
			c->startServer(server_realm, disableServerSendLast);
		}
		else
		{
			QCA_logTextMessage(QStringLiteral("sasl[%1]: c->startClient()").arg(q->objectName()), Logger::Information);
			c->startClient(mechlist, allowClientSendFirst);
		}
	}

	void putServerFirstStep(const QString &mech, const QByteArray *clientInit)
	{
		if(op != -1)
			return;

		QCA_logTextMessage(QStringLiteral("sasl[%1]: c->serverFirstStep()").arg(q->objectName()), Logger::Information);
		op = OpServerFirstStep;
		c->serverFirstStep(mech, clientInit);
	}

	void putStep(const QByteArray &stepData)
	{
		if(op != -1)
			return;

		QCA_logTextMessage(QStringLiteral("sasl[%1]: c->nextStep()").arg(q->objectName()), Logger::Information);
		op = OpNextStep;
		c->nextStep(stepData);
	}

	void tryAgain()
	{
		if(op != -1)
			return;

		QCA_logTextMessage(QStringLiteral("sasl[%1]: c->tryAgain()").arg(q->objectName()), Logger::Information);
		op = OpTryAgain;
		c->tryAgain();
	}

	void processNextAction()
	{
		if(actionQueue.isEmpty())
		{
			if(need_update)
				update();
			return;
		}

		const Action a = actionQueue.takeFirst();

		// set up for the next one, if necessary
		if(!actionQueue.isEmpty() || need_update)
		{
			if(!actionTrigger.isActive())
				actionTrigger.start();
		}

		if(a.type == Action::ClientStarted)
		{
			emit q->clientStarted(a.haveInit, a.stepData);
		}
		else if(a.type == Action::NextStep)
		{
			emit q->nextStep(a.stepData);
		}
		else if(a.type == Action::Authenticated)
		{
			authed = true;

			// write any app data waiting during authentication
			if(!out.isEmpty())
			{
				need_update = true;
				if(!actionTrigger.isActive())
					actionTrigger.start();
			}

			QCA_logTextMessage(QStringLiteral("sasl[%1]: authenticated").arg(q->objectName()), Logger::Information);
			emit q->authenticated();
		}
		else if(a.type == Action::ReadyRead)
		{
			emit q->readyRead();
		}
		else if(a.type == Action::ReadyReadOutgoing)
		{
			emit q->readyReadOutgoing();
		}
	}

	void update()
	{
		// defer writes while authenticating
		if(!authed)
		{
			QCA_logTextMessage(QStringLiteral("sasl[%1]: ignoring update while not yet authenticated").arg(q->objectName()), Logger::Information);
			return;
		}

		if(!actionQueue.isEmpty())
		{
			QCA_logTextMessage(QStringLiteral("sasl[%1]: ignoring update while processing actions").arg(q->objectName()), Logger::Information);
			need_update = true;
			return;
		}

		// only allow one operation at a time
		if(op != -1)
		{
			QCA_logTextMessage(QStringLiteral("sasl[%1]: ignoring update while operation active").arg(q->objectName()), Logger::Information);
			need_update = true;
			return;
		}

		need_update = false;

		QCA_logTextMessage(QStringLiteral("sasl[%1]: c->update()").arg(q->objectName()), Logger::Information);
		op = OpUpdate;
		out_pending += out.size();
		c->update(from_net, out);
		from_net.clear();
		out.clear();
	}

private Q_SLOTS:
	void sasl_resultsReady()
	{
		QCA_logTextMessage(QStringLiteral("sasl[%1]: c->resultsReady()").arg(q->objectName()), Logger::Information);

		int last_op = op;
		op = -1;

		const SASLContext::Result r = c->result();

		if(last_op == OpStart)
		{
			if(server)
			{
				if(r != SASLContext::Success)
				{
					errorCode = SASL::ErrorInit;
					emit q->error();
					return;
				}

				emit q->serverStarted();
				return;
			}
			else // client
			{
				mech = c->mech();

				// fall into this logic
				last_op = OpTryAgain;
			}
		}
		else if(last_op == OpServerFirstStep)
		{
			// fall into this logic
			last_op = OpTryAgain;
		}
		else if(last_op == OpNextStep)
		{
			// fall into this logic
			last_op = OpTryAgain;
		}

		if(last_op == OpTryAgain)
		{
			if(server)
			{
				if(r == SASLContext::Continue)
				{
					emit q->nextStep(c->stepData());
					return;
				}
				else if(r == SASLContext::AuthCheck)
				{
					emit q->authCheck(c->username(), c->authzid());
					return;
				}
				else if(r == SASLContext::Success)
				{
					if(!disableServerSendLast)
						actionQueue += Action(Action::NextStep, c->stepData());

					actionQueue += Action(Action::Authenticated);

					processNextAction();
					return;
				}
				else // error
				{
					errorCode = SASL::ErrorHandshake;
					emit q->error();
					return;
				}
			}
			else // client
			{
				if(first)
				{
					if(r == SASLContext::Error)
					{
						if(first)
							errorCode = SASL::ErrorInit;
						else
							errorCode = SASL::ErrorHandshake;
						emit q->error();
						return;
					}
					else if(r == SASLContext::Params)
					{
						const Params np = c->clientParams();
						emit q->needParams(np);
						return;
					}

					first = false;
					actionQueue += Action(Action::ClientStarted, c->haveClientInit(), c->stepData());
					if(r == SASLContext::Success)
						actionQueue += Action(Action::Authenticated);

					processNextAction();
					return;
				}
				else
				{
					if(r == SASLContext::Error)
					{
						errorCode = ErrorHandshake;
						emit q->error();
						return;
					}
					else if(r == SASLContext::Params)
					{
						const Params np = c->clientParams();
						emit q->needParams(np);
						return;
					}
					else if(r == SASLContext::Continue)
					{
						emit q->nextStep(c->stepData());
						return;
					}
					else if(r == SASLContext::Success)
					{
						actionQueue += Action(Action::NextStep, c->stepData());
						actionQueue += Action(Action::Authenticated);

						processNextAction();
						return;
					}
				}
			}
		}
		else if(last_op == OpUpdate)
		{
			if(r != SASLContext::Success)
			{
				errorCode = ErrorCrypt;
				emit q->error();
				return;
			}

			const QByteArray c_to_net = c->to_net();
			const QByteArray c_to_app = c->to_app();
			int enc = -1;
			if(!c_to_net.isEmpty())
				enc = c->encoded();

			bool io_pending = false;
			if(!c_to_net.isEmpty())
				out_pending -= enc;

			if(out_pending > 0)
				io_pending = true;

			if(!out.isEmpty())
				io_pending = true;

			to_net += c_to_net;
			in += c_to_app;
			to_net_encoded += enc;

			if(!c_to_net.isEmpty())
				actionQueue += Action(Action::ReadyReadOutgoing);

			if(!c_to_app.isEmpty())
				actionQueue += Action(Action::ReadyRead);

			if(io_pending)
				update();

			processNextAction();
			return;
		}
	}

	void doNextAction()
	{
		processNextAction();
	}
};

SASL::SASL(QObject *parent, const QString &provider)
:SecureLayer(parent), Algorithm(QStringLiteral("sasl"), provider)
{
	d = new Private(this);
}

SASL::~SASL()
{
	delete d;
}

void SASL::reset()
{
	d->reset(ResetAll);
}

SASL::Error SASL::errorCode() const
{
	return d->errorCode;
}

SASL::AuthCondition SASL::authCondition() const
{
	return d->c->authCondition();
}

void SASL::setConstraints(AuthFlags f, SecurityLevel s)
{
	int min = 0;
	if(s == SL_Integrity)
		min = 1;
	else if(s == SL_Export)
		min = 56;
	else if(s == SL_Baseline)
		min = 128;
	else if(s == SL_High)
		min = 192;
	else if(s == SL_Highest)
		min = 256;

	setConstraints(f, min, 256);
}

void SASL::setConstraints(AuthFlags f, int minSSF, int maxSSF)
{
	d->auth_flags = f;

	d->ssfmin = minSSF;
	d->ssfmax = maxSSF;
}

void SASL::setExternalAuthId(const QString &authid)
{
	d->ext_authid = authid;
}

void SASL::setExternalSSF(int strength)
{
	d->ext_ssf = strength;
}

void SASL::setLocalAddress(const QString &addr, quint16 port)
{
	d->localSet = true;
	d->local.addr = addr;
	d->local.port = port;
}

void SASL::setRemoteAddress(const QString &addr, quint16 port)
{
	d->remoteSet = true;
	d->remote.addr = addr;
	d->remote.port = port;
}

void SASL::startClient(const QString &service, const QString &host, const QStringList &mechlist, ClientSendMode mode)
{
	d->reset(ResetSessionAndData);
	d->setup(service, host);
	d->server = false;
	d->mechlist = mechlist;
	d->allowClientSendFirst = (mode == AllowClientSendFirst);
	d->start();
}

void SASL::startServer(const QString &service, const QString &host, const QString &realm, ServerSendMode mode)
{
	d->reset(ResetSessionAndData);
	d->setup(service, host);
	d->server = true;
	d->server_realm = realm;
	d->disableServerSendLast = (mode == DisableServerSendLast);
	d->start();
}

void SASL::putServerFirstStep(const QString &mech)
{
	d->putServerFirstStep(mech, nullptr);
}

void SASL::putServerFirstStep(const QString &mech, const QByteArray &clientInit)
{
	d->putServerFirstStep(mech, &clientInit);
}

void SASL::putStep(const QByteArray &stepData)
{
	d->putStep(stepData);
}

void SASL::setUsername(const QString &user)
{
	d->set_username = true;
	d->username = user;
	d->c->setClientParams(&user, nullptr, nullptr, nullptr);
}

void SASL::setAuthzid(const QString &authzid)
{
	d->set_authzid = true;
	d->authzid = authzid;
	d->c->setClientParams(nullptr, &authzid, nullptr, nullptr);
}

void SASL::setPassword(const SecureArray &pass)
{
	d->set_password = true;
	d->password = pass;
	d->c->setClientParams(nullptr, nullptr, &pass, nullptr);
}

void SASL::setRealm(const QString &realm)
{
	d->set_realm = true;
	d->realm = realm;
	d->c->setClientParams(nullptr, nullptr, nullptr, &realm);
}

void SASL::continueAfterParams()
{
	d->tryAgain();
}

void SASL::continueAfterAuthCheck()
{
	d->tryAgain();
}

QString SASL::mechanism() const
{
	return d->mech;
}

QStringList SASL::mechanismList() const
{
	return d->c->mechlist();
}

QStringList SASL::realmList() const
{
	return d->c->realmlist();
}

int SASL::ssf() const
{
	return d->c->ssf();
}

int SASL::bytesAvailable() const
{
	return d->in.size();
}

int SASL::bytesOutgoingAvailable() const
{
	return d->to_net.size();
}

void SASL::write(const QByteArray &a)
{
	d->out.append(a);
	d->layer.addPlain(a.size());
	d->update();
}

QByteArray SASL::read()
{
	const QByteArray a = d->in;
	d->in.clear();
	return a;
}

void SASL::writeIncoming(const QByteArray &a)
{
	d->from_net.append(a);
	d->update();
}

QByteArray SASL::readOutgoing(int *plainBytes)
{
	const QByteArray a = d->to_net;
	d->to_net.clear();
	if(plainBytes)
		*plainBytes = d->to_net_encoded;
	d->layer.specifyEncoded(a.size(), d->to_net_encoded);
	d->to_net_encoded = 0;
	return a;
}

int SASL::convertBytesWritten(qint64 bytes)
{
	return d->layer.finished(bytes);
}

}

#include "qca_securelayer.moc"
