/**
 * jbengine.cpp
 * Yet Another Jabber Component Protocol Stack
 * This file is part of the YATE Project http://YATE.null.ro
 *
 * Yet Another Telephony Engine - a fully featured software PBX and IVR
 * Copyright (C) 2004-2014 Null Team
 *
 * This software is distributed under multiple licenses;
 * see the COPYING file in the main directory for licensing
 * information for this specific distribution.
 *
 * This use of this software may be subject to additional restrictions.
 * See the LEGAL file in the main directory for details.
 *
 * This program 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.
 */

#include <yatejabber.h>
#include <stdio.h>
#include <stdlib.h>

using namespace TelEngine;


static unsigned int fixValue(const NamedList& p, const char* param,
    unsigned int defVal, unsigned int min, unsigned int max, bool zero = false)
{
    unsigned int val = p.getIntValue(param,defVal);
    if (!val) {
	if (!zero)
	    val = defVal;
    }
    else if (val < min)
	val = min;
    else if (val > max)
	val = max;
    return val;
}

const TokenDict JBEvent::s_type[] = {
    {"Message",         Message},
    {"Presence",        Presence},
    {"Iq",              Iq},
    {"Terminated",      Terminated},
    {"Destroy",         Destroy},
    {"Start",           Start},
    {"Auth",            Auth},
    {"Bind",            Bind},
    {"Running",         Running},
    {"DbResult",        DbResult},
    {"DbVerify",        DbVerify},
    {"RegisterOk",      RegisterOk},
    {"RegisterFailed",  RegisterFailed},
    {"Unknown",         Unknown},
    {0,0}
};

const TokenDict JBConnect::s_statusName[] =  {
    {"Start",     Start},
    {"Address",   Address},
    {"Srv",       Srv},
    {"Domain",    Domain},
    {0,0}
};

// Entity caps item tag in document
static const String s_entityCapsItem = "item";
// Node values used by entity caps
static const String s_googleTalkNode = "http://www.google.com/xmpp/client/caps";
static const String s_googleMailNode = "http://mail.google.com/xmpp/client/caps";
static const String s_googleAndroidNode = "http://www.android.com/gtalk/client/caps";
static const String s_googleAndroidNode2 = "http://www.android.com/gtalk/client/caps2";

// Stream read buffer
#define JB_STREAMBUF                8192
#define JB_STREAMBUF_MIN            1024
// Stream restart counter
#define JB_RESTART_COUNT               2
#define JB_RESTART_COUNT_MIN           1
#define JB_RESTART_COUNT_MAX          10
#define JB_RESTART_UPDATE          15000
#define JB_RESTART_UPDATE_MIN       5000
#define JB_RESTART_UPDATE_MAX     300000
// Stream setup timer
#define JB_SETUP_INTERVAL         180000
#define JB_SETUP_INTERVAL_MIN      60000
#define JB_SETUP_INTERVAL_MAX     600000
// Wait stream start timer
#define JB_START_INTERVAL          20000
#define JB_START_INTERVAL_MIN      10000
#define JB_START_INTERVAL_MAX      60000
// Stream connect timer
#define JB_CONNECT_INTERVAL        60000
#define JB_CONNECT_INTERVAL_MIN     1000
#define JB_CONNECT_INTERVAL_MAX   120000
// Stream SRV query timer
#define JB_SRV_INTERVAL            30000
#define JB_SRV_INTERVAL_MIN        10000
#define JB_SRV_INTERVAL_MAX       120000
// Ping
#define JB_PING_INTERVAL          600000
#define JB_PING_INTERVAL_MIN       60000
#define JB_PING_INTERVAL_MAX     3600000
#define JB_PING_TIMEOUT            30000
#define JB_PING_TIMEOUT_MIN        10000
#define JB_PING_TIMEOUT_MAX        JB_PING_INTERVAL_MIN
// Idle
#define JB_IDLE_INTERVAL         3600000 // 1h
#define JB_IDLE_INTERVAL_MIN      600000 // 10min
#define JB_IDLE_INTERVAL_MAX    21600000 // 6h
// Redirect
#define JB_REDIRECT_COUNT              0
#define JB_REDIRECT_COUNT_CLIENT       2
#define JB_REDIRECT_MIN	               0
#define JB_REDIRECT_MAX	              10


/*
 * SASL
 */
static inline unsigned int findZero(const char* buf, unsigned int max)
{
    if (!buf)
	return max + 1;
    unsigned int pos = 0;
    while (pos < max && buf[pos])
	pos++;
    return pos;
}

// Parse and decode a buffer containing SASL plain authentication data
// See RFC 4616 Section 2
// Format: [authzid] UTF8NUL username UTF8NUL passwd
// Each token must be up to 255 bytes length
static NamedList* splitPlainSasl(const DataBlock& buf)
{
    const char* d = (const char*)buf.data();
    unsigned int len = buf.length();
    if (!len)
	return 0;
    String user, pwd, authzid;
    // Use a while to break to the end
    bool ok = false;
    while (true) {
	// authzid
	unsigned int ll = findZero(d,len);
	if (ll && (ll > 255 || ll > len))
	    break;
	authzid.assign(d,ll);
	if (-1 == authzid.lenUtf8())
	    break;
	d += ll;
	len -= ll;
	// Username
	if (d[0] || len < 2)
	    break;
	ll = findZero(++d,--len);
	if (!(ll && ll < len && ll < 256))
	    break;
	user.assign(d,ll);
	if (-1 == user.lenUtf8())
	    break;
	d += ll;
	len -= ll;
	// Password
	if (d[0] || len < 2)
	    break;
	ll = findZero(++d,--len);
	if (ll != len || ll > 255)
	    break;
	pwd.assign(d,ll);
	ok = (-1 != pwd.lenUtf8());
	break;
    }
    if (!ok)
	return 0;
    NamedList* result = new NamedList("");
    result->addParam("username",user);
    result->addParam("response",pwd);
    if (authzid)
	result->addParam("authzid",authzid);
    return result;
}

static NamedList* splitDigestSasl(const String& buf)
{
    const char* d = buf.c_str();
    unsigned int len = buf.length();
    NamedList* result = 0;
    while (len) {
	// Find '='
	unsigned int i = 0;
	while (i < len && d[i] != '=')
	    i++;
	if (!i || i >= len) {
	    Debug(DebugNote,"splitDigestSasl() unexpected end of buffer '%s'",d);
	    break;
	}
	// Get param name and skip over '='
	String name(d,i);
	i++;
	d += i;
	len -= i;
	XDebug(DebugAll,"splitDigestSasl() found directive='%s' rest='%s' len=%u",
	    name.c_str(),d,len);
	String value;
	if (len) {
	    // Find ',', handle quoted parameters
	    if (*d == '\"') {
		if (len < 2) {
		    Debug(DebugNote,
			"splitDigestSasl() unexpected end of buffer '%s'",d);
		    break;
		}
		// Find an unescaped "
		for (i = 1; i < len; i++) {
		    if (d[i] == '"' && d[i-1] != '\\')
			break;
		}
		if (i == len) {
		    Debug(DebugNote,"splitDigestSasl() unclosed '\"' found at %u",
			buf.length() - len);
		    break;
		}
		// Unescape the content
		value.assign(d + 1,i - 1);
		int pos = -1;
		unsigned int start = 0;
		bool ok = true;
		while (-1 != (value.find('\\',start))) {
		    if (pos == 0) {
			// No character to escape: error
			if (value.length() == 1) {
			    Debug(DebugNote,"splitDigestSasl() 2");
			    ok = false;
			    break;
			}
			value = value.substr(1);
		    }
		    else if ((unsigned int)pos < value.length() - 1) {
			if (value[pos - 1] != '"') {
			    // Escaped char
			    value = value.substr(0,pos) + value.substr(0,pos + 1);
			    start = pos + 1;
			}
			else if (value[pos + 1] == '"') {
			    // Escaped backslash
			    value = value.substr(0,pos - 1) + "\\" + value.substr(0,pos + 2);
			    start = pos + 1;
			}
			else {
			    // Error
			    Debug(DebugNote,"splitDigestSasl() 3");
			    ok = false;
			    break;
			}
		    }
		    else {
			// No character to escape: error
			Debug(DebugNote,"splitDigestSasl() 4");
			ok = false;
			break;
		    }
		}
		if (!ok)
		    break;
		// Adjust buffer and length
		if (i < len) {
		    if (i == len - 1)
			i++;
		    else if (d[i + 1] == ',')
			i += 2;
		    else {
			Debug(DebugNote,"splitDigestSasl() ',' not found at %u rest=%s",
			    buf.length() - len + i + 1,d);
			break;
		    }
		}
	    }
	    else {
		// Skip until ,
		for (i = 0; i < len && d[i] != ','; i++)
		    ;
		if (i)
		    value.assign(d,i);
		if (i < len)
		    i++;
	    }
	    d += i;
	    len -= i;
	}
	if (!result)
	    result = new NamedList("");
	XDebug(DebugAll,"splitDigestSasl() found '%s'='%s' rest='%s' len=%u",
	    name.c_str(),value.c_str(),d,len);
	result->addParam(name,value);
    }
    if (len)
	TelEngine::destruct(result);
    return result;
}

// Apend a quoted directive to a string
// Escape the value
static inline void appendQDirective(String& buf, const String& name,
    const String& value)
{
    if (-1 == value.find('\"') && -1 == value.find('\\')) {
	buf.append(name + "=\"" + value + "\"",",");
	return;
    }
    // Replace \ with "\" and " with \"
    // See RFC2831 7.2
    String tmp;
    char c = 0;
    char* s = (char*)value.c_str();
    while ((c = *s++)) {
	if (c == '\"')
	    tmp << '\\' << c;
	else if (c == '\\')
	    tmp << "\"\\\"";
	else
	    tmp += c;
    }
    buf.append(name + "=\"" + tmp + "\"",",");
}

// Constructor
SASL::SASL(bool plain, const char* realm)
    : m_plain(plain), m_params(0), m_realm(realm), m_nonceCount(0)
{
}

// Set auth params
void SASL::setAuthParams(const char* user, const char* pwd)
{
    if (TelEngine::null(user) &&  TelEngine::null(pwd))
	return;
    if (!m_params)
	m_params = new NamedList("");
    if (!TelEngine::null(user))
	m_params->setParam("username",user);
    if (!TelEngine::null(pwd))
	m_params->setParam("password",pwd);
}

