/* Web Polygraph       http://www.web-polygraph.org/
 * (C) 2003-2006 The Measurement Factory
 * Licensed under the Apache License, Version 2.0 */

#include "base/polygraph.h"

#include "xstd/h/iomanip.h"

#include "xstd/Rnd.h"
#include "xstd/Ssl.h"
#include "base/RndPermut.h"
#include "base/polyLogCats.h"
#include "runtime/globals.h"
#include "runtime/AddrSubsts.h"
#include "runtime/HostMap.h"
#include "runtime/SslWrap.h"
#include "pgl/RobotSym.h"
#include "client/CltConnMgr.h"


CltConnMgr::CltConnMgr(): thePortMgr(0), thePipeDepth(0),
	theSslResumpProb(-1), theSslCacheLimit(-1),
	theMinNewConnProb(-1), theConnLvlLmt(-1)  {
}

CltConnMgr::~CltConnMgr() {
	closeAllIdle();
	while (theSslSessionCache.count()) delete theSslSessionCache.pop();
}

void CltConnMgr::configure(const SockOpt &anOpt, const RobotSym *cfg, int srvCnt) {
	ConnMgr::configure(anOpt, cfg->pconnUseLmt());
	cfg->openConnLimit(theConnLvlLmt);
	cfg->minimizeNewConn(theMinNewConnProb);

	if (theMinNewConnProb > 0 && !TheAddrSubsts->count()) {
		cerr << cfg->loc()
			<< "Robot's minimize_new_conn must be used with "
			<< "address substitutes to make sense " << endl << xexit;
	}

	thePipeDepth = cfg->pipelineDepth();

	// determine the maximum number of addresses (for concurrent idle conns)
	const int max = theConnLvlLmt >= 0 ? Min(theConnLvlLmt, srvCnt) : srvCnt;
	theIdleHash.ccAddrMax(max);
}

void CltConnMgr::portMgr(PortMgr *aPortMgr) {
	Assert(aPortMgr && !thePortMgr);
	thePortMgr = aPortMgr;
}

void CltConnMgr::configureSsl(SslCtx *aCtx, const SslWrap *wrap) {
	ConnMgr::configureSsl(aCtx, wrap);
	if (!theSslCtx)
		return;

	// we maintain our own cache, if any
	aCtx->sessionCacheMode(SSL_SESS_CACHE_OFF);

	theSslResumpProb = wrap->resumpProb();
	theSslCacheLimit = wrap->sessionCacheSize();
}

// note: idle connections can be closed on-demand
bool CltConnMgr::atHardConnLimit() const {
	return theConnLvlLmt >= 0 && (theConnLvl-theIdleQueue.count()) >= theConnLvlLmt;
}

Connection *CltConnMgr::get(const NetAddr &hopAddr, const NetAddr &destAddr) {
	static const NetAddr none;
	Connection *conn = 0;

	bool needsTunnel = false;
	bool needsSsl = needSsl(hopAddr, destAddr, needsTunnel);
	const NetAddr &tunnelAddr = needsTunnel ? destAddr : none;
	
	ConnHashPos pos;
	if (findIdle(hopAddr, tunnelAddr, pos)) {
		conn = theIdleHash.delAt(pos);
		theIdleQueue.dequeue(conn);
		conn->theRd.stop(this);
		TheFileScanner->clearTimeout(conn->sock().fd());
	} else {
		// check if we should close some idle conn first
		if (theConnLvlLmt >= 0 && theConnLvl >= theConnLvlLmt)
			closeIdle(theIdleQueue.firstOut(), ConnCloseStat::ckIdleLocal);
		conn = open(hopAddr, needsSsl);
		if (needsTunnel)
			conn->tunnelEnd(tunnelAddr);
	}

	if (conn)
		conn->startUse();

	Assert(theConnLvlLmt < 0 || theConnLvl <= theConnLvlLmt);
	Assert(!conn || (conn->sock() && !conn->bad()));
	return conn;
}

bool CltConnMgr::findIdle(const NetAddr &hopAddr, const NetAddr &tunnelAddr, ConnHashPos &pos) {
	return theIdleHash.find(hopAddr, tunnelAddr, pos) ||
		findIdleSubst(hopAddr, tunnelAddr, pos);
}

