/*
    ksslsocket.cpp - KDE SSL Socket

    Copyright (c) 2005      by Tommi Rantala <tommi.rantala@cs.helsinki.fi>
    Copyright (c) 2004      by Jason Keirstead <jason@keirstead.org>

    Kopete    (c) 2002-2005 by the Kopete developers <kopete-devel@kde.org>

    *************************************************************************
    *                                                                       *
    * 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 2 of the License, or     *
    * (at your option) any later version.                                   *
    *                                                                       *
    *************************************************************************
*/

#include <tqsocketnotifier.h>

#include <dcopclient.h>
#include <tdelocale.h>
#include <kdebug.h>
#include <kssl.h>
#include <ksslinfodlg.h>
#include <ksslpeerinfo.h>
#include <ksslcertchain.h>
#include <ksslcertificatecache.h>
#include <tdeapplication.h>
#include <tdemessagebox.h>

#include "ksslsocket.h"

struct KSSLSocketPrivate
{
	mutable KSSL *kssl;
	KSSLCertificateCache *cc;
	DCOPClient *dcc;
	TQMap<TQString,TQString> metaData;
	TQSocketNotifier *socketNotifier;
};

KSSLSocket::KSSLSocket() : KExtendedSocket()
{
	d = new KSSLSocketPrivate;
	d->kssl = 0;
	d->dcc = TDEApplication::kApplication()->dcopClient();
	d->cc = new KSSLCertificateCache;
	d->cc->reload();

	//No blocking
	setBlockingMode(false);

	//Connect internal slots
	TQObject::connect( this, TQ_SIGNAL(connectionSuccess()), this, TQ_SLOT(slotConnected()) );
	TQObject::connect( this, TQ_SIGNAL(closed(int)), this, TQ_SLOT(slotDisconnected()) );
	TQObject::connect( this, TQ_SIGNAL(connectionFailed(int)), this, TQ_SLOT(slotDisconnected()));
}

KSSLSocket::~KSSLSocket()
{
	//Close connection
	closeNow();

	if( d->kssl )
	{
		d->kssl->close();
		delete d->kssl;
	}

	delete d->cc;

	delete d;
}

TQ_LONG KSSLSocket::readBlock( char* data, TQ_ULONG maxLen )
{
	//Re-implemented because KExtSocket doesn't use this when not in buffered mode
	TQ_LONG retval = consumeReadBuffer(maxLen, data);

	if( retval == 0 )
	{
		if (sockfd == -1)
			return 0;

		retval = -1;
	}

	return retval;
}

int KSSLSocket::peekBlock( char* data, uint maxLen )
{
	//Re-implemented because KExtSocket doesn't use this when not in buffered mode
	if( socketStatus() < connected )
		return -2;

	if( sockfd == -1 )
		return -2;

	return consumeReadBuffer(maxLen, data, false);
}

TQ_LONG KSSLSocket::writeBlock( const char* data, TQ_ULONG len )
{
	return d->kssl ? (d->kssl->write(data, len)) : -1;
}

int KSSLSocket::bytesAvailable() const
{
	if( socketStatus() < connected )
		return -2;

	//Re-implemented because KExtSocket doesn't use this when not in buffered mode
	return TDEBufferedIO::bytesAvailable();
}

void KSSLSocket::slotReadData()
{
	kdDebug(14120) << k_funcinfo << d->kssl->pending() << endl;
	TQByteArray buff(512);
	int bytesRead = d->kssl->read( buff.data(), 512 );

	if( bytesRead < 0 )
	{
		kdError(14120) << k_funcinfo << "SSL read() error!" << endl;

		closeNow();
		emit sslFailure();
		return;
	}

	//Fill the read buffer
	feedReadBuffer( bytesRead, buff.data() );
	emit readyRead();
}

void KSSLSocket::slotConnected()
{
	if (!KSSL::doesSSLWork()) {
		kdError(14120) << k_funcinfo << "SSL not functional!" << endl;

		closeNow();
		emit sslFailure();
		return;
	}

	delete d->kssl;
	d->kssl = new KSSL();

	if (d->kssl->connect( sockfd ) != 1) {
		kdError(14120) << k_funcinfo << "SSL connect() failed." << endl;

		closeNow();
		emit sslFailure();
		return;
	}

	//Disconnect the KExtSocket notifier slot, we use our own
	TQObject::disconnect( readNotifier(), TQ_SIGNAL(activated( int )),
			this, TQ_SLOT(socketActivityRead()) );

	TQObject::connect( readNotifier(), TQ_SIGNAL(activated( int )),
			this, TQ_SLOT(slotReadData()) );

	readNotifier()->setEnabled(true);

	if (verifyCertificate() != 1) {
		closeNow();
		emit certificateRejected();
		return;
	}

	emit certificateAccepted();
}