// Build an auth response
bool SASL::buildAuthRsp(String& buf, const char* digestUri)
{
    if (!m_params)
	return false;

    // Plain. See RFC 4616 Section 2
    // Format: [authzid] UTF8NUL username UTF8NUL passwd
    // Each token must be up to 255 bytes length
    if (m_plain) {
	if (!m_params)
	    return false;
	String* user = m_params->getParam("username");
	String* pwd = m_params->getParam("password");
	if (!user || user->length() > 255 || !pwd || pwd->length() > 255)
	    return false;
	DataBlock data;
	unsigned char nul = 0;
	data.append(&nul,1);
	data += *user;
	data.append(&nul,1);
	data += *pwd;
	Base64 base64((void*)data.data(),data.length());
	base64.encode(buf);
	return true;
    }

    // Digest MD5. See RFC 2831 2.1.2.1
    String* pwd = m_params->getParam("password");
    if (!pwd)
	return false;

#define SASL_ADD_QDIR(n) { \
    NamedString* tmp = m_params->getParam(n); \
    if (tmp) \
	appendQDirective(buf,tmp->name(),*tmp); \
}
    SASL_ADD_QDIR("username")
    SASL_ADD_QDIR("realm")
    SASL_ADD_QDIR("nonce")
    MD5 md5(String((unsigned int)Random::random()));
    m_cnonce = md5.hexDigest();
    m_params->setParam("cnonce",m_cnonce);
    SASL_ADD_QDIR("cnonce")
    m_nonceCount++;
    char tmp[9];
    ::sprintf(tmp,"%08x",m_nonceCount);
    m_params->setParam("nc",tmp);
    SASL_ADD_QDIR("nc")
    m_params->setParam("qop","auth");
    SASL_ADD_QDIR("qop")
    m_params->setParam("digest-uri",digestUri);
    SASL_ADD_QDIR("digest-uri")
    String rsp;
    buildMD5Digest(rsp,*pwd);
    buf << ",response=" << rsp;
    SASL_ADD_QDIR("charset")
    SASL_ADD_QDIR("md5-sess")
    XDebug(DebugAll,"SASL built MD5 response %s [%p]",buf.c_str(),this);
#undef SASL_ADD_QDIR
    Base64 base64((void*)buf.c_str(),buf.length());
    buf.clear();
    base64.encode(buf);
    return true;
}

// Build an MD5 challenge from this object
// See RFC 2831 Section 2.1.1
bool SASL::buildMD5Challenge(String& buf)
{
    String tmp;
    if (m_realm) {
	if (-1 == m_realm.lenUtf8())
	    return false;
	appendQDirective(tmp,"realm",m_realm);
    }
    // Re-build nonce. Increase nonce count
    m_nonce.clear();
    m_nonce << (int)Time::msecNow() << (int)Random::random();
    MD5 md5(m_nonce);
    m_nonce = md5.hexDigest();
    m_nonceCount++;
    tmp.append("nonce=\"" + m_nonce + "\"",",");
    tmp << ",qop=\"auth\"";
    tmp << ",charset=\"utf-8\"";
    tmp << ",algorithm=\"md5-sess\"";
    // RFC 2831 2.1.1: The size of a digest-challenge MUST be less than 2048 bytes
    if (tmp.length() < 2048) {
	buf = tmp;
	return true;
    }
    m_nonceCount--;
    return false;
}

bool SASL::parsePlain(const DataBlock& buf)
{
#ifdef XDEBUG
    String tmp;
    tmp.hexify((void*)buf.data(),buf.length(),' ');
    Debug(DebugAll,"SASL::parsePlain() %s [%p]",tmp.c_str(),this);
#endif
    TelEngine::destruct(m_params);
    m_params = splitPlainSasl(buf);
    return m_params != 0;
}

// Parse and decode a buffer containing a SASL Digest MD5 challenge
bool SASL::parseMD5Challenge(const String& buf)
{
    XDebug(DebugAll,"SASL::parseMD5Challenge() %s [%p]",buf.c_str(),this);
    TelEngine::destruct(m_params);
    // RFC 2831 2.1.1: The size of a digest-response MUST be less than 2048 bytes
    if (buf.length() >= 2048) {
	Debug(DebugNote,"SASL::parseMD5Challenge() invalid length=%u (max=2048) [%p]",
	    buf.length(),this);
	return false;
    }
    m_params = splitDigestSasl(buf);
    if (!m_params) {
	Debug(DebugNote,"SASL::parseMD5Challenge() failed to split params [%p]",
	    this);
	return false;
    }
    return true;
}

// Parse and decode a buffer containing a SASL Digest MD5 response
// See RFC 2831
bool SASL::parseMD5ChallengeRsp(const String& buf)
{
    XDebug(DebugAll,"SASL::parseMD5ChallengeRsp() %s [%p]",buf.c_str(),this);
    TelEngine::destruct(m_params);
    // RFC 2831 2.1.2: The size of a digest-response MUST be less than 4096 bytes
    if (buf.length() >= 4096) {
	Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid length=%u (max=4096) [%p]",
	    buf.length(),this);
	return false;
    }
    m_params = splitDigestSasl(buf);
    if (!m_params) {
	Debug(DebugNote,"SASL::parseMD5ChallengeRsp() failed to split params [%p]",
	    this);
	return false;
    }
    bool ok = false;
    // Check realm, nonce, nonce count
    // Use a while to break to the end
    while (true) {
	String* tmp = m_params->getParam("realm");
	if (!tmp || *tmp != m_realm) {
	    Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid realm='%s' [%p]",
		TelEngine::c_safe(tmp),this);
	    break;
	}
	tmp = m_params->getParam("nonce");
	if (!tmp || *tmp != m_nonce) {
	    Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid nonce='%s' [%p]",
		TelEngine::c_safe(tmp),this);
	    break;
	}
	tmp = m_params->getParam("nc");
	if (!tmp || (unsigned int)tmp->toInteger(0,16) != m_nonceCount) {
	    Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid nonce count='%s' [%p]",
		TelEngine::c_safe(tmp),this);
	    break;
	}
	ok = true;
	break;
    }
    if (ok)
	return true;
    TelEngine::destruct(m_params);
    return false;
}

// Build a Digest MD5 SASL to be sent with authentication responses
// See RFC 2831 2.1.2.1
void SASL::buildMD5Digest(String& dest, const NamedList& params,
    const char* password, bool challengeRsp)
{
    const char* nonce = params.getValue("nonce");
    const char* cnonce = params.getValue("cnonce");
    String qop = params.getValue("qop","auth");
    MD5 md5;
    md5 << params.getValue("username") << ":" << params.getValue("realm");
    md5 << ":" << password;
    MD5 md5A1(md5.rawDigest(),16);
    md5A1 << ":" << nonce << ":" << cnonce;
    const char* authzid = params.getValue("authzid");
    if (authzid)
	md5A1 << ":" << authzid;
    MD5 md5A2;
    if (challengeRsp)
	md5A2 << "AUTHENTICATE";
    md5A2 << ":" << params.getValue("digest-uri");
    if (qop != "auth")
	md5A2 << ":" << String('0',32);
    MD5 md5Rsp;
    md5Rsp << md5A1.hexDigest();
    md5Rsp << ":" << nonce << ":" << params.getValue("nc");
    md5Rsp << ":" << cnonce << ":" << qop << ":" << md5A2.hexDigest();
    dest = md5Rsp.hexDigest();
}


/*
 * JBConnect
 */
// Constructor. Add itself to the stream's engine
JBConnect::JBConnect(const JBStream& stream)
    : m_status(Start), m_domain(stream.serverHost()), m_port(0),
    m_engine(stream.engine()), m_stream(stream.toString()),
    m_streamType((JBStream::Type)stream.type())
{
    bool redir = false;
    stream.connectAddr(m_address,m_port,m_localIp,m_status,m_srvs,&redir);
    if (redir && m_address) {
	char c = m_address[0];
	if ((c < '0' || c > '9') && c != '[' && m_address[m_address.length() - 1] != ']') {
	    // Redirect to domain: replace stream domain, clear address
	    m_domain = m_address;
	    m_address.clear();
	}
	else {
	    // Redirect to IP address: clear stream domain
	    m_domain.clear();
	}
    }
    if (m_engine)
	m_engine->connectStatus(this,true);
}

// Remove itself from engine
JBConnect::~JBConnect()
{
    terminated(0,true);
}

// Stop the thread
void JBConnect::stopConnect()
{
    Debug(m_engine,DebugStub,"JBConnect::stopConnect() not implemented!");
}

// Retrieve the stream name
const String& JBConnect::toString() const
{
    return m_stream;
}

// Connect the socket.
void JBConnect::connect()
{
    if (!m_engine)
	return;
    Debug(m_engine,DebugAll,"JBConnect(%s) starting stat=%s [%p]",
	m_stream.c_str(),lookup(m_status,s_statusName),this);
    int port = m_port;
    if (!port) {
	if (m_streamType == JBStream::c2s)
	    port = XMPP_C2S_PORT;
	else if (m_streamType == JBStream::s2s)
	    port = XMPP_S2S_PORT;
	else {
	    Debug(m_engine,DebugNote,"JBConnect(%s) no port for %s stream [%p]",
		m_stream.c_str(),lookup(m_streamType,JBStream::s_typeName),this);
	    return;
	}
    }
    Socket* sock = 0;
    bool stop = false;
    advanceStatus();
    // Try to use ip/port
    if (m_status == Address) {
	if (m_address && port) {
	    sock = connect(m_address,port,stop);
	    if (sock || stop || exiting(sock)) {
		terminated(sock,false);
		return;
	    }
	}
	advanceStatus();
    }
    if (m_status == Srv && m_domain) {
	if (!m_srvs.skipNull()) {
	    // Get SRV records from remote party
	    String query;
	    if (m_streamType == JBStream::c2s)
		query = "_xmpp-client._tcp.";
	    else
		query = "_xmpp-server._tcp.";
	    query << m_domain;
	    String error;
	    // Start connecting timeout
	    if (!notifyConnecting(true,true))
		return;
	    int code = 0;
	    if (Resolver::init())
		code = Resolver::srvQuery(query,m_srvs,&error);
	    // Stop the timeout if not exiting
	    if (exiting(sock) || !notifyConnecting(false,true)) {
		terminated(0,false);
		return;
	    }
	    if (!code)
		DDebug(m_engine,DebugAll,"JBConnect(%s) SRV query for '%s' got %u records [%p]",
		    m_stream.c_str(),query.c_str(),m_srvs.count(),this);
	    else
		Debug(m_engine,DebugNote,"JBConnect(%s) SRV query for '%s' failed: %d '%s' [%p]",
		    m_stream.c_str(),query.c_str(),code,error.c_str(),this);
	}
	else
	    // Remove the first entry: we already used it
	    m_srvs.remove();
	ObjList* o = 0;
	while (0 != (o = m_srvs.skipNull())) {
	    SrvRecord* rec = static_cast<SrvRecord*>(o->get());
	    sock = connect(rec->address(),rec->port(),stop);
	    o->remove();
	    if (sock || stop || exiting(sock)) {
		terminated(sock,false);
		return;
	    }
	}
	advanceStatus();
    }
    else if (m_status == Srv)
	advanceStatus();
    if (m_status == Domain) {
	// Try to resolve the domain
	if (port && m_domain)
	    sock = connect(m_domain,port,stop);
	advanceStatus();
    }
    terminated(sock,false);
}