void CltConnMgr::closeAllIdle() {
	while (theIdleQueue.count())
		closeIdle(theIdleQueue.firstOut(), ConnCloseStat::ckIdleLocal);
}

Connection *CltConnMgr::open(const NetAddr &hopAddr, bool needsSsl) {
	Connection *conn = TheConnFarm.get();
	conn->logCat(lgcCltSide);

	if (needsSsl) {
		SslSession *sess = 0;
		static RndGen rng(LclPermut(rndSslSessionCache));
		if (theSslSessionCache.count() && rng.event(theSslResumpProb))
			sess = theSslSessionCache.pop();
		conn->useSsl(theSslCtx, sess); // eventually
	}

	if (conn->connect(hopAddr, theSockOpt, thePortMgr)) {
		int limit = 1; // no pipeling by default
		if (thePipeDepth)
			limit = (int)MiniMax(1.1, thePipeDepth->trial(), (double)INT_MAX);
		conn->useLevelLimit(limit);

		opened(conn);
		return conn;
	} else {
		// connect failed
		TheConnFarm.put(conn);
		return 0;
	}
}

bool CltConnMgr::needSsl(const NetAddr &hopAddr, const NetAddr &tunnelAddr, bool &needTunnel) const {
	// need SSL encryption if both source and at least one of the distinations
	// (hop or tunnel) need it

	if (!theSslCtx)
		return false; // source cannot do SSL

	if (TheHostMap->findSslWrap(hopAddr))
		needTunnel = false; // the next hop does SSL
	else
	if (TheHostMap->findSslWrap(tunnelAddr))
		needTunnel = hopAddr != tunnelAddr; // hop does not do SSL, final does
	else
		return false;      // neither hop nor final do SSL

	return true;
}


// called on an idle connection closed by the other side
void CltConnMgr::noteReadReady(int fd) {
	Connection *conn = TheConnIdx[fd];
	Assert(conn && conn->sock() && !conn->bad());

	if (conn->closing()) {
		ConnMgr::noteReadReady(fd);
	} else {
		// delete connection from the idle queue
		closeIdle(conn, ConnCloseStat::ckIdleForeign);
	}
}

bool CltConnMgr::findIdleSubst(const NetAddr &hopAddr, const NetAddr &tunnelAddr, ConnHashPos &pos) {
	static RndGen rng;
	if (!rng.event(theMinNewConnProb))
		return false;

	Assert(TheAddrSubsts);
	for (AddrSubsts::Iterator i = TheAddrSubsts->iterator(hopAddr); i; ++i) {
		if (theIdleHash.find(i.addr(), tunnelAddr, pos))
			return true;
	}
	return false;
}

void CltConnMgr::putIdle(Connection *conn) {
	Assert(conn && conn->sock() && !conn->bad());
	theIdleQueue.enqueue(conn);
	theIdleHash.add(conn);
	ConnMgr::putIdle(conn);
}

void CltConnMgr::delIdle(Connection *conn) {
	Assert(conn);
	theIdleQueue.dequeue(conn);
	theIdleHash.del(conn);
	conn->theRd.stop(this);
	TheFileScanner->clearTimeout(conn->sock().fd());
}

void CltConnMgr::closePrep(Connection *conn) {
	if (const Ssl *ssl = conn->sslActive()) {
		SslSession *sess = conn->sslSession();
		Should(!sess || theSslResumpProb > 0);

		if (sess && (conn->bad() || !ssl->reusedSession())) {
			// delete sessions of bad connections
			delete sess;
			sess = 0;
			conn->sslSessionForget();
		} else
		if (!sess && !conn->bad() && needMoreSslSessions()) {
			// remember sessions of new successful connections
			sess = ssl->refCountedSession();
		}

		if (sess)
			theSslSessionCache.push(sess);
	}
	ConnMgr::closePrep(conn);
}

bool CltConnMgr::needMoreSslSessions() const {
	return theSslResumpProb > 0 && (theSslCacheLimit < 0 ||
		theSslSessionCache.count() < theSslCacheLimit);
}


syntax highlighted by Code2HTML, v. 0.9.1