void KSSLSocket::slotDisconnected()
{
	kdDebug(14120) << k_funcinfo << "Disconnected" << endl;

	if( readNotifier() )
		readNotifier()->setEnabled(false);

	delete d->kssl;
	d->kssl = 0L;
}

void KSSLSocket::showInfoDialog()
{
	if( socketStatus() == connected )
	{
		if (!d->dcc->isApplicationRegistered("tdeio_uiserver"))
		{
			TDEApplication::startServiceByDesktopPath("tdeio_uiserver.desktop",TQStringList());
		}

		TQByteArray data, ignore;
		TQCString ignoretype;
		TQDataStream arg(data, IO_WriteOnly);
		arg << "irc://" + peerAddress()->pretty() + ":" + port() << d->metaData;
		d->dcc->call("tdeio_uiserver", "UIServer",
			"showSSLInfoDialog(TQString,TDEIO::MetaData)", data, ignoretype, ignore);
	}
}

void KSSLSocket::setMetaData( const TQString &key, const TQVariant &data )
{
	TQVariant v = data;
	d->metaData[key] = v.asString();
}

bool KSSLSocket::hasMetaData( const TQString &key )
{
	return d->metaData.contains(key);
}

TQString KSSLSocket::metaData( const TQString &key )
{
	if( d->metaData.contains(key) )
		return d->metaData[key];
	return TQString();
}

/*
I basically copied the below from tcpTDEIO::SlaveBase.hpp, with some modificaions and formatting.

 * Copyright (C) 2000 Alex Zepeda <zipzippy@sonic.net
 * Copyright (C) 2001-2003 George Staikos <staikos@kde.org>
 * Copyright (C) 2001 Dawit Alemayehu <adawit@kde.org>
*/

int KSSLSocket::messageBox( TDEIO::SlaveBase::MessageBoxType type, const TQString &text, const TQString &caption,
	const TQString &buttonYes, const TQString &buttonNo )
{
	kdDebug(14120) << "messageBox " << type << " " << text << " - " << caption << buttonYes << buttonNo << endl;
	TQByteArray data, result;
	TQCString returnType;
	TQDataStream arg(data, IO_WriteOnly);
	arg << (int)1 << (int)type << text << caption << buttonYes << buttonNo;

	if (!d->dcc->isApplicationRegistered("tdeio_uiserver"))
	{
		TDEApplication::startServiceByDesktopPath("tdeio_uiserver.desktop",TQStringList());
	}

	d->dcc->call("tdeio_uiserver", "UIServer",
			"messageBox(int,int,TQString,TQString,TQString,TQString)", data, returnType, result);

	if( returnType == "int" )
	{
		int res;
		TQDataStream r(result, IO_ReadOnly);
		r >> res;
		return res;
	}
	else
		return 0; // communication failure
}