// Create and try to connect a socket. Return it on success
// Set stop on fatal failure and return 0
Socket* JBConnect::connect(const char* addr, int port, bool& stop)
{
    Socket* sock = new Socket(PF_INET,SOCK_STREAM);
    // Bind to local ip
    if (m_localIp) {
	SocketAddr lip(PF_INET);
	lip.host(m_localIp);
	bool ok = false;
	if (lip.host()) {
	    ok = sock->bind(lip);
	    if (!ok) {
		String tmp;
		Thread::errorString(tmp,sock->error());
		Debug(m_engine,DebugNote,
		    "JBConnect(%s) failed to bind to '%s' (%s). %d '%s' [%p]",
		    m_stream.c_str(),lip.host().c_str(),m_localIp.c_str(),
		    sock->error(),tmp.c_str(),this);
	    }
	}
	else
	    Debug(m_engine,DebugNote,"JBConnect(%s) invalid local ip '%s' [%p]",
		m_stream.c_str(),m_localIp.c_str(),this);
	stop = !ok || exiting(sock);
	if (stop) {
	    deleteSocket(sock);
	    return 0;
	}
	DDebug(m_engine,DebugAll,"JBConnect(%s) bound to '%s' (%s) [%p]",
	    m_stream.c_str(),lip.host().c_str(),m_localIp.c_str(),this);
    }
    // Use async connect
    u_int64_t tout = 0;
    if (m_engine)
	tout = m_engine->m_connectTimeout * 1000;
    if (tout && !(sock->canSelect() && sock->setBlocking(false))) {
	tout = 0;
	if (sock->canSelect()) {
	    String tmp;
	    Thread::errorString(tmp,sock->error());
	    Debug(m_engine,DebugInfo,
		"JBConnect(%s) using sync connect (async set failed). %d '%s' [%p]",
		m_stream.c_str(),sock->error(),tmp.c_str(),this);
	}
	else
	    Debug(m_engine,DebugInfo,
		"JBConnect(%s) using sync connect (select() not available) [%p]",
		m_stream.c_str(),this);
    }
    if (!notifyConnecting(tout == 0)) {
	stop = true;
	deleteSocket(sock);
	return 0;
    }
    u_int64_t start = tout ? Time::now() : 0;
    SocketAddr a(PF_INET);
    a.host(addr);
    a.port(port);
    // Check exiting: it may take some time to resolve the domain
    stop = exiting(sock);
    if (stop)
	return 0;
    if (!a.host()) {
	Debug(m_engine,DebugNote,"JBConnect(%s) failed to resolve '%s' [%p]",
	    m_stream.c_str(),addr,this);
	deleteSocket(sock);
	return 0;
    }
    unsigned int intervals = 0;
    if (start) {
	start = Time::now() - start;
	if (tout > start)
	    intervals = (unsigned int)(tout - start) / Thread::idleUsec();
	// Make sure we wait for at least 1 timeout interval
	if (!intervals)
	    intervals = 1;
    }
    String domain;
    if (a.host() != addr)
	domain << " (" << addr << ")";
    Debug(m_engine,DebugAll,"JBConnect(%s) attempt to connect to '%s:%d'%s [%p]",
	m_stream.c_str(),a.host().c_str(),a.port(),domain.safe(),this);
    bool ok = (0 != sock->connect(a));
    bool timeout = false;
    // Async connect in progress
    if (!ok && sock->inProgress()) {
	bool done = false;
	bool event = false;
	while (intervals && !(done || event || stop)) {
	    if (!sock->select(0,&done,&event,Thread::idleUsec()))
	        break;
	    intervals--;
	    stop = exiting(sock);
	}
	timeout = !intervals && !(done || event);
	if (sock && !sock->error() && (done || event) && sock->updateError())
	    ok = !sock->error();
    }
    if (ok) {
	Debug(m_engine,DebugAll,"JBConnect(%s) connected to '%s:%d'%s [%p]",
	    m_stream.c_str(),a.host().c_str(),a.port(),domain.safe(),this);
	return sock;
    }
    if (sock) {
	String reason;
	if (timeout)
	    reason = "Timeout";
	else {
	    String tmp;
	    Thread::errorString(tmp,sock->error());
	    reason << sock->error() << " '" << tmp << "'";
	}
	Debug(m_engine,DebugNote,"JBConnect(%s) failed to connect to '%s:%d'%s. %s [%p]",
	    m_stream.c_str(),a.host().c_str(),a.port(),domain.safe(),reason.safe(),this);
	deleteSocket(sock);
    }
    return 0;
}

// Check if exiting. Release socket
bool JBConnect::exiting(Socket*& sock)
{
    bool done = Thread::check(false) || !m_engine || m_engine->exiting();
    if (done && sock)
	deleteSocket(sock);
    return done;
}

// Notify termination, remove from engine
void JBConnect::terminated(Socket* sock, bool final)
{
    bool done = exiting(sock);
    JBEngine* engine = m_engine;
    m_engine = 0;
    // Remove from engine
    if (engine)
	engine->connectStatus(this,false);
    if (done) {
	if (!final && Thread::check(false))
	    Debug(m_engine,DebugAll,"JBConnect(%s) cancelled [%p]",m_stream.c_str(),this);
	return;
    }
    JBStream* stream = engine->findStream(m_stream,m_streamType);
    if (!final)
	Debug(engine,DebugAll,"JBConnect(%s) terminated [%p]",m_stream.c_str(),this);
    else if (stream)
	Debug(engine,DebugWarn,"JBConnect(%s) abnormally terminated! [%p]",
	    m_stream.c_str(),this);
    // Notify stream
    if (stream) {
	stream->connectTerminated(sock);
	TelEngine::destruct(stream);
    }
    else {
	deleteSocket(sock);
	DDebug(engine,DebugInfo,"JBConnect(%s) stream vanished while connecting [%p]",
	    m_stream.c_str(),this);
    }
}

// Notify connecting to the stream. Return false if stream vanished
bool JBConnect::notifyConnecting(bool sync, bool useCurrentStat)
{
    JBStream* stream = m_engine ? m_engine->findStream(m_stream,m_streamType) : 0;
    if (!stream)
	return false;
    int stat = m_status;
    if (!useCurrentStat) {
	// Advertised state:
	// Srv --> Address: we'll advance the state on retry
	// Domain --> Start to re-start on retry
	if (m_status == Srv)
	    stat = Address;
	else if (m_status == Domain)
	    stat = Start;
    }
    bool ok = stream->connecting(sync,stat,m_srvs);
    TelEngine::destruct(stream);
    return ok;
}

// Delete a socket
void JBConnect::deleteSocket(Socket*& sock)
{
    if (!sock)
	return;
    sock->setReuse();
    sock->setLinger(0);
    delete sock;
    sock = 0;
}

// Advance the status
void JBConnect::advanceStatus()
{
    if (m_status == Start)
	m_status = Address;
    else if (m_status == Address) {
	if (m_domain) {
	    if (!m_port &&
		(m_streamType == JBStream::c2s || m_streamType == JBStream::s2s))
		m_status = Srv;
	    else
		m_status = Domain;
	}
	else
	    m_status = Start;
    }
    else if (m_status == Srv)
	m_status = Domain;
    else if (m_status == Domain)
	m_status = Start;
    else
	m_status = Address;
}


/*
 * JBEngine
 */
JBEngine::JBEngine(const char* name)
    : Mutex(true,"JBEngine"),
    m_exiting(false),
    m_restartMax(JB_RESTART_COUNT), m_restartUpdInterval(JB_RESTART_UPDATE),
    m_setupTimeout(JB_SETUP_INTERVAL), m_startTimeout(JB_START_INTERVAL),
    m_connectTimeout(JB_CONNECT_INTERVAL), m_srvTimeout(JB_SRV_INTERVAL),
    m_pingInterval(JB_PING_INTERVAL), m_pingTimeout(JB_PING_TIMEOUT),
    m_idleTimeout(0), m_pptTimeoutC2s(0), m_pptTimeout(0),
    m_streamReadBuffer(JB_STREAMBUF), m_maxIncompleteXml(XMPP_MAX_INCOMPLETEXML),
    m_redirectMax(JB_REDIRECT_COUNT),
    m_hasClientTls(true), m_printXml(0), m_initialized(false)
{
    debugName(name);
    XDebug(this,DebugAll,"JBEngine [%p]",this);
}

JBEngine::~JBEngine()
{
    XDebug(this,DebugAll,"~JBEngine [%p]",this);
}

// Cleanup streams. Stop all threads owned by this engine. Release memory
void JBEngine::destruct()
{
    cleanup(true,false);
    GenObject::destruct();
}

// Initialize the engine's parameters
void JBEngine::initialize(const NamedList& params)
{
    int lvl = params.getIntValue("debug_level",-1);
    if (lvl != -1)
	debugLevel(lvl);
    JBClientEngine* client = YOBJECT(JBClientEngine,this);
    String tmp = params.getValue("printxml");
    if (!tmp && client)
	tmp = "verbose";
    m_printXml = tmp.toBoolean() ? -1: ((tmp == "verbose") ? 1 : 0);

    m_streamReadBuffer = fixValue(params,"stream_readbuffer",
	JB_STREAMBUF,JB_STREAMBUF_MIN,(unsigned int)-1);
    m_maxIncompleteXml = fixValue(params,"stream_parsermaxbuffer",
	XMPP_MAX_INCOMPLETEXML,1024,(unsigned int)-1);
    m_restartMax = fixValue(params,"stream_restartcount",
	JB_RESTART_COUNT,JB_RESTART_COUNT_MIN,JB_RESTART_COUNT_MAX);
    m_restartUpdInterval = fixValue(params,"stream_restartupdateinterval",
	JB_RESTART_UPDATE,JB_RESTART_UPDATE_MIN,JB_RESTART_UPDATE_MAX);
    m_setupTimeout = fixValue(params,"stream_setuptimeout",
	JB_SETUP_INTERVAL,JB_SETUP_INTERVAL_MIN,JB_SETUP_INTERVAL_MAX);
    m_startTimeout = fixValue(params,"stream_starttimeout",
	JB_START_INTERVAL,JB_START_INTERVAL_MIN,JB_START_INTERVAL_MAX);
    m_connectTimeout = fixValue(params,"stream_connecttimeout",
	JB_CONNECT_INTERVAL,JB_CONNECT_INTERVAL_MIN,JB_CONNECT_INTERVAL_MAX);
    m_srvTimeout = fixValue(params,"stream_srvtimeout",
	JB_SRV_INTERVAL,JB_SRV_INTERVAL_MIN,JB_SRV_INTERVAL_MAX);
    m_pingInterval = fixValue(params,"stream_pinginterval",
	client ? JB_PING_INTERVAL : 0,JB_PING_INTERVAL_MIN,JB_PING_INTERVAL_MAX,true);
    m_pingTimeout = fixValue(params,"stream_pingtimeout",
	client ? JB_PING_TIMEOUT : 0,JB_PING_TIMEOUT_MIN,JB_PING_TIMEOUT_MAX,true);
    if (!(m_pingInterval && m_pingTimeout))
	m_pingInterval = m_pingTimeout = 0;
    m_idleTimeout = fixValue(params,"stream_idletimeout",
	JB_IDLE_INTERVAL,JB_IDLE_INTERVAL_MIN,JB_IDLE_INTERVAL_MAX);
    int defVal = JB_REDIRECT_COUNT;
    if (client)
	defVal = JB_REDIRECT_COUNT_CLIENT;
    m_redirectMax = params.getIntValue("stream_redirectcount",
	defVal,JB_REDIRECT_MIN,JB_REDIRECT_MAX);
    m_pptTimeoutC2s = params.getIntValue("stream_ppttimeout_c2s",10000,0,120000);
    m_pptTimeout = params.getIntValue("stream_ppttimeout",60000,0,180000);
    m_initialized = true;
}

// Terminate all streams
void JBEngine::cleanup(bool final, bool waitTerminate)
{
    DDebug(this,DebugAll,"JBEngine::cleanup() final=%s wait=%s",
	String::boolText(final),String::boolText(waitTerminate));
    dropAll(JBStream::TypeCount,JabberID::empty(),JabberID::empty(),
	XMPPError::Shutdown);
    lock();
    ObjList* found = m_connect.skipNull();
    if (found) {
	Debug(this,DebugAll,"Terminating %u stream connect threads",m_connect.count());
	for (ObjList* o = found; o; o = o->skipNext()) {
	    JBConnect* conn = static_cast<JBConnect*>(o->get());
	    XDebug(this,DebugAll,"Terminating connect thread (%p)",conn);
	    conn->stopConnect();
	}
    }
    unlock();
    if (found) {
	XDebug(this,DebugAll,"Waiting for stream connect threads to terminate");
	while (found) {
	    Thread::yield(false);
	    Lock lock(this);
	    found = m_connect.skipNull();
	}
	Debug(this,DebugAll,"Stream connect threads terminated");
    }
    stopStreamSets(waitTerminate);
}

// Accept an incoming stream connection. Build a stream
bool JBEngine::acceptConn(Socket* sock, SocketAddr& remote, JBStream::Type t, bool ssl)
{
    if (!sock)
	return false;
    if (exiting()) {
	Debug(this,DebugNote,
	    "Can't accept connection from '%s:%d' type='%s': engine is exiting",
	    remote.host().c_str(),remote.port(),lookup(t,JBStream::s_typeName));
	return false;
    }
    if (ssl && t != JBStream::c2s) {
	Debug(this,DebugNote,"SSL connection on non c2s stream");
	return false;
    }
    JBStream* s = 0;
    if (t == JBStream::c2s)
	s = new JBClientStream(this,sock,ssl);
    else if (t == JBStream::s2s)
	s = new JBServerStream(this,sock,false);
    else if (t == JBStream::comp)
	s = new JBServerStream(this,sock,true);
    else if (t == JBStream::cluster)
	s = new JBClusterStream(this,sock);
    if (s)
	addStream(s);
    else
	Debug(this,DebugNote,"Can't accept connection from '%s:%d' type='%s'",
	    remote.host().c_str(),remote.port(),lookup(t,JBStream::s_typeName));
    return s != 0;
}

// Find a stream by its name
JBStream* JBEngine::findStream(const String& id, JBStream::Type hint)
{
    if (!id)
	return 0;
    RefPointer<JBStreamSetList> list[JBStream::TypeCount];
    getStreamLists(list,hint);
    for (unsigned int i = 0; i < JBStream::TypeCount; i++) {
	if (!list[i])
	    continue;
	JBStream* stream = JBEngine::findStream(id,list[i]);
	if (stream) {
	    for (; i < JBStream::TypeCount; i++)
		list[i] = 0;
	    return stream;
	}
	list[i] = 0;
    }
    return 0;
}

// Find all c2s streams whose local or remote bare jid matches a given one
ObjList* JBEngine::findClientStreams(bool in, const JabberID& jid, int flags)
{
    if (!jid.node())
	return 0;
    RefPointer<JBStreamSetList> list;
    getStreamList(list,JBStream::c2s);
    if (!list)
	return 0;
    ObjList* result = 0;
    list->lock();
    for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) {
	JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) {
	    JBClientStream* stream = static_cast<JBClientStream*>(s->get());
	    // Ignore destroying streams
	    if (stream->incoming() != in || stream->state() == JBStream::Destroy)
		continue;
	    Lock lock(stream);
	    const JabberID& sid = in ? stream->remote() : stream->local();
	    if (sid.bare() == jid.bare() && stream->flag(flags) && stream->ref()) {
		if (!result)
		    result = new ObjList;
		result->append(stream);
	    }
	}
    }
    list->unlock();
    list = 0;
    return result;
}

// Find all c2s streams whose local or remote bare jid matches a given one and
//  their resource is found in the given list
ObjList* JBEngine::findClientStreams(bool in, const JabberID& jid, const ObjList& resources,
    int flags)
{
    if (!jid.node())
	return 0;
    RefPointer<JBStreamSetList> list;
    getStreamList(list,JBStream::c2s);
    if (!list)
	return 0;
    ObjList* result = 0;
    list->lock();
    for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) {
	JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) {
	    JBClientStream* stream = static_cast<JBClientStream*>(s->get());
	    // Ignore destroying streams
	    if (stream->incoming() != in || stream->state() == JBStream::Destroy)
		continue;
	    Lock lock(stream);
	    const JabberID& sid = in ? stream->remote() : stream->local();
	    if (sid.bare() == jid.bare() && resources.find(sid.resource()) &&
		stream->flag(flags) && stream->ref()) {
		if (!result)
		    result = new ObjList;
		result->append(stream);
	    }
	}
    }
    list->unlock();
    list = 0;
    return result;
}

// Find a c2s stream by its local or remote jid
JBClientStream* JBEngine::findClientStream(bool in, const JabberID& jid)
{
    if (!jid.node())
	return 0;
    RefPointer<JBStreamSetList> list;
    getStreamList(list,JBStream::c2s);
    if (!list)
	return 0;
    JBClientStream* found = 0;
    list->lock();
    for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) {
	JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) {
	    found = static_cast<JBClientStream*>(s->get());
	    // Ignore destroying streams
	    if (found->incoming() != in || found->state() == JBStream::Destroy)
		continue;
	    Lock lock(found);
	    const JabberID& sid = in ? found->remote() : found->local();
	    if (sid == jid && found->ref())
		break;
	    found = 0;
	}
	if (found)
	    break;
    }
    list->unlock();
    list = 0;
    return found;
}

// Terminate all streams matching type and/or local/remote jid
unsigned int JBEngine::dropAll(JBStream::Type type, const JabberID& local,
    const JabberID& remote, XMPPError::Type error, const char* reason)
{
    XDebug(this,DebugInfo,"dropAll(%s,%s,%s,%s,%s)",lookup(type,JBStream::s_typeName),
	local.c_str(),remote.c_str(),XMPPUtils::s_error[error].c_str(),reason);
    RefPointer<JBStreamSetList> list[JBStream::TypeCount];
    getStreamLists(list,type);
    unsigned int n = 0;
    for (unsigned int i = 0; i < JBStream::TypeCount; i++) {
	if (!list[i])
	    continue;
	list[i]->lock();
	for (ObjList* o = list[i]->sets().skipNull(); o; o = o->skipNext()) {
	    JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	    n += set->dropAll(local,remote,error,reason);
	}
	list[i]->unlock();
	list[i] = 0;
    }
    DDebug(this,DebugInfo,
	"Dropped %u streams type=%s local=%s remote=%s error=%s reason=%s",
	n,lookup(type,JBStream::s_typeName),local.c_str(),remote.c_str(),
	XMPPUtils::s_error[error].c_str(),reason);
    return n;
}

// Process an event
void JBEngine::processEvent(JBEvent* ev)
{
    Debug(this,DebugStub,"JBEngine::processEvent() not implemented!");
    returnEvent(ev);
}

// Return an event to this engine
void JBEngine::returnEvent(JBEvent* ev, XMPPError::Type error, const char* reason)
{
    if (!ev)
	return;
    // Send error when supported
    if (error != XMPPError::NoError)
	ev->sendStanzaError(error,reason);
    XDebug(this,DebugAll,"Deleting returned event (%p,%s)",ev,ev->name());
    TelEngine::destruct(ev);
}

// Start stream TLS
void JBEngine::encryptStream(JBStream* stream)
{
    Debug(this,DebugStub,"JBEngine::encryptStream() not implemented!");
}

// Connect an outgoing stream
void JBEngine::connectStream(JBStream* stream)
{
    Debug(this,DebugStub,"JBEngine::connectStream() not implemented!");
}

// Start stream compression
void JBEngine::compressStream(JBStream* stream, const String& formats)
{
    Debug(this,DebugStub,"JBEngine::compressStream() not implemented!");
}

// Build a dialback key
void JBEngine::buildDialbackKey(const String& id, const String& local,
    const String& remote, String& key)
{
    Debug(this,DebugStub,"JBEngine::buildDialbackKey() not implemented!");
}

// Check for duplicate stream id at a remote server
bool JBEngine::checkDupId(JBStream* stream)
{
    if (!stream || stream->incoming())
	return false;
    RefPointer<JBStreamSetList> list;
    getStreamList(list,stream->type());
    if (!list)
	return false;
    stream->lock();
    String domain = stream->remote().domain();
    String id = stream->id();
    stream->unlock();
    list->lock();
    JBStream* found = 0;
    for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) {
	JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) {
	    found = static_cast<JBStream*>(s->get());
	    if (found != stream && found->outgoing()) {
		// Lock the stream: its data might change
		Lock lock(found);
		// Ignore destroying streams
		if (found->remote().domain() == domain &&
		    found->id() == id && found->state() != JBStream::Destroy)
		    break;
	    }
	    found = 0;
	}
	if (found)
	    break;
    }
    list->unlock();
    list = 0;
    return found != 0;
}

// Print XML to output
void JBEngine::printXml(const JBStream* stream, bool send, XmlChild& xml) const
{
    if (!(m_printXml && debugAt(DebugInfo)))
	return;
    String s;
    if (m_printXml > 0)
	s << "\r\n-----";
    XMPPUtils::print(s,xml,m_printXml > 0);
    if (m_printXml > 0)
	s << "\r\n-----";
    const char* dir = send ? "Sending to" : "Receiving from";
    if (m_printXml < 0)
	Debug(stream,DebugInfo,"%s '%s' %s [%p]",dir,stream->remote().c_str(),s.c_str(),stream);
    else
	Debug(stream,DebugInfo,"%s '%s' [%p]%s",dir,stream->remote().c_str(),stream,s.c_str());
}