//  Returns 0 for failed verification, -1 for rejected cert and 1 for ok
int KSSLSocket::verifyCertificate()
{
	int rc = 0;
	bool permacache = false;
	bool _IPmatchesCN = false;
	int result;
	bool doAddHost = false;
	TQString ourHost = host();
	TQString ourIp = peerAddress()->pretty();

	TQString theurl = "irc://" + ourHost + ":" + port();

	if (!d->cc)
		d->cc = new KSSLCertificateCache;

	KSSLCertificate& pc = d->kssl->peerInfo().getPeerCertificate();

	KSSLCertificate::KSSLValidationList ksvl = pc.validateVerbose(KSSLCertificate::SSLServer);

	_IPmatchesCN = d->kssl->peerInfo().certMatchesAddress();

	if (!_IPmatchesCN)
	{
		ksvl << KSSLCertificate::InvalidHost;
	}

	KSSLCertificate::KSSLValidation ksv = KSSLCertificate::Ok;
	if (!ksvl.isEmpty())
		ksv = ksvl.first();

	/* Setting the various bits of meta-info that will be needed. */
	setMetaData("ssl_cipher", d->kssl->connectionInfo().getCipher());
	setMetaData("ssl_cipher_desc", d->kssl->connectionInfo().getCipherDescription());
	setMetaData("ssl_cipher_version", d->kssl->connectionInfo().getCipherVersion());
	setMetaData("ssl_cipher_used_bits", TQString::number(d->kssl->connectionInfo().getCipherUsedBits()));
	setMetaData("ssl_cipher_bits", TQString::number(d->kssl->connectionInfo().getCipherBits()));
	setMetaData("ssl_peer_ip", ourIp );

	TQString errorStr;
	for(KSSLCertificate::KSSLValidationList::ConstIterator it = ksvl.begin();
		it != ksvl.end(); ++it)
	{
		errorStr += TQString::number(*it)+":";
	}

	setMetaData("ssl_cert_errors", errorStr);
	setMetaData("ssl_peer_certificate", pc.toString());

	if (pc.chain().isValid() && pc.chain().depth() > 1)
	{
		TQString theChain;
		TQPtrList<KSSLCertificate> chain = pc.chain().getChain();
		for (KSSLCertificate *c = chain.first(); c; c = chain.next())
		{
			theChain += c->toString();
			theChain += "\n";
		}
		setMetaData("ssl_peer_chain", theChain);
	}
	else
	{
		setMetaData("ssl_peer_chain", "");
	}

	setMetaData("ssl_cert_state", TQString::number(ksv));

	if (ksv == KSSLCertificate::Ok)
	{
		rc = 1;
		setMetaData("ssl_action", "accept");
	}

	// Since we're the parent, we need to teach the child.
	setMetaData("ssl_parent_ip", ourIp );
	setMetaData("ssl_parent_cert", pc.toString());

	//  - Read from cache and see if there is a policy for this
	KSSLCertificateCache::KSSLCertificatePolicy cp = d->cc->getPolicyByCertificate(pc);

	//  - validation code
	if (ksv != KSSLCertificate::Ok)
	{
		if( cp == KSSLCertificateCache::Unknown || cp == KSSLCertificateCache::Ambiguous)
		{
			cp = KSSLCertificateCache::Prompt;
		}
		else
		{
			// A policy was already set so let's honor that.
			permacache = d->cc->isPermanent(pc);
		}

		if (!_IPmatchesCN && cp == KSSLCertificateCache::Accept)
		{
			cp = KSSLCertificateCache::Prompt;
		}

		// Precondition: cp is one of Reject, Accept or Prompt
		switch (cp)
		{
			case KSSLCertificateCache::Accept:
				rc = 1;
				break;

			case KSSLCertificateCache::Reject:
				rc = -1;
				break;

			case KSSLCertificateCache::Prompt:
			{
				do
				{
					if (ksv == KSSLCertificate::InvalidHost)
					{
						TQString msg = i18n("The IP address of the host %1 "
								"does not match the one the "
								"certificate was issued to.");
						result = messageBox( TDEIO::SlaveBase::WarningYesNoCancel,
						msg.arg(ourHost),
						i18n("Server Authentication"),
						i18n("&Details"),
						i18n("Co&ntinue") );
					}
					else
					{
						TQString msg = i18n("The server certificate failed the "
							"authenticity test (%1).");
						result = messageBox( TDEIO::SlaveBase::WarningYesNoCancel,
						msg.arg(ourHost),
						i18n("Server Authentication"),
						i18n("&Details"),
						i18n("Co&ntinue") );
					}

					if (result == KMessageBox::Yes)
					{
						showInfoDialog();
					}
				}
				while (result == KMessageBox::Yes);

				if (result == KMessageBox::No)
				{
					rc = 1;
					cp = KSSLCertificateCache::Accept;
					doAddHost = true;
					result = messageBox( TDEIO::SlaveBase::WarningYesNo,
							i18n("Would you like to accept this "
							"certificate forever without "
							"being prompted?"),
							i18n("Server Authentication"),
								i18n("&Forever"),
								i18n("&Current Sessions Only"));
					if (result == KMessageBox::Yes)
						permacache = true;
					else
						permacache = false;
				}
				else
				{
					rc = -1;
					cp = KSSLCertificateCache::Prompt;
				}

				break;
		}
		default:
		kdDebug(14120) << "SSL error in cert code."
				<< "Please report this to kopete-devel@kde.org."
				<< endl;
		break;
		}
	}

	//  - cache the results
	d->cc->addCertificate(pc, cp, permacache);
	if (doAddHost)
		d->cc->addHost(pc, ourHost);


	if (rc == -1)
		return rc;


	kdDebug(14120) << "SSL connection information follows:" << endl
		<< "+-----------------------------------------------" << endl
		<< "| Cipher: " << d->kssl->connectionInfo().getCipher() << endl
		<< "| Description: " << d->kssl->connectionInfo().getCipherDescription() << endl
		<< "| Version: " << d->kssl->connectionInfo().getCipherVersion() << endl
		<< "| Strength: " << d->kssl->connectionInfo().getCipherUsedBits()
		<< " of " << d->kssl->connectionInfo().getCipherBits()
		<< " bits used." << endl
		<< "| PEER:" << endl
		<< "| Subject: " << d->kssl->peerInfo().getPeerCertificate().getSubject() << endl
		<< "| Issuer: " << d->kssl->peerInfo().getPeerCertificate().getIssuer() << endl
		<< "| Validation: " << (int)ksv << endl
		<< "| Certificate matches IP: " << _IPmatchesCN << endl
		<< "+-----------------------------------------------"
		<< endl;

	// sendMetaData();  Do not call this function!!
	return rc;
}


#include "ksslsocket.moc"