// Print an XML fragment to output
void JBEngine::printXml(const JBStream* stream, bool send, XmlFragment& frag) const
{
    if (!(m_printXml && debugAt(DebugInfo)))
	return;
    String s;
    if (m_printXml > 0)
	s << "\r\n-----";
    for (ObjList* o = frag.getChildren().skipNull(); o; o = o->skipNext())
	XMPPUtils::print(s,*static_cast<XmlChild*>(o->get()),m_printXml > 0);
    if (m_printXml > 0)
	s << "\r\n-----";
    const char* dir = send ? "Sending to" : "Receiving from";
    if (m_printXml < 0)
	Debug(stream,DebugInfo,"%s '%s' %s [%p]",dir,stream->remote().c_str(),s.c_str(),stream);
    else
	Debug(stream,DebugInfo,"%s '%s' [%p]%s",dir,stream->remote().c_str(),stream,s.c_str());
}

// Add a stream to one of the stream lists
void JBEngine::addStream(JBStream* stream)
{
    Debug(this,DebugStub,"JBEngine::addStream() not implemented!");
}

// Remove a stream
void JBEngine::removeStream(JBStream* stream, bool delObj)
{
    if (!stream)
	return;
    stopConnect(stream->toString());
}

// Add/remove a connect stream thread when started/stopped
void JBEngine::connectStatus(JBConnect* conn, bool started)
{
    if (!conn)
	return;
    Lock lock(this);
    if (started) {
	// Make sure we remove any existing connect stream with the same name
	stopConnect(conn->toString());
	m_connect.append(conn)->setDelete(false);
	DDebug(this,DebugAll,"Added stream connect thread (%p)",conn);
    }
    else {
	GenObject* o = m_connect.remove(conn,false);
	if (o)
	    DDebug(this,DebugAll,"Removed stream connect thread (%p)",conn);
    }
}

// Stop a connect stream
void JBEngine::stopConnect(const String& name)
{
    Lock lock(this);
    ObjList* o = m_connect.find(name);
    if (!o)
	return;
    JBConnect* conn = static_cast<JBConnect*>(o->get());
    Debug(this,DebugAll,"Stopping stream connect thread (%p,%s)",conn,name.c_str());
    conn->stopConnect();
    o->remove(false);
}

// Find a stream by its name in a given set list
JBStream* JBEngine::findStream(const String& id, JBStreamSetList* list)
{
    if (!list)
	return 0;
    Lock lock(list);
    ObjList* found = 0;
    for (ObjList* o = list->sets().skipNull(); !found && o; o = o->skipNext()) {
	JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	found = set->clients().find(id);
    }
    JBStream* stream = found ? static_cast<JBStream*>(found->get()) : 0;
    if (stream && !stream->ref())
	stream = 0;
    return stream;
}


/*
 * JBServerEngine
 */
JBServerEngine::JBServerEngine(const char* name)
    : JBEngine(name),
    m_streamIndex(0),
    m_c2sReceive(0), m_c2sProcess(0), m_s2sReceive(0), m_s2sProcess(0),
    m_compReceive(0), m_compProcess(0), m_clusterReceive(0), m_clusterProcess(0)
{
}

JBServerEngine::~JBServerEngine()
{
}

// Terminate all streams
void JBServerEngine::cleanup(bool final, bool waitTerminate)
{
    JBEngine::cleanup(final,waitTerminate);
    DDebug(this,DebugAll,"JBServerEngine::cleanup() final=%s wait=%s",
	String::boolText(final),String::boolText(waitTerminate));
    if (!final)
	return;
    Lock lock(this);
    TelEngine::destruct(m_c2sReceive);
    TelEngine::destruct(m_c2sProcess);
    TelEngine::destruct(m_s2sReceive);
    TelEngine::destruct(m_s2sProcess);
    TelEngine::destruct(m_compReceive);
    TelEngine::destruct(m_compProcess);
    TelEngine::destruct(m_clusterReceive);
    TelEngine::destruct(m_clusterProcess);
}

// Stop all stream sets
void JBServerEngine::stopStreamSets(bool waitTerminate)
{
    XDebug(this,DebugAll,"JBServerEngine::stopStreamSets() wait=%s",
	String::boolText(waitTerminate));
    lock();
    RefPointer<JBStreamSetList> sets[] = {m_c2sReceive,m_c2sProcess,
	m_s2sReceive,m_s2sProcess,m_compReceive,m_compProcess,
	m_clusterReceive,m_clusterProcess};
    unlock();
    int n = 2 * JBStream::TypeCount;
    for (int i = 0; i < n; i++)
	if (sets[i])
	    sets[i]->stop(0,waitTerminate);
    for (int j = 0; j < n; j++)
	sets[j] = 0;
}

// Retrieve the list of streams of a given type
void JBServerEngine::getStreamList(RefPointer<JBStreamSetList>& list, int type)
{
    Lock lock(this);
    if (type == JBStream::c2s)
	list = m_c2sReceive;
    else if (type == JBStream::s2s)
	list = m_s2sReceive;
    else if (type == JBStream::comp)
	list = m_compReceive;
    else if (type == JBStream::cluster)
	list = m_clusterReceive;
}

// Retrieve the stream lists of a given type
void JBServerEngine::getStreamListsType(int type, RefPointer<JBStreamSetList>& recv,
    RefPointer<JBStreamSetList>& process)
{
    if (type == JBStream::c2s) {
	recv = m_c2sReceive;
	process = m_c2sProcess;
    }
    else if (type == JBStream::s2s) {
	recv = m_s2sReceive;
	process = m_s2sProcess;
    }
    else if (type == JBStream::comp) {
	recv = m_compReceive;
	process = m_compProcess;
    }
    else if (type == JBStream::cluster) {
	recv = m_clusterReceive;
	process = m_clusterProcess;
    }
}

// Find a server to server or component stream by local/remote domain.
// Skip over outgoing dialback streams
JBServerStream* JBServerEngine::findServerStream(const String& local, const String& remote,
    bool out, bool auth)
{
    if (!(local && remote))
	return 0;
    lock();
    RefPointer<JBStreamSetList> list[2] = {m_s2sReceive,m_compReceive};
    unlock();
    JBServerStream* stream = 0;
    for (int i = 0; i < 2; i++) {
	list[i]->lock();
	for (ObjList* o = list[i]->sets().skipNull(); o; o = o->skipNext()) {
	    JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	    for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) {
		stream = static_cast<JBServerStream*>(s->get());
		if (stream->type() == JBStream::comp ||
		    (out == stream->outgoing() && !stream->dialback())) {
		    // Lock the stream: remote jid might change
		    Lock lock(stream);
		    if (local != stream->local()) {
			stream = 0;
			continue;
		    }
		    bool checkRemote = out || stream->type() == JBStream::comp;
		    if ((checkRemote && remote == stream->remote()) ||
			(!checkRemote && stream->hasRemoteDomain(remote,auth))) {
			stream->ref();
			break;
		    }
		}
		stream = 0;
	    }
	    if (stream)
		break;
	}
	list[i]->unlock();
	if (stream)
	    break;
    }
    list[0] = list[1] = 0;
    return stream;
}

// Create an outgoing s2s stream.
JBServerStream* JBServerEngine::createServerStream(const String& local,
    const String& remote, const char* dbId, const char* dbKey, bool dbOnly,
    const NamedList* params)
{
    if (exiting()) {
	Debug(this,DebugAll,"Can't create s2s local=%s remote=%s: engine is exiting",
	    local.c_str(),remote.c_str());
	return 0;
    }
    JBServerStream* stream = 0;
    if (!dbOnly)
	stream = findServerStream(local,remote,true);
    if (!stream) {
	stream = new JBServerStream(this,local,remote,dbId,dbKey,dbOnly,params);
	stream->ref();
	addStream(stream);
    }
    else
	TelEngine::destruct(stream);
    return stream;
}

// Create an outgoing comp stream
JBServerStream* JBServerEngine::createCompStream(const String& name, const String& local,
    const String& remote, const NamedList* params)
{
    if (exiting()) {
	Debug(this,DebugAll,"Can't create comp local=%s remote=%s: engine is exiting",
	    local.c_str(),remote.c_str());
	return 0;
    }
    JBServerStream* stream = findServerStream(local,remote,true);
    if (!stream) {
	stream = new JBServerStream(this,local,remote,&name,params);
	stream->ref();
	addStream(stream);
    }
    return stream;
}

// Find a cluster stream by remote domain
JBClusterStream* JBServerEngine::findClusterStream(const String& remote,
    JBClusterStream* skip)
{
    if (!remote)
	return 0;
    lock();
    RefPointer<JBStreamSetList> list = m_clusterReceive;
    unlock();
    JBClusterStream* stream = 0;
    list->lock();
    for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) {
	JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) {
	    stream = static_cast<JBClusterStream*>(s->get());
	    if (skip != stream) {
		Lock lock(stream);
		if (stream->state() != JBStream::Destroy &&
		    remote == stream->remote()) {
		    stream->ref();
		    break;
		}
	    }
	    stream = 0;
	}
    }
    list->unlock();
    list = 0;
    return stream;
}

// Create an outgoing cluster stream
JBClusterStream* JBServerEngine::createClusterStream(const String& local,
    const String& remote, const NamedList* params)
{
    if (exiting()) {
	Debug(this,DebugAll,"Can't create cluster local=%s remote=%s: engine is exiting",
	    local.c_str(),remote.c_str());
	return 0;
    }
    JBClusterStream* stream = findClusterStream(remote);
    if (!stream) {
	stream = new JBClusterStream(this,local,remote,params);
	stream->ref();
	addStream(stream);
    }
    return stream;
}

// Terminate all incoming c2s streams matching a given JID
unsigned int JBServerEngine::terminateClientStreams(const JabberID& jid,
    XMPPError::Type error, const char* reason)
{
    unsigned int n = 0;
    ObjList* list = findClientStreams(true,jid);
    if (!list)
	return 0;
    n = list->count();
    DDebug(this,DebugInfo,"Terminating %u incoming c2s streams jid=%s error=%s reason=%s",
	n,jid.bare().c_str(),XMPPUtils::s_tag[error].c_str(),reason);
    for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
	JBClientStream* stream = static_cast<JBClientStream*>(o->get());
	stream->terminate(-1,true,0,error,reason);
    }
    TelEngine::destruct(list);
    return n;
}

// Add a stream to one of the stream lists
void JBServerEngine::addStream(JBStream* stream)
{
    if (!stream)
	return;
    lock();
    RefPointer<JBStreamSetList> recv;
    RefPointer<JBStreamSetList> process;
    getStreamListsType(stream->type(),recv,process);
    unlock();
    if (recv && process) {
	recv->add(stream);
	process->add(stream);
    }
    else
	DDebug(this,DebugStub,"JBServerEngine::addStream() type='%s' not handled!",
	    stream->typeName());
    recv = 0;
    process = 0;
    TelEngine::destruct(stream);
}

// Remove a stream
void JBServerEngine::removeStream(JBStream* stream, bool delObj)
{
    if (!stream)
	return;
    JBEngine::removeStream(stream,delObj);
    lock();
    DDebug(this,DebugAll,"JBServerEngine::removeStream(%p,%u) id=%s",
	stream,delObj,stream->toString().c_str());
    RefPointer<JBStreamSetList> recv;
    RefPointer<JBStreamSetList> process;
    getStreamListsType(stream->type(),recv,process);
    unlock();
    if (recv)
	recv->remove(stream,delObj);
    if (process)
	process->remove(stream,delObj);
    recv = 0;
    process = 0;
}


/*
 * JBClientEngine
 */
JBClientEngine::JBClientEngine(const char* name)
    : JBEngine(name),
    m_receive(0), m_process(0)
{
}

JBClientEngine::~JBClientEngine()
{
}

// Terminate all streams
void JBClientEngine::cleanup(bool final, bool waitTerminate)
{
    JBEngine::cleanup(final,waitTerminate);
    DDebug(this,DebugAll,"JBClientEngine::cleanup() final=%s wait=%s",
	String::boolText(final),String::boolText(waitTerminate));
    if (!final)
	return;
    Lock lock(this);
    TelEngine::destruct(m_receive);
    TelEngine::destruct(m_process);
}

// Find a stream by account
JBClientStream* JBClientEngine::findAccount(const String& account)
{
    if (!account)
	return 0;
    RefPointer<JBStreamSetList> list;
    getStreamList(list,JBStream::c2s);
    if (!list)
	return 0;
    JBClientStream* found = 0;
    list->lock();
    for (ObjList* o = list->sets().skipNull(); !found && o; o = o->skipNext()) {
	JBStreamSet* set = static_cast<JBStreamSet*>(o->get());
	for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) {
	    found = static_cast<JBClientStream*>(s->get());
	    if (account == found->account())
		break;
	    found = 0;
	}
    }
    if (found && !found->ref())
	found = 0;
    list->unlock();
    list = 0;
    return found;
}

// Build an outgoing client stream
JBClientStream* JBClientEngine::create(const String& account, const NamedList& params,
    const String& name)
{
    if (!account)
	return 0;
    String serverHost;
    String username = params.getValue("username");
    String domain = params.getValue("domain");
    int pos = username.find("@");
    if (pos > 0) {
	serverHost = domain;
	domain = username.substr(pos + 1);
	username = username.substr(0,pos);
    }
    if (!domain)
	domain = params.getValue("server",params.getValue("address"));
    JabberID jid(username,domain,params.getValue("resource"));
    if (!jid.bare()) {
	Debug(this,DebugNote,"Can't create client stream: invalid jid=%s",jid.bare().c_str());
	return 0;
    }
    Lock lock(this);
    JBClientStream* stream = static_cast<JBClientStream*>(findAccount(account));
    if (!stream) {
	stream = new JBClientStream(this,jid,account,params,name,serverHost);
	stream->ref();
	addStream(stream);
    }
    else
	TelEngine::destruct(stream);
    return stream;
}

// Add a stream to one of the stream lists
void JBClientEngine::addStream(JBStream* stream)
{
    if (!stream)
	return;
    lock();
    RefPointer<JBStreamSetList> recv = 0;
    RefPointer<JBStreamSetList> process = 0;
    if (stream->type() == JBStream::c2s) {
	recv = m_receive;
	process = m_process;
    }
    unlock();
    if (recv && process) {
	recv->add(stream);
	process->add(stream);
    }
    else
	DDebug(this,DebugStub,"JBClientEngine::addStream() type='%s' not handled!",
	    stream->typeName());
    recv = 0;
    process = 0;
    TelEngine::destruct(stream);
}

// Remove a stream
void JBClientEngine::removeStream(JBStream* stream, bool delObj)
{
    if (!stream)
	return;
    JBEngine::removeStream(stream,delObj);
    lock();
    DDebug(this,DebugAll,"JBClientEngine::removeStream(%p,%u) id=%s",
	stream,delObj,stream->toString().c_str());
    RefPointer<JBStreamSetList> recv;
    RefPointer<JBStreamSetList> process;
    if (stream->type() == JBStream::c2s) {
	recv = m_receive;
	process = m_process;
    }
    unlock();
    if (recv)
	recv->remove(stream,delObj);
    if (process)
	process->remove(stream,delObj);
    recv = 0;
    process = 0;
}

// Stop all stream sets
void JBClientEngine::stopStreamSets(bool waitTerminate)
{
    XDebug(this,DebugAll,"JBClientEngine::stopStreamSets() wait=%s",
	String::boolText(waitTerminate));
    lock();
    RefPointer<JBStreamSetList> receive = m_receive;
    RefPointer<JBStreamSetList> process = m_process;
    unlock();
    if (receive)
	receive->stop(0,waitTerminate);
    if (process)
	process->stop(0,waitTerminate);
    receive = 0;
    process = 0;
}

// Retrieve the list of streams of a given type
void JBClientEngine::getStreamList(RefPointer<JBStreamSetList>& list, int type)
{
    if (type != JBStream::c2s)
	return;
    Lock lock(this);
    list = m_receive;
}


/*
 * JBEvent
 */
JBEvent::~JBEvent()
{
    releaseStream(true);
    releaseXml(true);
    XDebug(DebugAll,"JBEvent::~JBEvent [%p]",this);
}

// Get a client stream from the event's stream
JBClientStream* JBEvent::clientStream()
{
    return m_stream ? m_stream->clientStream() : 0;
}

// Get a server stream from the event's stream
JBServerStream* JBEvent::serverStream()
{
    return m_stream ? m_stream->serverStream() : 0;
}

JBClusterStream* JBEvent::clusterStream()
{
    return m_stream ? m_stream->clusterStream() : 0;
}

// Delete the underlying XmlElement(s). Release the ownership.
XmlElement* JBEvent::releaseXml(bool del)
{
    m_child = 0;
    if (del) {
	TelEngine::destruct(m_element);
	return 0;
    }
    XmlElement* tmp = m_element;
    m_element = 0;
    return tmp;
}

void JBEvent::releaseStream(bool release)
{
    if (m_link && m_stream) {
	m_stream->eventTerminated(this);
	m_link = false;
    }
    if (release)
	TelEngine::destruct(m_stream);
}

// Build an 'iq' result stanza from event data
XmlElement* JBEvent::buildIqResult(bool addTags, XmlElement* child)
{
    XmlElement* xml = 0;
    if (addTags)
	xml = XMPPUtils::createIqResult(m_to,m_from,m_id,child);
    else
	xml = XMPPUtils::createIqResult(0,0,m_id,child);
    return xml;
}

// Build and send a stanza 'result' from enclosed 'iq' element
bool JBEvent::sendIqResult(XmlElement* child)
{
    if (!(m_element && m_stream && XMPPUtils::isUnprefTag(*m_element,XmlTag::Iq))) {
	TelEngine::destruct(child);
	return false;
    }
    if (m_stanzaType == "error" || m_stanzaType == "result") {
	TelEngine::destruct(child);
	return false;
    }
    XmlElement* xml = buildIqResult(true,child);
    bool ok = m_stream->state() == JBStream::Running ?
	m_stream->sendStanza(xml) : m_stream->sendStreamXml(m_stream->state(),xml);
    if (ok) {
	releaseXml(true);
	return true;
    }
    return false;
}

// Build an 'iq' error stanza from event data
XmlElement* JBEvent::buildIqError(bool addTags, XMPPError::Type error, const char* reason,
    XMPPError::ErrorType type)
{
    XmlElement* xml = 0;
    if (addTags)
	xml = XMPPUtils::createIq(XMPPUtils::IqError,m_to,m_from,m_id);
    else
	xml = XMPPUtils::createIq(XMPPUtils::IqError,0,0,m_id);
    if (!m_id)
	xml->addChild(releaseXml());
    xml->addChild(XMPPUtils::createError(type,error,reason));
    return xml;
}

// Build and send a stanza error from enclosed element
bool JBEvent::sendStanzaError(XMPPError::Type error, const char* reason,
    XMPPError::ErrorType type)
{
    if (!(m_element && m_stream && XMPPUtils::isStanza(*m_element)))
	return false;
    if (m_stanzaType == "error" || m_stanzaType == "result")
	return false;
    XmlElement* xml = new XmlElement(m_element->toString());
    xml->setAttributeValid("from",m_to);
    xml->setAttributeValid("to",m_from);
    xml->setAttributeValid("id",m_id);
    xml->setAttribute("type","error");
    xml->addChild(XMPPUtils::createError(type,error,reason));
    bool ok = m_stream->state() == JBStream::Running ?
	m_stream->sendStanza(xml) : m_stream->sendStreamXml(m_stream->state(),xml);
    if (ok) {
	releaseXml(true);
	return true;
    }
    return false;
}

bool JBEvent::init(JBStream* stream, XmlElement* element,
    const JabberID* from, const JabberID* to)
{
    bool bRet = true;
    if (stream && stream->ref())
	m_stream = stream;
    else
	bRet = false;
    m_element = element;
    if (from)
	m_from = *from;
    if (to)
	m_to = *to;
    XDebug(DebugAll,"JBEvent::init type=%s stream=(%p) xml=(%p) [%p]",
	name(),m_stream,m_element,this);
    if (!m_element)
	return bRet;

    // Most elements have these parameters:
    m_stanzaType = m_element->getAttribute("type");
    if (!from)
	m_from = m_element->getAttribute("from");
    if (!to)
	m_to = m_element->getAttribute("to");
    m_id = m_element->getAttribute("id");

    // Decode some data
    int t = XMPPUtils::tag(*m_element);
    switch (t) {
	case XmlTag::Message:
	    if (m_stanzaType != "error")
		m_text = XMPPUtils::body(*m_element);
	    else
		XMPPUtils::decodeError(m_element,m_text,m_text);
	    break;
	case XmlTag::Iq:
	case XmlTag::Presence:
	    if (m_stanzaType != "error")
		break;
	default:
	    XMPPUtils::decodeError(m_element,m_text,m_text);
    }
    return bRet;
}


/*
 * JBStreamSet
 */
// Constructor
JBStreamSet::JBStreamSet(JBStreamSetList* owner)
    : Mutex(true,"JBStreamSet"),
    m_changed(false), m_exiting(false), m_owner(owner)
{
    XDebug(m_owner->engine(),DebugAll,"JBStreamSet::JBStreamSet(%s) [%p]",
	m_owner->toString().c_str(),this);
}

// Remove from owner
JBStreamSet::~JBStreamSet()
{
    if (m_clients.skipNull())
	Debug(m_owner->engine(),DebugCrit,
	    "JBStreamSet(%s) destroyed while owning %u streams [%p]",
	    m_owner->toString().c_str(),m_clients.count(),this);
    m_owner->remove(this);
    XDebug(m_owner->engine(),DebugAll,"JBStreamSet::~JBStreamSet(%s) [%p]",
	m_owner->toString().c_str(),this);
}

// Add a stream to the set. The stream's reference counter will be increased
bool JBStreamSet::add(JBStream* client)
{
    if (!client)
	return false;
    Lock lock(this);
    if (m_exiting || (m_owner->maxStreams() && m_clients.count() >= m_owner->maxStreams()) ||
	!client->ref())
	return false;
    m_clients.append(client);
    m_changed = true;
    DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) added (%p,'%s') type=%s [%p]",
	m_owner->toString().c_str(),client,client->name(),client->typeName(),this);
    return true;
}

// Remove a stream from set
bool JBStreamSet::remove(JBStream* client, bool delObj)
{
    if (!client)
	return false;
    Lock lock(this);
    ObjList* o = m_clients.find(client);
    if (!o)
	return false;
    DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) removing (%p,'%s') delObj=%u [%p]",
	m_owner->toString().c_str(),client,client->name(),delObj,this);
    o->remove(delObj);
    m_changed = true;
    return true;
}

// Terminate all streams matching and/or local/remote jid
unsigned int JBStreamSet::dropAll(const JabberID& local, const JabberID& remote,
    XMPPError::Type error, const char* reason)
{
    DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) dropAll(%s,%s,%s,%s) [%p]",
	m_owner->toString().c_str(),local.c_str(),remote.c_str(),
	XMPPUtils::s_error[error].c_str(),reason,this);
    unsigned int n = 0;
    lock();
    for (ObjList* s = m_clients.skipNull(); s; s = s->skipNext()) {
	JBStream* stream = static_cast<JBStream*>(s->get());
	Lock lck(stream);
	bool terminate = false;
	if (!(local || remote))
	    terminate = true;
	else {
	    if (local)
		terminate = stream->local().match(local);
	    if (remote && !terminate) {
		JBServerStream* s2s = stream->incoming() ? stream->serverStream() : 0;
		if (s2s)
		    terminate = s2s->hasRemoteDomain(remote,false);
		else
		    terminate = stream->remote().match(remote);
	    }
	}
	if (terminate) {
	    if (stream->state() != JBStream::Destroy)
		n++;
	    stream->terminate(-1,true,0,error,reason);
	}
    }
    unlock();
    return n;
}

// Process the list
void JBStreamSet::run()
{
    DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) start running [%p]",
	m_owner->toString().c_str(),this);
    ObjList* o = 0;
    while (true) {
	if (Thread::check(false)) {
	    m_exiting = true;
	    break;
	}
	lock();
	if (m_changed) {
	    o = 0;
	    m_changed = false;
	}
	else if (o)
	    o = o->skipNext();
	if (!o)
	    o = m_clients.skipNull();
	bool eof = o && !o->skipNext();
	RefPointer<JBStream> stream = o ? static_cast<JBStream*>(o->get()) : 0;
	unlock();
	if (stream) {
	    process(*stream);
	    stream = 0;
	}
	else {
	    // Lock the owner to prevent adding a new client
	    // Don't exit if a new client was already added
	    Lock lock(m_owner);
	    if (!m_changed) {
		m_exiting = true;
		break;
	    }
	}
	if (eof) {
	    if (m_owner->m_sleepMs)
		Thread::msleep(m_owner->m_sleepMs,false);
	    else
		Thread::idle(false);
	}
    }
    DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) stop running [%p]",
	m_owner->toString().c_str(),this);
}

// Start running
bool JBStreamSet::start()
{
    Debug(m_owner->engine(),DebugStub,"JBStreamSet(%s)::start() [%p]",
	m_owner->toString().c_str(),this);
    return false;
}

// Stop running
void JBStreamSet::stop()
{
    Debug(m_owner->engine(),DebugStub,"JBStreamSet(%s)::stop() [%p]",
	m_owner->toString().c_str(),this);
}


/*
 * JBStreamSetProcessor
 */
// Calls stream's getEvent(). Pass a generated event to the engine
bool JBStreamSetProcessor::process(JBStream& stream)
{
    JBEvent* ev = stream.getEvent();
    if (!ev)
	return false;
    bool remove = (ev->type() == JBEvent::Destroy);
    m_owner->engine()->processEvent(ev);
    if (remove) {
	DDebug(m_owner->engine(),DebugAll,
	    "JBStreamSetProcessor(%s) requesting stream (%p,%s) ref %u removal [%p]",
	    m_owner->toString().c_str(),&stream,stream.toString().c_str(),
	    stream.refcount(),this);
	m_owner->engine()->removeStream(&stream,true);
    }
    return true;
}


/*
 * JBStreamSetReceive
 */
JBStreamSetReceive::JBStreamSetReceive(JBStreamSetList* owner)
    : JBStreamSet(owner)
{
    if (owner && owner->engine())
	m_buffer.assign(0,owner->engine()->streamReadBuffer());
}

// Calls stream's readSocket()
bool JBStreamSetReceive::process(JBStream& stream)
{
    return stream.readSocket((char*)m_buffer.data(),m_buffer.length());
}


/*
 * JBStreamSetList
 */
// Constructor
JBStreamSetList::JBStreamSetList(JBEngine* engine, unsigned int max,
    unsigned int sleepMs, const char* name)
    : Mutex(true,"JBStreamSetList"),
    m_engine(engine), m_name(name),
    m_max(max), m_sleepMs(sleepMs), m_streamCount(0)
{
    XDebug(m_engine,DebugAll,"JBStreamSetList::JBStreamSetList(%s) [%p]",
	m_name.c_str(),this);
}

// Destructor
JBStreamSetList::~JBStreamSetList()
{
    XDebug(m_engine,DebugAll,"JBStreamSetList::~JBStreamSetList(%s) [%p]",
	m_name.c_str(),this);
}

// Add a stream to the list. Build a new set if there is no room in existing sets
bool JBStreamSetList::add(JBStream* client)
{
    if (!client || m_engine->exiting())
	return false;
    Lock lock(this);
    for (ObjList* o = m_sets.skipNull(); o; o = o->skipNext()) {
	if ((static_cast<JBStreamSet*>(o->get()))->add(client)) {
	    m_streamCount++;
	    return true;
	}
    }
    // Build a new set
    JBStreamSet* set = build();
    if (!set)
	return false;
    if (!set->add(client)) {
	lock.drop();
	TelEngine::destruct(set);
	return false;
    }
    m_streamCount++;
    m_sets.append(set);
    Debug(m_engine,DebugAll,"JBStreamSetList(%s) added set (%p) count=%u [%p]",
	m_name.c_str(),set,m_sets.count(),this);
    lock.drop();
    if (!set->start())
	TelEngine::destruct(set);
    return true;
}

// Remove a stream from list
void JBStreamSetList::remove(JBStream* client, bool delObj)
{
    if (!client)
	return;
    DDebug(m_engine,DebugAll,"JBStreamSetList(%s) removing (%p,'%s') delObj=%u [%p]",
	m_name.c_str(),client,client->name(),delObj,this);
    Lock lock(this);
    for (ObjList* o = m_sets.skipNull(); o; o = o->skipNext()) {
	if ((static_cast<JBStreamSet*>(o->get()))->remove(client,delObj)) {
	    if (m_streamCount)
		m_streamCount--;
	    return;
	}
    }
}

// Stop one set or all sets
void JBStreamSetList::stop(JBStreamSet* set, bool waitTerminate)
{
    // A set will stop when all its streams will terminate
    // Stop it now if wait is not requested
    Lock lck(this);
    if (set) {
	if (set->m_owner != this)
	    return;
	DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopping set (%p) [%p]",
	    m_name.c_str(),set,this);
	set->dropAll();
	if (!waitTerminate)
	    set->stop();
	lck.drop();
	while (true) {
	    lock();
	    bool ok = (0 == m_sets.find(set));
	    unlock();
	    if (ok)
		break;
	    Thread::yield(!waitTerminate);
	}
	DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopped set (%p) [%p]",
	    m_name.c_str(),set,this);
	return;
    }
    ObjList* o = m_sets.skipNull();
    if (!o)
	return;
    DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopping %u sets [%p]",
	m_name.c_str(),m_sets.count(),this);
    for (; o; o =  o->skipNext()) {
	set = static_cast<JBStreamSet*>(o->get());
	set->dropAll();
	if (!waitTerminate)
	    set->stop();
    }
    lck.drop();
    while (true) {
	lock();
	bool ok = (0 == m_sets.skipNull());
	unlock();
	if (ok)
	    break;
	Thread::yield(!waitTerminate);
    }
    DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopped all sets [%p]",
	m_name.c_str(),this);
}

// Get the string representation of this list
const String& JBStreamSetList::toString() const
{
    return m_name;
}

// Stop all sets. Release memory
void JBStreamSetList::destroyed()
{
    stop(0,true);
    RefObject::destroyed();
}

//Remove a set from list without deleting it
void JBStreamSetList::remove(JBStreamSet* set)
{
    if (!set)
	return;
    Lock lock(this);
    ObjList* o = m_sets.find(set);
    if (!o)
	return;
    o->remove(false);
    Debug(m_engine,DebugAll,"JBStreamSetList(%s) removed set (%p) count=%u [%p]",
	m_name.c_str(),set,m_sets.count(),this);
}

// Build a specialized stream set. Descendants must override this method
JBStreamSet* JBStreamSetList::build()
{
    Debug(m_engine,DebugStub,"JBStreamSetList(%s) build() not implemented! [%p]",
	m_name.c_str(),this);
    return 0;
}


/*
 * JBEntityCapsList
 */

class EntityCapsRequest : public String
{
public:
    inline EntityCapsRequest(const String& id, JBEntityCaps* caps)
	: String(id), m_caps(caps), m_expire(Time::msecNow() + 30000)
	{}
    inline ~EntityCapsRequest()
	{ TelEngine::destruct(m_caps); }
    JBEntityCaps* m_caps;
    u_int64_t m_expire;
private:
    EntityCapsRequest() {}
};

// Expire pending requests
void JBEntityCapsList::expire(u_int64_t msecNow)
{
    if (!m_enable)
	return;
    Lock lock(this);
    // Stop at the first not expired item: the other items are added after it
    for (ObjList* o = m_requests.skipNull(); o; o = o->skipNull()) {
	EntityCapsRequest* r = static_cast<EntityCapsRequest*>(o->get());
	if (r->m_caps && msecNow < r->m_expire)
	    break;
	DDebug(DebugInfo,"JBEntityCapsList request id=%s timed out [%p]",
	    r->toString().c_str(),this);
	o->remove();
    }
}

// Process a response. This method is thread safe
bool JBEntityCapsList::processRsp(XmlElement* rsp, const String& id, bool ok)
{
    if (!(rsp && id && id.startsWith(m_reqPrefix)))
	return false;
    if (!m_enable)
	return true;
    Lock lock(this);
    GenObject* o = m_requests.remove(id,false);
    if (!o) {
	DDebug(DebugInfo,"JBEntityCapsList::processRsp(%p,%s,%u) id not found [%p]",
	    &rsp,id.c_str(),ok,this);
	return true;
    }
    while (ok) {
	XmlElement* query = XMPPUtils::findFirstChild(*rsp,XmlTag::Query);
	if (!(query && XMPPUtils::hasXmlns(*query,XMPPNamespace::DiscoInfo)))
	    break;
	EntityCapsRequest* r = static_cast<EntityCapsRequest*>(o);
	JBEntityCaps* caps = r->m_caps;
	if (!caps)
	    break;
	// Check node (only for XEP 0115 ver >= 1.4)
	if (caps->m_version == JBEntityCaps::Ver1_4) {
	    String* node = query->getAttribute("node");
	    if (node && *node != (caps->m_node + "#" + caps->m_data)) {
		DDebug(DebugAll,"JBEntityCapsList response with invalid node=%s [%p]",
		    node->c_str(),this);
		break;
	    }
	}
	caps->m_features.fromDiscoInfo(*query);
	// Check hash
	if (caps->m_version == JBEntityCaps::Ver1_4) {
	    caps->m_features.updateEntityCaps();
	    if (caps->m_data != caps->m_features.m_entityCapsHash) {
		DDebug(DebugAll,"JBEntityCapsList response with invalid hash=%s (expected=%s) [%p]",
		    caps->m_features.m_entityCapsHash.c_str(),caps->m_data.c_str(),this);
		break;
	    }
	}
	r->m_caps = 0;
	// OK
	append(caps);
	capsAdded(caps);
	break;
    }
    TelEngine::destruct(o);
    return true;
}

// Request entity capabilities.
void JBEntityCapsList::requestCaps(JBStream* stream, const char* from, const char* to,
    const String& id, char version, const char* node, const char* data)
{
    if (!stream)
	return;
    Lock lock(this);
    // Make sure we don't send another disco info for the same id
    for (ObjList* o = m_requests.skipNull(); o; o = o->skipNext()) {
	EntityCapsRequest* r = static_cast<EntityCapsRequest*>(o->get());
	if (r->m_caps && id == r->m_caps)
	    return;
    }
    String reqId;
    reqId << m_reqPrefix << ++m_reqIndex;
    m_requests.append(new EntityCapsRequest(reqId,new JBEntityCaps(id,version,node,data)));
    lock.drop();
    XmlElement* d = 0;
    if (version == JBEntityCaps::Ver1_4)
	d = XMPPUtils::createIqDisco(true,true,from,to,reqId,node,data);
    else
	d = XMPPUtils::createIqDisco(true,true,from,to,reqId);
    DDebug(DebugAll,"JBEntityCapsList sending request to=%s node=%s id=%s [%p]",
	to,node,reqId.c_str(),this);
    stream->sendStanza(d);
}

// Build a document from this list
XmlDocument* JBEntityCapsList::toDocument(const char* rootName)
{
    Lock lock(this);
    XmlDocument* doc = new XmlDocument;
    XmlDeclaration* decl = new XmlDeclaration;
    XmlSaxParser::Error err = doc->addChild(decl);
    if (err != XmlSaxParser::NoError)
	TelEngine::destruct(decl);
    XmlComment* info = new XmlComment("Generated jabber entity capabilities cache");
    err = doc->addChild(info);
    if (err != XmlSaxParser::NoError)
	TelEngine::destruct(info);
    XmlElement* root = new XmlElement(rootName);
    err = doc->addChild(root);
    if (err != XmlSaxParser::NoError) {
	TelEngine::destruct(root);
	return doc;
    }
    for (ObjList* o = skipNull(); o; o = o->skipNext()) {
	JBEntityCaps* caps = static_cast<JBEntityCaps*>(o->get());
	XmlElement* item = new XmlElement(s_entityCapsItem);
	item->setAttribute("id",caps->c_str());
	item->setAttribute("version",String((int)caps->m_version));
	item->setAttribute("node",caps->m_node);
	item->setAttribute("data",caps->m_data);
	caps->m_features.add(*item);
	doc->addChild(item);
    }
    return doc;
}

// Build this list from an XML document
void JBEntityCapsList::fromDocument(XmlDocument& doc, const char* rootName)
{
    Lock lock(this);
    clear();
    m_requests.clear();
    XmlElement* root = doc.root();
    if (!root || (!TelEngine::null(rootName) && root->toString() != rootName)) {
	DDebug(DebugAll,"JBEntityCapsList invalid document root %p '%s' (expected=%s) [%p]",
	    root,root ? root->tag() : "",rootName,this);
	return;
    }
    XmlElement* item = root->findFirstChild(&s_entityCapsItem);
    for (; item; item = root->findNextChild(item,&s_entityCapsItem)) {
	String* id = item->getAttribute("id");
	if (TelEngine::null(id))
	    continue;
	String* tmp = item->getAttribute("version");
	JBEntityCaps* cap = new JBEntityCaps(*id,tmp ? tmp->toInteger(-1) : -1,
	    item->attribute("node"),item->attribute("data"));
	cap->m_features.fromDiscoInfo(*item);
	append(cap);
    }
    capsAdded(0);
}

// Process an element containing an entity capabily child.
// Request capabilities if not found in the list
bool JBEntityCapsList::processCaps(String& capsId, XmlElement* xml, JBStream* stream,
    const char* from, const char* to)
{
    if (!(m_enable && xml))
	return false;
    char version = 0;
    String* node = 0;
    String* ver = 0;
    String* ext = 0;
    if (!decodeCaps(*xml,version,node,ver,ext))
	return false;
    JBEntityCaps::buildId(capsId,version,*node,*ver,ext);
    Lock lock(this);
    JBEntityCaps* caps = findCaps(capsId);
    if (caps)
	return true;
    // Hack for google (doesn't support disco info, supports only disco info with node)
    if (version == JBEntityCaps::Ver1_3 &&
	(*node == s_googleTalkNode || *node == s_googleMailNode ||
	*node == s_googleAndroidNode || *node == s_googleAndroidNode2)) {
	caps = new JBEntityCaps(capsId,version,*node,*ver);
	if (ext) {
	    ObjList* list = ext->split(' ',false);
	    if (list->find("voice-v1")) {
		caps->m_features.add(XMPPNamespace::JingleSession);
		caps->m_features.add(XMPPNamespace::JingleAudio);
	    }
	    TelEngine::destruct(list);
	}
	append(caps);
	capsAdded(caps);
	return true;
    }
    if (stream)
	requestCaps(stream,from,to,capsId,version,*node,*ver);
    return stream != 0;
}

// Add capabilities to a list.
void JBEntityCapsList::addCaps(NamedList& list, JBEntityCaps& caps)
{
#define CHECK_NS(ns,param) \
    if (caps.hasFeature(ns)) { \
	params->append(param,","); \
	list.addParam(param,String::boolText(true)); \
    }
    int jingleVersion = -1;
    if (caps.m_features.get(XMPPNamespace::Jingle))
	jingleVersion = 1;
    else if (caps.m_features.get(XMPPNamespace::JingleSession) ||
	caps.m_features.get(XMPPNamespace::JingleVoiceV1))
	jingleVersion = 0;
    NamedString* params = new NamedString("caps.params");
    list.addParam("caps.id",caps.toString());
    list.addParam(params);
    if (jingleVersion != -1) {
	params->append("caps.jingle_version");
	list.addParam("caps.jingle_version",String(jingleVersion));
	if (caps.hasAudio()) {
	    params->append("caps.audio",",");
	    list.addParam("caps.audio",String::boolText(true));
	}
	switch (jingleVersion) {
	    case 1:
		CHECK_NS(XMPPNamespace::JingleTransfer,"caps.calltransfer");
		CHECK_NS(XMPPNamespace::JingleAppsFileTransfer,"caps.filetransfer");
		break;
	    case 0:
		break;
	}
	CHECK_NS(XMPPNamespace::FileInfoShare,"caps.fileinfoshare");
	CHECK_NS(XMPPNamespace::ResultSetMngt,"caps.resultsetmngt");
    }
    CHECK_NS(XMPPNamespace::Muc,"caps.muc");
#undef CHECK_NS
}

// Load (reset) this list from an XML document file.
bool JBEntityCapsList::loadXmlDoc(const char* file, DebugEnabler* enabler)
{
    if (!m_enable)
	return false;
    XmlDocument d;
    int io = 0;
    DDebug(enabler,DebugAll,"Loading entity caps from '%s'",file);
    XmlSaxParser::Error err = d.loadFile(file,&io);
    if (err == XmlSaxParser::NoError) {
	fromDocument(d);
	return true;
    }
    String error;
    if (err == XmlSaxParser::IOError) {
	String tmp;
	Thread::errorString(tmp,io);
	error << " " << io << " '" << tmp << "'";
    }
    Debug(enabler,DebugNote,"Failed to load entity caps from '%s': %s%s",
	file,XmlSaxParser::getError(err),error.safe());
    return false;
}

// Save this list to an XML document file.
bool JBEntityCapsList::saveXmlDoc(const char* file, DebugEnabler* enabler)
{
    DDebug(enabler,DebugAll,"Saving entity caps to '%s'",file);
    if (TelEngine::null(file))
	return false;
    XmlDocument* doc = toDocument();
    int res = doc->saveFile(file,true,"  ");
    if (res)
	Debug(enabler,DebugNote,"Failed to save entity caps to '%s'",file);
    delete doc;
    return res == 0;
}

// Check if an XML element has a 'c' entity capability child and process it
bool JBEntityCapsList::decodeCaps(const XmlElement& xml, char& version, String*& node,
    String*& ver, String*& ext)
{
    // Find the first entity caps child with valid node and ext
    XmlElement* c = 0;
    while (true) {
	c = XMPPUtils::findNextChild(xml,c,XmlTag::EntityCapsTag,
	    XMPPNamespace::EntityCaps);
	if (!c)
	    break;
	if (TelEngine::null(c->getAttribute("node")) ||
	    TelEngine::null(c->getAttribute("ver")))
	    continue;
	break;
    }
    if (!c)
	return false;
    // Check for a subsequent child with new entity caps if the first one is an old version
    if (!c->getAttribute("hash")) {
	XmlElement* s = c;
	while (true) {
	    s = XMPPUtils::findNextChild(xml,s,XmlTag::EntityCapsTag,
		XMPPNamespace::EntityCaps);
	    if (!s)
		break;
	    if (!s->getAttribute("hash") ||
		TelEngine::null(s->getAttribute("node")) ||
		TelEngine::null(s->getAttribute("ver")))
		continue;
	    c = s;
	    break;
	}
    }
    node = c->getAttribute("node");
    ver = c->getAttribute("ver");
    String* hash = c->getAttribute("hash");
    if (hash) {
	// Version 1.4 or greater
	if (*hash != "sha-1")
	    return false;
	version = JBEntityCaps::Ver1_4;
	ext = 0;
    }
    else {
	version = JBEntityCaps::Ver1_3;
	ext = c->getAttribute("ext");
    }
    return true;
}

/* vi: set ts=8 sw=4 sts=4 noet: */
