yate/libs/yjingle/jbstream.cpp

2385 lines
71 KiB
C++

/**
* jbstream.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-2006 Null Team
*
* 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.
*
* 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. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <yatejabber.h>
#include <yatemime.h>
#include <string.h>
#include <stdio.h>
using namespace TelEngine;
static XMPPNamespace s_ns; // Just use the operators
static XMPPError s_err;
static String s_qop = "auth"; // Used to build Digest MD5 SASL
static TokenDict s_streamState[] = {
{"Idle", JBStream::Idle},
{"Connecting", JBStream::Connecting},
{"Started", JBStream::Started},
{"Securing", JBStream::Securing},
{"Register", JBStream::Register},
{"Auth", JBStream::Auth},
{"Running", JBStream::Running},
{"Destroy", JBStream::Destroy},
{0,0},
};
TokenDict JBStream::s_flagName[] = {
{"autorestart", AutoRestart},
{"noversion1", NoVersion1},
{"noremoteversion1", NoRemoteVersion1},
{"tls", UseTls},
{"sasl", UseSasl},
{"secured", StreamSecured},
{"authenticated", StreamAuthenticated},
{"allowplainauth", AllowPlainAuth},
{"allowunsafesetup", AllowUnsafeSetup},
{0,0}
};
static String s_version = "1.0"; // Protocol version
static String s_declaration = "<?xml version='1.0' encoding='UTF-8'?>";
// Utility: append a quoted param to a string
inline void appendQParam(String& dest, const char* name, const char* value,
bool quotes, bool first = false)
{
if (!first)
dest << ",";
dest << name << "=";
if (quotes)
dest << "\"" << value << "\"";
else
dest << value;
}
// Check if an element exists and have an expected namespace
inline bool checkValidXmlns(XMLElement* e, XMPPNamespace::Type ns,
XMPPError::Type& error)
{
if (e && XMPPUtils::hasXmlns(*e,ns))
return true;
error = e ? XMPPError::SFeatureNotImpl : XMPPError::SBadRequest;
return false;
}
#define DROP_AND_EXIT { dropXML(xml); return; }
#define INVALIDXML_AND_EXIT(code,reason) { invalidStreamXML(xml,code,reason); return; }
#define ERRORXML_AND_EXIT { errorStreamXML(xml); return; }
/**
* JBSocket
*/
JBSocket::JBSocket(JBEngine* engine, JBStream* stream,
const char* address, int port)
: m_engine(engine), m_stream(stream), m_socket(0), m_remoteDomain(address),
m_address(PF_INET),
m_streamMutex(true,"JBSocket::stream"),
m_receiveMutex(true,"JBSocket::receive")
{
m_address.port(port);
}
// Connect the socket
bool JBSocket::connect(bool& terminated, const char* newAddr, int newPort)
{
terminate();
Lock2 lck1(m_streamMutex,m_receiveMutex);
m_socket = new Socket(PF_INET,SOCK_STREAM);
// Set new connection data. Resolve remote domain
if (newAddr)
m_remoteDomain = newAddr;
if (newPort)
m_address.port(newPort);
lck1.drop();
m_address.host(m_remoteDomain);
Thread::check(true);
if (!m_address.host()) {
m_error = "Resolver failure";
DDebug(m_engine,DebugWarn,"Stream. Failed to resolve '%s' [%p]",
m_remoteDomain.safe(),m_stream);
terminated = (m_socket == 0);
terminate();
return false;
}
DDebug(m_engine,DebugInfo,"Stream. Attempt to connect to '%s:%d' [%p]",
m_address.host().safe(),m_address.port(),m_stream);
terminated = false;
bool res = m_socket && m_socket->connect(m_address);
Thread::check(true);
// Lock again to update data
Lock2 lck2(m_streamMutex,m_receiveMutex);
bool ok = false;
while (true) {
if (!m_socket) {
Debug(m_engine,DebugMild,
"Stream. Socket deleted while connecting [%p]",m_stream);
terminated = true;
break;
}
// Check connect result
if (!res) {
m_error = "";
Thread::errorString(m_error,m_socket->error());
if (m_error.null())
m_error = "Socket connect failure";
Debug(m_engine,DebugWarn,
"Stream. Failed to connect socket to '%s:%d'. %d: '%s' [%p]",
m_address.host().c_str(),m_address.port(),
m_socket->error(),m_error.c_str(),m_stream);
break;
}
// Connected
ok = true;
m_socket->setBlocking(false);
DDebug(m_engine,DebugAll,"Stream. Connected to '%s:%d'. [%p]",
m_address.host().c_str(),m_address.port(),m_stream);
break;
}
lck2.drop();
if (!ok)
terminate();
return ok;
}
// Close the socket
void JBSocket::terminate(bool shutdown)
{
Lock2 lck(m_streamMutex,m_receiveMutex);
if (!m_socket)
return;
Socket* tmp = m_socket;
m_socket = 0;
Debug(m_engine,DebugInfo,
"Stream. Terminating socket shutdown=%s error=%s [%p]",
String::boolText(shutdown),m_error.c_str(),m_stream);
m_error = "";
lck.drop();
if (shutdown)
tmp->shutdown(true,true);
else {
tmp->setLinger(-1);
tmp->terminate();
}
delete tmp;
}
// Read data from socket
bool JBSocket::recv(char* buffer, unsigned int& len)
{
if (!valid()) {
if (m_error.null())
m_error = "Socket read failure";
return false;
}
int read = m_socket->readData(buffer,len);
if (read != Socket::socketError()) {
if (!read)
Debug(m_engine,DebugInfo,"Stream EOF [%p]",this);
#ifdef XDEBUG
else {
String s(buffer,read);
Debug(m_engine,DebugAll,"Stream recv [%p]\r\n%s",
m_stream,s.c_str());
}
#endif
len = read;
return (len != 0);
}
len = 0;
if (!m_socket->canRetry()) {
m_error = ::strerror(m_socket->error());
if (m_error.null())
m_error = "Socket read failure";
Debug(m_engine,DebugWarn,
"Stream. Socket read error: %d: '%s' [%p]",
m_socket->error(),::strerror(m_socket->error()),m_stream);
return false;
}
return true;
}
// Write data to socket
bool JBSocket::send(const char* buffer, unsigned int& len)
{
if (!valid()) {
if (m_error.null())
m_error = "Socket write failure";
return false;
}
XDebug(m_engine,DebugAll,"Stream sending %s [%p]",buffer,m_stream);
int c = m_socket->writeData(buffer,len);
if (c != Socket::socketError()) {
len = c;
return true;
}
if (!m_socket->canRetry()) {
m_error = ::strerror(m_socket->error());
if (m_error.null())
m_error = "Socket write failure";
Debug(m_engine,DebugWarn,"Stream. Socket send error: %d: '%s' [%p]",
m_socket->error(),::strerror(m_socket->error()),m_stream);
return false;
}
len = 0;
DDebug(m_engine,DebugMild,
"Stream. Socket temporary unavailable to send: %d: '%s' [%p]",
m_socket->error(),::strerror(m_socket->error()),m_stream);
return true;
}
/**
* JBStream
*/
// Outgoing
JBStream::JBStream(JBEngine* engine, int type, XMPPServerInfo& info,
const JabberID& localJid, const JabberID& remoteJid)
: m_password(info.password()), m_flags(0), m_challengeCount(2),
m_waitState(WaitIdle), m_authMech(JIDFeatureSasl::MechNone), m_register(false), m_type(type),
m_state(Idle), m_outgoing(true), m_restart(0), m_restartMax(0),
m_timeToFillRestart(0), m_setupTimeout(0), m_idleTimeout(0),
m_local(localJid.node(),localJid.domain(),localJid.resource()),
m_remote(remoteJid.node(),remoteJid.domain(),remoteJid.resource()),
m_engine(engine), m_socket(engine,0,info.address(),info.port()),
m_lastEvent(0), m_terminateEvent(0), m_startEvent(0), m_recvCount(-1),
m_streamXML(0), m_declarationSent(0), m_nonceCount(0), m_registerId(0)
{
m_socket.m_stream = this;
if (!engine) {
Debug(DebugNote,"Can't create stream without engine [%p]",this);
return;
}
// Update options from server info
if (!info.flag(XMPPServerInfo::NoAutoRestart))
m_flags |= AutoRestart;
// Stream version supported by server
if (info.flag(XMPPServerInfo::OldStyleAuth))
m_flags |= NoVersion1;
else {
// Force stream encryption if required by config
if (info.flag(XMPPServerInfo::TlsRequired))
m_flags |= UseTls;
// Use RFC-3920 SASL instead of XEP-0078 authentication
m_flags |= UseSasl;
}
// Allow plain auth
if (info.flag(XMPPServerInfo::AllowPlainAuth))
m_flags |= AllowPlainAuth;
// Allow unsecure user registration
if (info.flag(XMPPServerInfo::AllowUnsafeSetup))
m_flags |= AllowUnsafeSetup;
// Restart counter and update interval
if (flag(AutoRestart))
m_restartMax = m_restart = engine->m_restartCount;
else
m_restartMax = m_restart = 1;
m_timeToFillRestart = Time::msecNow() + engine->m_restartUpdateInterval;
if (m_engine->debugAt(DebugAll)) {
String f;
XMPPUtils::buildFlags(f,m_flags,s_flagName);
Debug(m_engine,DebugAll,
"Stream dir=outgoing type=%s local=%s remote=%s options=%s [%p]",
JBEngine::lookupProto(m_type),m_local.safe(),m_remote.safe(),
f.c_str(),this);
}
}
JBStream::~JBStream()
{
XDebug(m_engine,DebugAll,"JBStream::~JBStream() [%p]",this);
}
// Close the stream. Release memory
void JBStream::destroyed()
{
XDebug(m_engine,DebugAll,"Stream::destroyed() state=%s [%p]",lookupState(state()),this);
if (m_engine) {
Lock lock(m_engine);
m_engine->m_streams.remove(this,false);
}
terminate(false,0,XMPPError::NoError,0,false,true);
// m_terminateEvent shouldn't be valid:
// do that to print a DebugFail output for the stream inside the event
TelEngine::destruct(m_terminateEvent);
TelEngine::destruct(m_startEvent);
DDebug(m_engine,DebugAll,"Stream destroyed local=%s remote=%s [%p]",
m_local.safe(),m_remote.safe(),this);
RefObject::destroyed();
}
// Check the 'to' attribute of a received element
bool JBStream::checkDestination(XMLElement* xml, bool& respond)
{
respond = false;
return true;
}
// Connect the stream
void JBStream::connect()
{
Lock2 lck(m_socket.m_streamMutex,m_socket.m_receiveMutex);
if (state() != Idle) {
Debug(m_engine,state()!=Connecting?DebugNote:DebugAll,
"Stream. Attempt to connect when not idle [%p]",this);
return;
}
DDebug(m_engine,DebugInfo,
"Stream. Attempt to connect local=%s remote=%s count=%u [%p]",
m_local.safe(),m_remote.safe(),m_restart,this);
// Check if we can restart. Destroy the stream if not auto restarting
if (m_restart)
m_restart--;
else
return;
// Reset data
m_id = "";
m_parser.reset();
lck.drop();
// Re-connect socket
bool terminated = false;
changeState(Connecting);
// TODO: check with the engine if server info is available
// get address and port and pass them to socket
if (!m_socket.connect(terminated,0,0)) {
if (!terminated)
terminate(false,0,XMPPError::HostGone,m_socket.error(),false);
return;
}
Debug(m_engine,DebugAll,"Stream. local=%s remote=%s connected to %s:%d [%p]",
m_local.safe(),m_remote.safe(),addr().host().safe(),addr().port(),this);
m_setupTimeout = 0;
if (m_engine && m_engine->m_streamSetupInterval)
m_setupTimeout = Time::msecNow() + m_engine->m_streamSetupInterval;
// Send stream start
sendStreamStart();
}
// Read data from socket and pass it to the parser
// Terminate stream on parser or socket error
bool JBStream::receive()
{
char buf[1024];
if (!m_recvCount || state() == Securing || state() == Destroy ||
state() == Idle || state() == Connecting)
return false;
XMPPError::Type error = XMPPError::NoError;
bool send = false;
// Lock between start read and end consume to serialize input
m_socket.m_receiveMutex.lock();
const char* text = 0;
unsigned int len = (m_recvCount < 0 ? sizeof(buf) : 1);
if (m_socket.recv(buf,len)) {
if (len) {
XDebug(m_engine,DebugAll,"Stream. Received %u bytes [%p]",len,this);
if (!m_parser.consume(buf,len)) {
bool full = (m_parser.bufLen() > m_parser.s_maxDataBuffer);
text = m_parser.ErrorDesc();
// Don't display the buffer if full
String tmp;
if (!full)
m_parser.getBuffer(tmp);
else
tmp << "overflow len=" << m_parser.bufLen() << " max=" <<
m_parser.s_maxDataBuffer;
Debug(m_engine,DebugNote,"Stream. Parser error='%s' buffer='%s' [%p]",
text,tmp.c_str(),this);
error = full ? XMPPError::Internal : XMPPError::Xml;
send = true;
}
else
startIdleTimer();
// Check if the parser consumed all it's buffer and the stream
// will start TLS
if (!m_parser.bufLen() && m_recvCount > 0)
setRecvCount(0);
}
}
else {
error = XMPPError::HostGone;
text = "remote server not found";
}
m_socket.m_receiveMutex.unlock();
if (error != XMPPError::NoError)
terminate(false,0,error,text,send);
return len != 0;
}
// Send a stanza
JBStream::Error JBStream::sendStanza(XMLElement* stanza, const char* senderId)
{
if (!stanza)
return ErrorContext;
Lock lock(m_socket.m_streamMutex);
if (state() == Destroy) {
Debug(m_engine,DebugNote,
"Stream. Can't send stanza (%p,%s). Stream is destroying [%p]",
stanza,stanza->name(),this);
TelEngine::destruct(stanza);
return ErrorContext;
}
DDebug(m_engine,DebugAll,"Stream. Posting stanza (%p,%s) id='%s' [%p]",
stanza,stanza->name(),senderId,this);
XMLElementOut* e = new XMLElementOut(stanza,senderId);
// List not empty: the return value will be ErrorPending
// Else: element will be sent
bool pending = (0 != m_outXML.skipNull());
m_outXML.append(e);
// Send first element
Error result = sendPending();
return pending ? ErrorPending : result;
}
// Extract an element from parser and construct an event
JBEvent* JBStream::getEvent(u_int64_t time)
{
Lock lock(m_socket.m_streamMutex);
if (m_lastEvent)
return 0;
if (!m_engine) {
Debug(DebugMild,"Stream. Engine vanished. Can't live as orphan [%p]",this);
terminate(true,0,XMPPError::Internal,"Engine is missing",false);
if (m_terminateEvent) {
m_lastEvent = m_terminateEvent;
m_terminateEvent = 0;
}
return m_lastEvent;
}
// Increase stream restart counter if it's time to and should auto restart
if (flag(AutoRestart) && m_timeToFillRestart < time) {
m_timeToFillRestart = time + m_engine->m_restartUpdateInterval;
if (m_restart < m_restartMax) {
m_restart++;
Debug(m_engine,DebugAll,"Stream. restart count=%u max=%u [%p]",
m_restart,m_restartMax,this);
}
}
// Do nothing if destroying or connecting
// Just check Terminated or Running events
// Idle: check if we can restart. Destroy the stream if not auto restarting
if (state() == Idle || state() == Destroy || state() == Connecting) {
if (state() == Idle) {
if (m_restart) {
lock.drop();
m_engine->connect(this);
return 0;
}
if (!flag(AutoRestart))
terminate(true,0,XMPPError::NoError,"connection-failed",false);
}
if (m_terminateEvent) {
m_lastEvent = m_terminateEvent;
m_terminateEvent = 0;
}
else if (m_startEvent) {
m_lastEvent = m_startEvent;
m_startEvent = 0;
}
return m_lastEvent;
}
while (true) {
if (m_terminateEvent)
break;
// Send pending elements and process the received ones
sendPending();
if (m_terminateEvent)
break;
// Process the received XML
XMLElement* xml = m_parser.extract();
if (!xml)
break;
// Print it
m_engine->printXml(*xml,this,false);
// Check destination
bool respond = false;
if (!checkDestination(xml,respond)) {
String type = xml->getAttribute("type");
Debug(m_engine,DebugNote,
"Stream. Received %s with unacceptable destination to=%s type=%s [%p]",
xml->name(),xml->getAttribute("to"),type.c_str(),this);
if (state() == Running)
if (respond)
switch (xml->type()) {
case XMLElement::Iq:
case XMLElement::Presence:
case XMLElement::Message:
if (type != "error" && type != "result") {
sendStanza(XMPPUtils::createError(xml,XMPPError::TypeModify,
XMPPError::HostUnknown,"Unknown destination"));
break;
}
default:
dropXML(xml);
}
else
dropXML(xml);
else if (respond || this->type() == JBEngine::Client)
invalidStreamXML(xml,XMPPError::HostUnknown,"Received invalid destination");
break;
}
// Check if stream end was received (end tag or error)
if (xml->type() == XMLElement::StreamEnd ||
xml->type() == XMLElement::StreamError) {
Debug(m_engine,DebugAll,"Stream. Remote closed in state %s [%p]",
lookupState(state()),this);
terminate(false,xml,XMPPError::NoError,xml->getText(),false);
break;
}
XDebug(m_engine,DebugAll,"Stream. Processing (%p,%s) in state %s [%p]",
xml,xml->name(),lookupState(state()),this);
switch (state()) {
case Running:
processRunning(xml);
break;
case Auth:
processAuth(xml);
break;
case Securing:
processSecuring(xml);
break;
case Started:
// Set stream id if not already set
if (!m_id) {
if (xml->type() != XMLElement::StreamStart) {
dropXML(xml);
break;
}
m_id = xml->getAttribute("id");
if (!m_id || m_engine->checkDupId(this)) {
invalidStreamXML(xml,XMPPError::InvalidId,"Duplicate stream id");
break;
}
DDebug(m_engine,DebugAll,"Stream. Id set to '%s' [%p]",
m_id.c_str(),this);
}
processStarted(xml);
break;
case Register:
processRegister(xml);
break;
default:
Debug(m_engine,DebugStub,"Unhandled stream state %u '%s' [%p]",
state(),lookupState(state()),this);
TelEngine::destruct(xml);
}
break;
}
// Return terminate event if set
// Get events from queue if not set to terminate
if (m_terminateEvent) {
m_lastEvent = m_terminateEvent;
m_terminateEvent = 0;
}
else if (m_startEvent) {
m_lastEvent = m_startEvent;
m_startEvent = 0;
}
else {
ObjList* obj = m_events.skipNull();
m_lastEvent = obj ? static_cast<JBEvent*>(obj->get()) : 0;
if (m_lastEvent)
m_events.remove(m_lastEvent,false);
}
// Check timers if no events
if (!m_lastEvent) {
if (m_idleTimeout && time > m_idleTimeout) {
if (startIdleTimer(time)) {
DDebug(m_engine,DebugAll,"Stream. Sending keep alive in state %s [%p]",
lookupState(state()),this);
const char* keepAlive = "\t";
unsigned int l = 1;
if (!m_socket.send(keepAlive,l)) {
// Keep the reason: terminate() might override the last socket error
String reason = m_socket.m_error;
terminate(false,0,XMPPError::HostGone,reason,true);
}
}
}
else if (m_setupTimeout && time > m_setupTimeout) {
Debug(m_engine,DebugNote,"Stream. Setup timed out in state %s [%p]",
lookupState(state()),this);
terminate(true,0,XMPPError::ConnTimeout,"Connection timeout",true);
}
if (m_terminateEvent) {
m_lastEvent = m_terminateEvent;
m_terminateEvent = 0;
}
}
#ifdef DEBUG
if (m_lastEvent)
Debug(m_engine,DebugAll,"Stream. Raising event (%p,%s) [%p]",
m_lastEvent,m_lastEvent->name(),this);
#endif
return m_lastEvent;
}
// Terminate stream. Send stream end tag or error. Remove pending stanzas without id
// Deref stream if destroying
void JBStream::terminate(bool destroy, XMLElement* recvStanza, XMPPError::Type error,
const char* reason, bool send, bool final, bool sendError)
{
XDebug(m_engine,DebugAll,"Stream::terminate(%u,%p,%u,%s,%u,%u) state=%s [%p]",
destroy,recvStanza,error,reason,send,final,lookupState(state()),this);
Lock2 lock(m_socket.m_streamMutex,m_socket.m_receiveMutex);
if (!flag(AutoRestart))
destroy = true;
setRecvCount(-1);
m_nonceCount = 0;
m_setupTimeout = m_idleTimeout = 0;
TelEngine::destruct(m_startEvent);
if (m_streamXML) {
if (m_streamXML->dataCount())
send = false;
TelEngine::destruct(m_streamXML);
}
if (state() == Destroy) {
resetStream();
m_socket.terminate(true);
TelEngine::destruct(recvStanza);
return;
}
if (error == XMPPError::NoError && m_engine->exiting()) {
error = XMPPError::Shutdown;
reason = 0;
}
Debug(m_engine,DebugAll,
"Stream. Terminate state=%s destroy=%u error=%s reason='%s' final=%u [%p]",
lookupState(state()),destroy,s_err[error],reason,final,this);
// Send ending stream element
if (send && state() != Connecting && state() != Idle) {
XMLElement* e = 0;
if (sendError && error != XMPPError::NoError) {
e = XMPPUtils::createStreamError(error,reason);
// Add received element
// Preserve received element: we might generate an event
if (recvStanza)
e->addChild(new XMLElement(*recvStanza));
}
bool ok = e ? sendStreamXML(e,m_state) : true;
if (ok)
sendStreamXML(new XMLElement(XMLElement::StreamEnd),m_state);
}
m_socket.terminate(state() == Connecting);
// Done if called from destructor
if (final) {
changeState(Destroy);
resetStream();
TelEngine::destruct(recvStanza);
return;
}
// Cancel all outgoing elements without id
removePending(false,0,true);
// Always set termination event, except when exiting
if (!(m_terminateEvent || m_engine->exiting())) {
if (!recvStanza && error != XMPPError::NoError)
recvStanza = XMPPUtils::createStreamError(error,reason);
Debug(m_engine,DebugAll,"Stream. Set terminate error=%s reason=%s [%p]",
s_err[error],reason,this);
m_terminateEvent = new JBEvent(destroy?JBEvent::Destroy:JBEvent::Terminated,
this,recvStanza);
if (m_terminateEvent->m_text.null())
m_terminateEvent->m_text = reason;
recvStanza = 0;
}
TelEngine::destruct(recvStanza);
// Terminate
changeState(destroy ? Destroy : Idle);
resetStream();
if (destroy) {
DDebug(m_engine,DebugAll,"Stream::terminate() deref() in state=%s [%p]",
lookupState(state()),this);
lock.drop();
deref();
}
}
// Get an object from this stream
void* JBStream::getObject(const String& name) const
{
if (name == "Socket*")
return state() == Securing ? (void*)&m_socket.m_socket : 0;
if (name == "JBStream")
return (void*)this;
return RefObject::getObject(name);
}
// Get the name of a stream state
const char* JBStream::lookupState(int state)
{
return lookup(state,s_streamState);
}
// Get the starting stream element to be sent after stream connected
XMLElement* JBStream::getStreamStart()
{
m_remoteFeatures.clear();
m_parser.reset();
m_waitState = WaitStart;
XMLElement* start = XMPPUtils::createElement(XMLElement::StreamStart,
XMPPNamespace::Client);
start->setAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]);
start->setAttribute("to",remote());
// Add version to notify the server we support RFC3920 TLS/SASL authentication
if (!flag(NoVersion1))
start->setAttribute("version",s_version);
return start;
}
// Get the authentication element to be sent when authentication starts
XMLElement* JBStream::getAuthStart()
{
XMLElement* xml = 0;
// Deprecated XEP-0078 authentication
if (!flag(UseSasl)) {
xml = XMPPUtils::createIq(XMPPUtils::IqGet,0,0,"auth_1");
xml->addChild(XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::IqAuth));
m_waitState = WaitChallenge;
return xml;
}
// RFC 3920 SASL
if (m_authMech != JIDFeatureSasl::MechMD5 &&
m_authMech != JIDFeatureSasl::MechPlain)
return 0;
String rsp;
// MD5: send auth element, wait challenge
// Plain auth: send auth element with credentials and wait response (success/failure)
if (m_authMech == JIDFeatureSasl::MechMD5)
m_waitState = WaitChallenge;
else {
buildSaslResponse(rsp);
m_waitState = WaitResponse;
}
xml = XMPPUtils::createElement(XMLElement::Auth,XMPPNamespace::Sasl,rsp);
xml->setAttribute("mechanism",lookup(m_authMech,JIDFeatureSasl::s_authMech));
return xml;
}
// Process received data while running
void JBStream::processRunning(XMLElement* xml)
{
XDebug(m_engine,DebugAll,"JBStream::processRunning('%s') [%p]",xml->name(),this);
switch (xml->type()) {
case XMLElement::Message:
m_events.append(new JBEvent(JBEvent::Message,this,xml));
return;
case XMLElement::Presence:
m_events.append(new JBEvent(JBEvent::Presence,this,xml));
return;
case XMLElement::Iq:
break;
default:
m_events.append(new JBEvent(JBEvent::Unhandled,this,xml));
return;
}
XMPPError::Type error = XMPPError::NoError;
int iq = XMPPUtils::iqType(xml->getAttribute("type"));
JBEvent* ev = getIqEvent(xml,iq,error);
if (ev) {
m_events.append(ev);
return;
}
if (error == XMPPError::NoError) {
m_events.append(new JBEvent(JBEvent::Unhandled,this,xml));
return;
}
// Don't respond to error or result
if (iq == XMPPUtils::IqError || iq == XMPPUtils::IqResult) {
dropXML(xml);
return;
}
// Send error
sendStanza(XMPPUtils::createError(xml,XMPPError::TypeModify,error));
}
// Process a received element in Securing state
void JBStream::processSecuring(XMLElement* xml)
{
Debug(m_engine,DebugInfo,"Stream. Received '%s' while securing the stream [%p]",
xml->name(),this);
dropXML(xml);
}
// Process a received element in Register state
void JBStream::processRegister(XMLElement* xml)
{
XDebug(m_engine,DebugAll,"JBStream::processRegister('%s') [%p]",xml->name(),this);
XMPPUtils::IqType type = XMPPUtils::iqType(xml->getAttribute("type"));
String tmp = xml->getAttribute("id");
unsigned int id = tmp.toInteger();
if (xml->type() != XMLElement::Iq ||
(type != XMPPUtils::IqResult && type != XMPPUtils::IqError) ||
id != m_registerId) {
terminate(true,xml,XMPPError::UndefinedCondition,"Unacceptable response",true);
return;
}
// Check for errors
if (type == XMPPUtils::IqError) {
String error, text;
XMPPUtils::decodeError(xml,error,text);
delete xml;
terminate(true,0,XMPPError::UndefinedCondition,text?text:error,false);
return;
}
XMLElement* query = 0;
XMPPError::Type err = XMPPError::NoError;
const char* reason = "Invalid response";
#define SETERR_BREAK(e,r) { err = e; reason = r; break; }
// Wait for register query response
if (m_registerId == 1)
while (true) {
XMLElement* query = xml->findFirstChild(XMLElement::Query);
if (!query)
SETERR_BREAK(XMPPError::UndefinedCondition,reason);
if (!XMPPUtils::hasXmlns(*query,XMPPNamespace::IqRegister))
SETERR_BREAK(XMPPError::InvalidNamespace,reason);
// Check if already registered
XMLElement* r = query->findFirstChild(XMLElement::Registered);
if (r) {
m_register = false;
TelEngine::destruct(r);
break;
}
// Check if we have username/password
XMLElement* user = query->findFirstChild(XMLElement::Username);
XMLElement* pwd = query->findFirstChild(XMLElement::Password);
bool ok = (user && pwd);
TelEngine::destruct(user);
TelEngine::destruct(pwd);
if (!ok)
SETERR_BREAK(XMPPError::UndefinedCondition,"Unsupported method");
// Send credential
m_registerId = 2;
XMLElement* q = XMPPUtils::createRegisterQuery(0,0,
String(m_registerId),m_local.node(),m_password);
sendStreamXML(q,state());
break;
}
// Registered
else if (m_registerId == 2)
m_register = false;
else {
dropXML(xml);
xml = 0;
}
#undef SETERR_BREAK
TelEngine::destruct(query);
if (xml)
TelEngine::destruct(xml);
if (!m_register)
startAuth();
else if (err != XMPPError::NoError)
terminate(true,0,err,reason,true);
}
// Process a received element in Auth state
void JBStream::processAuth(XMLElement* xml)
{
XDebug(m_engine,DebugAll,"JBStream::processAuth('%s') [%p]",xml->name(),this);
// Waiting for abort to be confirmed
if (m_waitState == WaitAborted) {
if (xml->type() != XMLElement::Aborted)
DROP_AND_EXIT
if (!XMPPUtils::hasXmlns(*xml,XMPPNamespace::Sasl))
INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0)
terminate(true,0,XMPPError::Aborted,"Authentication aborted",false);
TelEngine::destruct(xml);
return;
}
while (true) {
// Sanity: check wait state
if (m_waitState != WaitChallenge && m_waitState != WaitResponse)
DROP_AND_EXIT
// SASL: accept challenge, failure, success
if (flag(UseSasl)) {
if (xml->type() != XMLElement::Success &&
xml->type() != XMLElement::Challenge &&
xml->type() != XMLElement::Failure)
DROP_AND_EXIT
if (!XMPPUtils::hasXmlns(*xml,XMPPNamespace::Sasl))
INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0)
// Waiting for response (sent auth or challenge response)
if (m_waitState == WaitResponse)
// MD5 auth
// Stream already authenticated: we received a challenge with valid rspauth
// and sent an empty response: wait for sucess/failure
// Stream not authenticated: we've sent auth or challenge response:
// wait for challenge or success with credentials
// Plain auth: accept success/failure
switch (m_authMech) {
case JIDFeatureSasl::MechMD5:
if (xml->type() == XMLElement::Failure)
break;
if (!flag(StreamAuthenticated)) {
String tmp = xml->getText();
DataBlock rspauth;
Base64 base((void*)tmp.c_str(),tmp.length(),false);
bool ok = base.decode(rspauth);
base.clear(false);
if (!ok)
INVALIDXML_AND_EXIT(XMPPError::IncorrectEnc,0);
tmp.assign((const char*)rspauth.data(),rspauth.length());
if (!tmp.startSkip("rspauth=",false))
INVALIDXML_AND_EXIT(XMPPError::BadFormat,"Invalid response");
String rspAuth;
buildDigestMD5Sasl(rspAuth,false);
if (rspAuth != tmp)
INVALIDXML_AND_EXIT(XMPPError::InvalidAuth,"Invalid response auth");
DDebug(m_engine,DebugAll,"Stream. Stream authenticated [%p]",this);
m_flags |= StreamAuthenticated;
// Send empty response to challenge
if (xml->type() == XMLElement::Challenge) {
TelEngine::destruct(xml);
xml = XMPPUtils::createElement(XMLElement::Response,XMPPNamespace::Sasl);
sendStreamXML(xml,state());
return;
}
break;
}
if (xml->type() != XMLElement::Success)
DROP_AND_EXIT
break;
case JIDFeatureSasl::MechPlain:
if (xml->type() == XMLElement::Challenge)
DROP_AND_EXIT
break;
default:
DDebug(m_engine,DebugStub,
"Stream. Unhandled SASL auth mechanism in Auth state [%p]",this);
DROP_AND_EXIT
}
// Waiting for challenge: sent auth element
else {
if (xml->type() != XMLElement::Challenge)
DROP_AND_EXIT
if (m_challengeCount) {
m_challengeCount--;
sendAuthResponse(xml);
}
else {
// Abort
m_waitState = WaitAborted;
TelEngine::destruct(xml);
xml = XMPPUtils::createElement(XMLElement::Abort,XMPPNamespace::Sasl);
sendStreamXML(xml,state());
}
return;
}
if (xml->type() == XMLElement::Success) {
TelEngine::destruct(xml);
break;
}
// Failure
if (xml->type() != XMLElement::Failure)
DROP_AND_EXIT
XMLElement* e = xml->findFirstChild();
XMPPError::Type err = XMPPError::NoError;
if (e) {
err = (XMPPError::Type)XMPPError::type(e->name());
if (err == XMPPError::Count)
err = XMPPError::NoError;
TelEngine::destruct(e);
}
terminate(true,xml,err,"Authentication failed",true,false,false);
return;
}
// XEP-0078: accept iq result or error
if (xml->type() != XMLElement::Iq)
DROP_AND_EXIT
// Check if received correct type
XMPPUtils::IqType t = XMPPUtils::iqType(xml->getAttribute("type"));
if (t != XMPPUtils::IqResult && t != XMPPUtils::IqError)
DROP_AND_EXIT
// Check if received correct id for the current waiting state
if (xml->hasAttribute("id","auth_1")) {
if (m_waitState != WaitChallenge)
DROP_AND_EXIT
}
else if (xml->hasAttribute("id","auth_2")) {
if (m_waitState != WaitResponse)
DROP_AND_EXIT
}
else
DROP_AND_EXIT
// Terminate now on valid error
if (t == XMPPUtils::IqError)
ERRORXML_AND_EXIT
// Result.
// WaitResponse: authenticated
if (m_waitState == WaitResponse) {
TelEngine::destruct(xml);
break;
}
// WaitChallenge: Check child and its namespace. Send response
XMLElement* child = 0;
XMLElement* username = 0;
XMLElement* resource = 0;
XMPPError::Type err = XMPPError::NoError;
const char* reason = 0;
while (true) {
child = xml->findFirstChild(XMLElement::Query);
if (!(child && XMPPUtils::hasXmlns(*child,XMPPNamespace::IqAuth))) {
err = XMPPError::InvalidNamespace;
break;
}
// XEP-0078: username and resource children must be present
username = child->findFirstChild(XMLElement::Username);
resource = child->findFirstChild(XMLElement::Resource);
if (!(username && resource)) {
reason = "Username or resource child is missing";
err = XMPPError::InvalidXml;
break;
}
// Get authentication methods
m_remoteFeatures.clear();
if (child->hasChild(XMLElement::Digest))
m_remoteFeatures.add(new JIDFeatureSasl(JIDFeatureSasl::MechSHA1));
if (child->hasChild(XMLElement::Password))
m_remoteFeatures.add(new JIDFeatureSasl(JIDFeatureSasl::MechPlain));
break;
}
TelEngine::destruct(username);
TelEngine::destruct(resource);
TelEngine::destruct(child);
if (err != XMPPError::NoError)
INVALIDXML_AND_EXIT(err,reason)
setClientAuthMechanism();
sendAuthResponse(xml);
return;
}
// Authenticated
resetStream();
if (flag(UseSasl))
sendStreamStart();
else {
Debug(m_engine,DebugInfo,"Stream. Authenticated [%p]",this);
changeState(Running);
}
}
// Process a received element in Started state
void JBStream::processStarted(XMLElement* xml)
{
XDebug(m_engine,DebugAll,"JBStream::processStarted('%s') [%p]",xml->name(),this);
if (m_waitState == WaitStart) {
if (xml->type() != XMLElement::StreamStart)
DROP_AND_EXIT
// Check attributes: namespaces, from
if (!(xml->hasAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]) &&
XMPPUtils::hasXmlns(*xml,XMPPNamespace::Client)))
INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0)
if (!(remote().domain() &= xml->getAttribute("from")))
INVALIDXML_AND_EXIT(XMPPError::HostUnknown,0)
// Get received version
String version = xml->getAttribute("version");
if (version.null())
m_flags |= NoRemoteVersion1;
else {
int pos = version.find('.');
String majorStr = (pos != -1) ? version.substr(0,pos) : version;
int major = majorStr.toInteger(0);
if (major == 0)
m_flags |= NoRemoteVersion1;
else
m_flags &= ~NoRemoteVersion1;
}
// Version 1: wait stream features
// Version 0: XEP-0078: start auth
// Start registering a new user if required
setRecvCount(-1);
if (flag(NoVersion1))
if (!m_register)
startAuth();
else
startRegister();
else
m_waitState = WaitFeatures;
}
else if (m_waitState == WaitFeatures) {
if (xml->type() != XMLElement::StreamFeatures)
DROP_AND_EXIT
if (!getStreamFeatures(xml))
return;
// Check TLS if not already secured
if (!flag(StreamSecured)) {
// Ignore all other features if TLS is started
// If missing: TLS shouldn't be used
// If present but not required check the local flag
JIDFeature* f = m_remoteFeatures.get(XMPPNamespace::Starttls);
if (f && (f->required() || flag(UseTls))) {
setRecvCount(1);
TelEngine::destruct(xml);
xml = XMPPUtils::createElement(XMLElement::Starttls,XMPPNamespace::Starttls);
sendStreamXML(xml,state());
m_waitState = WaitTlsRsp;
return;
}
// Allow user register through unsecured streams ?
if (m_register && !flag(AllowUnsafeSetup)) {
TelEngine::destruct(xml);
terminate(true,0,XMPPError::Policy,"Can't register new user on unsecured stream",true);
return;
}
}
m_flags |= StreamSecured;
// Check if already authenticated
// Start registering a new user if required
if (!flag(StreamAuthenticated)) {
// RFC 3920 6.1: no mechanisms --> SASL not supported
XMLElement* e = xml->findFirstChild(XMLElement::Mechanisms);
if (!(e && e->hasChild(0)))
m_flags &= ~UseSasl;
TelEngine::destruct(e);
TelEngine::destruct(xml);
if (!m_register)
startAuth();
else
startRegister();
return;
}
m_flags |= StreamAuthenticated;
// Bind resource
XMLElement* bind = XMPPUtils::createElement(XMLElement::Bind,XMPPNamespace::Bind);
if (!m_local.resource().null())
bind->addChild(new XMLElement(XMLElement::Resource,0,m_local.resource()));
XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"bind_1");
iq->addChild(bind);
m_waitState = WaitBindRsp;
sendStreamXML(iq,state());
}
else if (m_waitState == WaitTlsRsp) {
// Accept proceed and failure
bool ok = (xml->type() == XMLElement::Proceed);
if (!(ok || xml->type() == XMLElement::Failure) &&
!XMPPUtils::hasXmlns(*xml,XMPPNamespace::Starttls))
INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0)
if (ok)
startTls();
else
terminate(true,0,XMPPError::NoError,"Server can't start TLS",false);
}
else if (m_waitState == WaitBindRsp) {
// Accept iq result or error
if (xml->type() != XMLElement::Iq)
DROP_AND_EXIT
// Check if received correct type
XMPPUtils::IqType t = XMPPUtils::iqType(xml->getAttribute("type"));
if (t != XMPPUtils::IqResult && t != XMPPUtils::IqError)
DROP_AND_EXIT
// Check if received correct id for the current waiting state
if (!xml->hasAttribute("id","bind_1"))
DROP_AND_EXIT
// Terminate now on valid error
if (t == XMPPUtils::IqError)
ERRORXML_AND_EXIT
// Result
XMLElement* bind = 0;
XMLElement* jidxml = 0;
XMPPError::Type err = XMPPError::NoError;
const char* reason = 0;
while (true) {
bind = xml->findFirstChild(XMLElement::Bind);
if (!bind) {
err = XMPPError::InvalidXml;
reason = "Bind child is missing";
break;
}
if (!XMPPUtils::hasXmlns(*bind,XMPPNamespace::Bind)) {
err = XMPPError::InvalidNamespace;
break;
}
jidxml = bind->findFirstChild(XMLElement::Jid);
if (!jidxml) {
err = XMPPError::InvalidXml;
reason = "Jid child is misssing";
break;
}
JabberID jid(jidxml->getText());
if (!jid.isFull()) {
err = XMPPError::InvalidXml;
reason = "Jid is not full";
break;
}
bool changed = !(local() == jid);
m_local.set(jid.node(),jid.domain(),jid.resource());
if (changed)
Debug(m_engine,DebugInfo,"Stream. Local jid changed to '%s' [%p]",
local().c_str(),this);
break;
}
TelEngine::destruct(jidxml);
TelEngine::destruct(bind);
if (err == XMPPError::NoError)
// Create session if required
if (m_remoteFeatures.get(XMPPNamespace::Session)) {
XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"sess_1");
iq->addChild(XMPPUtils::createElement(XMLElement::Session,
XMPPNamespace::Session));
m_waitState = WaitSessionRsp;
sendStreamXML(iq,state());
}
else
changeState(Running);
else
INVALIDXML_AND_EXIT(err,reason)
}
else if (m_waitState == WaitSessionRsp) {
// Accept iq result or error
if (xml->type() != XMLElement::Iq)
DROP_AND_EXIT
// Check if received correct type
XMPPUtils::IqType t = XMPPUtils::iqType(xml->getAttribute("type"));
if (t != XMPPUtils::IqResult && t != XMPPUtils::IqError)
DROP_AND_EXIT
// Check if received correct id for the current waiting state
if (!xml->hasAttribute("id","sess_1"))
DROP_AND_EXIT
// Terminate on error
if (t == XMPPUtils::IqError)
ERRORXML_AND_EXIT
// Result
changeState(Running);
}
else
DROP_AND_EXIT
TelEngine::destruct(xml);
}
// Create an iq event from a received iq stanza
// Filter iq stanzas to generate an appropriate event
// Get iq type : set/get, error, result
// result: MAY have a first child with a response
// set/get: MUST have a first child
// error: MAY have a first child with the sent stanza
// MUST have an 'error' child
// Check type and the first child's namespace
JBEvent* JBStream::getIqEvent(XMLElement* xml, int iqType, XMPPError::Type& error)
{
#define IQEVENT_SET_REQ(get,set) evType = (iqType == XMPPUtils::IqGet) ? get : set
#define IQEVENT_SET_RSP(res,err) evType = (iqType == XMPPUtils::IqResult) ? res : err
JBEvent::Type evType;
switch (iqType) {
case XMPPUtils::IqGet:
case XMPPUtils::IqSet:
evType = JBEvent::Iq;
break;
case XMPPUtils::IqResult:
evType = JBEvent::IqResult;
break;
case XMPPUtils::IqError:
evType = JBEvent::IqError;
break;
default:
error = XMPPError::SFeatureNotImpl;
return 0;
}
error = XMPPError::NoError;
XMLElement* child = xml->findFirstChild();
// Request (type is set or get): check the child (MUST exists)
// Result: check it only if it has a child
// Error: check it only if it has an 'iq' child with a child
if (evType == JBEvent::Iq) {
// No child: request what ???
if (!child) {
error = XMPPError::SBadRequest;
return 0;
}
switch (child->type()) {
case XMLElement::Jingle:
if (checkValidXmlns(child,XMPPNamespace::Jingle,error))
IQEVENT_SET_REQ(JBEvent::IqJingleGet,JBEvent::IqJingleSet);
break;
case XMLElement::Session:
if (checkValidXmlns(child,XMPPNamespace::JingleSession,error))
IQEVENT_SET_REQ(JBEvent::IqJingleGet,JBEvent::IqJingleSet);
break;
case XMLElement::Query:
if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoInfo))
IQEVENT_SET_REQ(JBEvent::IqDiscoInfoGet,JBEvent::IqDiscoInfoSet);
else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoItems))
IQEVENT_SET_REQ(JBEvent::IqDiscoItemsGet,JBEvent::IqDiscoItemsSet);
else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Roster)) {
if (iqType == XMPPUtils::IqGet)
error = XMPPError::SBadRequest;
else
evType = JBEvent::IqRosterSet;
}
break;
case XMLElement::Command:
if (checkValidXmlns(child,XMPPNamespace::Command,error))
IQEVENT_SET_REQ(JBEvent::IqCommandGet,JBEvent::IqCommandSet);
break;
default: ;
}
}
else if (child) {
switch (child->type()) {
case XMLElement::Jingle:
if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Jingle))
IQEVENT_SET_RSP(JBEvent::IqJingleRes,JBEvent::IqJingleErr);
break;
case XMLElement::Session:
if (XMPPUtils::hasXmlns(*child,XMPPNamespace::JingleSession))
IQEVENT_SET_RSP(JBEvent::IqJingleRes,JBEvent::IqJingleErr);
break;
case XMLElement::Query:
if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoInfo))
IQEVENT_SET_RSP(JBEvent::IqDiscoInfoRes,JBEvent::IqDiscoInfoErr);
else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoItems))
IQEVENT_SET_RSP(JBEvent::IqDiscoItemsRes,JBEvent::IqDiscoItemsErr);
else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Roster))
IQEVENT_SET_RSP(JBEvent::IqRosterRes,JBEvent::IqRosterErr);
break;
case XMLElement::Command:
if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Command))
IQEVENT_SET_RSP(JBEvent::IqCommandRes,JBEvent::IqCommandErr);
break;
default: ;
}
}
if (error == XMPPError::NoError)
return new JBEvent(evType,this,xml,child);
TelEngine::destruct(child);
return 0;
#undef IQEVENT_SET_REQ
#undef IQEVENT_SET_RSP
}
// Send declaration and stream start
bool JBStream::sendStreamStart()
{
m_id = "";
m_declarationSent = 0;
return sendStreamXML(getStreamStart(),Started);
}
// Send stream XML elements through the socket
bool JBStream::sendStreamXML(XMLElement* e, State newState)
{
Lock lock(m_socket.m_streamMutex);
Error ret = ErrorContext;
while (e) {
if (state() == Idle || state() == Destroy)
break;
if (m_streamXML) {
ret = sendPending();
if (ret != ErrorNone) {
TelEngine::destruct(e);
break;
}
}
bool unclose = (e->type() == XMLElement::StreamStart ||
e->type() == XMLElement::StreamEnd);
m_streamXML = new XMLElementOut(e,0,unclose);
ret = sendPending();
if (ret == ErrorPending)
ret = ErrorNone;
break;
}
if (ret == ErrorNone)
changeState(newState);
return (ret == ErrorNone);
}
// Terminate stream on receiving invalid elements
void JBStream::invalidStreamXML(XMLElement* xml, XMPPError::Type error, const char* reason)
{
if (!xml)
return;
Debug(m_engine,DebugNote,
"Stream. Invalid XML (%p,%s) state=%s error='%s' reason='%s' [%p]",
xml,xml->name(),lookupState(state()),s_err[error],reason,this);
terminate(true,xml,error,reason,true);
}
// Terminate stream on receiving stanza errors
void JBStream::errorStreamXML(XMLElement* xml)
{
String error, reason;
if (xml) {
XMLElement* tmp = xml->findFirstChild(XMLElement::Error);
XMPPUtils::decodeError(tmp,error,reason);
TelEngine::destruct(tmp);
TelEngine::destruct(xml);
}
Debug(m_engine,DebugNote,"Stream. Received error=%s reason='%s' state=%s [%p]",
error.c_str(),reason.c_str(),lookupState(state()),this);
terminate(false,0,XMPPError::NoError,reason?reason:error,true);
}
// Drop an unexpected or unhandled element
void JBStream::dropXML(XMLElement* xml, bool unexpected)
{
if (!xml)
return;
Debug(m_engine,unexpected?DebugNote:DebugInfo,
"Stream. Dropping %s element (%p,%s) in state %s [%p]",
unexpected?"unexpected":"unhandled",xml,xml->name(),
lookupState(state()),this);
TelEngine::destruct(xml);
}
// Change stream state
void JBStream::changeState(State newState)
{
if (m_state == newState)
return;
Debug(m_engine,DebugInfo,"Stream. Changing state from %s to %s [%p]",
lookupState(m_state),lookupState(newState),this);
m_state = newState;
if (newState == Running) {
m_setupTimeout = 0;
startIdleTimer();
streamRunning();
if (!m_startEvent)
m_startEvent = new JBEvent(JBEvent::Running,this,0);
}
}
// Parse receive stream features
bool JBStream::getStreamFeatures(XMLElement* features)
{
// Get xmlType child, check 'ns' namespace, add remote feature
#define GET_FEATURE(xmlType,ns) { \
XMLElement* e = features->findFirstChild(xmlType); \
if (e) { \
if (!(XMPPUtils::hasXmlns(*e,ns))) { \
TelEngine::destruct(e); \
invalidStreamXML(features,XMPPError::InvalidNamespace,0); \
return false; \
} \
m_remoteFeatures.add(ns,e->hasChild(XMLElement::Required)); \
TelEngine::destruct(e); \
} \
}
m_remoteFeatures.clear();
if (!features)
return true;
// TLS
GET_FEATURE(XMLElement::Starttls,XMPPNamespace::Starttls)
// SASL
XMLElement* sasl = features->findFirstChild(XMLElement::Mechanisms);
if (sasl) {
if (!(XMPPUtils::hasXmlns(*sasl,XMPPNamespace::Sasl))) {
TelEngine::destruct(sasl);
invalidStreamXML(features,XMPPError::InvalidNamespace,0);
return false;
}
int auth = 0;
XMLElement* m = 0;
while (0 != (m = sasl->findNextChild(m,XMLElement::Mechanism)))
auth |= lookup(m->getText(),JIDFeatureSasl::s_authMech);
m_remoteFeatures.add(new JIDFeatureSasl(auth,sasl->hasChild(XMLElement::Required)));
TelEngine::destruct(sasl);
}
setClientAuthMechanism();
// Old auth (older then version 1.0 SASL)
GET_FEATURE(XMLElement::Auth,XMPPNamespace::IqAuthFeature)
// Register new user
GET_FEATURE(XMLElement::Register,XMPPNamespace::Register)
// Bind resources
GET_FEATURE(XMLElement::Bind,XMPPNamespace::Bind)
// Sessions
GET_FEATURE(XMLElement::Session,XMPPNamespace::Session)
return true;
#undef GET_FEATURE
#undef REQUIRED
}
// Start client TLS. Terminate the stream on error
bool JBStream::startTls()
{
Debug(m_engine,DebugInfo,"Stream. Initiating TLS [%p]",this);
changeState(Securing);
if (m_engine->encryptStream(this)) {
m_flags |= StreamSecured;
setRecvCount(-1);
sendStreamStart();
return true;
}
terminate(false,0,XMPPError::NoError,"Failed to start TLS",false);
return false;
}
// Start client registration
bool JBStream::startRegister()
{
// Allow user register through unsecured streams ?
if (!flag(StreamSecured | AllowUnsafeSetup)) {
terminate(true,0,XMPPError::Policy,"Can't register new user on unsecured stream",true);
return false;
}
// Check if the server already told us it supports in-band register
// or query register support
XMLElement* xml = 0;
m_registerId = m_remoteFeatures.get(XMPPNamespace::Register) ? 2 : 1;
String id(m_registerId);
if (m_registerId == 2)
xml = XMPPUtils::createRegisterQuery(0,0,id,m_local.node(),m_password);
else
xml = XMPPUtils::createRegisterQuery(XMPPUtils::IqGet,0,0,id);
return sendStreamXML(xml,Register);
}
// Start client authentication
bool JBStream::startAuth()
{
XMLElement* xml = getAuthStart();
if (xml) {
Debug(m_engine,DebugAll,
"Stream. Starting authentication type=%s mechanism=%s [%p]",
((type()==JBEngine::Component)?"handshake":(flag(UseSasl)?"SASL":"IQ")),
lookup(m_authMech,JIDFeatureSasl::s_authMech),this);
return sendStreamXML(xml,Auth);
}
Debug(m_engine,DebugNote,"Stream. Failed to build auth start [%p]",this);
terminate(false,0,XMPPError::InvalidMechanism,"No mechanism available",true);
return false;
}
// Send auth response to received challenge/iq
bool JBStream::sendAuthResponse(XMLElement* challenge)
{
XMLElement* xml = 0;
String response;
XMPPError::Type code = XMPPError::NoError;
const char* error = 0;
if (flag(UseSasl))
#define SET_CODE_AND_BREAK(c,e) { code = c; error = e; break; }
while (true) {
if (m_authMech != JIDFeatureSasl::MechMD5 &&
m_authMech != JIDFeatureSasl::MechPlain)
SET_CODE_AND_BREAK(XMPPError::InvalidMechanism,"No mechanism available")
// This should never happen
if (!(challenge && challenge->type() == XMLElement::Challenge))
SET_CODE_AND_BREAK(XMPPError::Internal,"Unexpected element while expecting 'challenge'")
// TODO: implement challenge when using plain authentication
if (m_authMech == JIDFeatureSasl::MechPlain) {
const char* s = "Challenge not implemented for plain authentication";
Debug(m_engine,DebugStub,"Stream. %s [%p]",s,this);
SET_CODE_AND_BREAK(XMPPError::UndefinedCondition,s)
}
const char* chgText = challenge->getText();
if (!chgText)
SET_CODE_AND_BREAK(XMPPError::BadFormat,"Challenge is empty")
Base64 base64((void*)chgText,::strlen(chgText),false);
DataBlock chg;
bool ok = base64.decode(chg,false);
base64.clear(false);
if (!ok)
SET_CODE_AND_BREAK(XMPPError::IncorrectEnc,"Challenge with incorrect encoding")
String tmp((const char*)chg.data(),chg.length());
if (tmp.null())
SET_CODE_AND_BREAK(XMPPError::BadFormat,"Challenge is empty")
Debug(m_engine,DebugAll,"Stream(%s). Received challenge '%s' [%p]",
toString().c_str(),tmp.c_str(),this);
String nonce,realm;
ObjList* obj = tmp.split(',',false);
for (ObjList* o = obj->skipNull(); o; o = o->skipNext()) {
String* s = static_cast<String*>(o->get());
if (s->startsWith("realm="))
realm = s->substr(6);
else if (s->startsWith("nonce="))
nonce = s->substr(6);
}
TelEngine::destruct(obj);
MimeHeaderLine::delQuotes(realm);
MimeHeaderLine::delQuotes(nonce);
if (nonce.null())
SET_CODE_AND_BREAK(XMPPError::BadFormat,"Challenge is incomplete")
buildSaslResponse(response,&realm,&nonce);
xml = XMPPUtils::createElement(XMLElement::Response,XMPPNamespace::Sasl,response);
break;
}
#undef SET_CODE_AND_BREAK
else {
xml = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"auth_2");
XMLElement* q = XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::IqAuth);
q->addChild(new XMLElement(XMLElement::Username,0,m_local.node()));
q->addChild(new XMLElement(XMLElement::Resource,0,m_local.resource()));
if (m_authMech == JIDFeatureSasl::MechSHA1) {
SHA1 sha;
sha << id() << m_password;
q->addChild(new XMLElement(XMLElement::Digest,0,sha.hexDigest()));
}
else if (m_authMech == JIDFeatureSasl::MechPlain)
q->addChild(new XMLElement(XMLElement::Password,0,m_password));
else {
code = XMPPError::InvalidMechanism;
error = "No mechanism available";
}
xml->addChild(q);
}
if (!error) {
TelEngine::destruct(challenge);
m_waitState = WaitResponse;
return sendStreamXML(xml,state());
}
TelEngine::destruct(xml);
Debug(m_engine,DebugNote,"Stream. Failed to respond error=%s reason='%s'. %s [%p]",
s_err[code],error,flag(UseSasl)?"Aborting":"Terminating",this);
if (flag(UseSasl)) {
TelEngine::destruct(challenge);
xml = XMPPUtils::createElement(XMLElement::Abort,XMPPNamespace::Sasl);
return sendStreamXML(xml,state());
}
terminate(false,challenge,code,error,true);
return false;
}
// Build SASL authentication response
// A valid mechanism must be previously set
void JBStream::buildSaslResponse(String& response, String* realm, String* nonce)
{
// Digest MD5. See RFC 4616 Section 2
// [authzid] UTF8NUL authcid UTF8NUL passwd
if (m_authMech == JIDFeatureSasl::MechPlain) {
DataBlock data;
unsigned char nul = 0;
data.append(&nul,1);
data += m_local.node();
data.append(&nul,1);
data += m_password;
Base64 base64((void*)data.data(),data.length());
base64.encode(response);
return;
}
// Digest MD5. See RFC 2831 2.1.2.1
MD5 md5(String((unsigned int)::random()));
m_cnonce = md5.hexDigest();
appendQParam(response,"username",m_local.node(),true,true);
m_realm = realm ? *realm : "";
if (m_realm)
appendQParam(response,"realm",m_realm,true);
m_nonce = nonce ? *nonce : "";
appendQParam(response,"nonce",m_nonce,true);
m_nonceCount++;
char tmp[9];
::sprintf(tmp,"%08x",m_nonceCount);
m_nc = tmp;
appendQParam(response,"nc",m_nc,false);
appendQParam(response,"cnonce",m_cnonce,true);
appendQParam(response,"digest-uri",String("xmpp/")+m_local.domain(),true);
appendQParam(response,"qop",s_qop,true);
String rsp;
buildDigestMD5Sasl(rsp,true);
appendQParam(response,"response",rsp,false);
appendQParam(response,"charset","utf-8",false);
appendQParam(response,"algorithm","md5-sess",false);
Debug(m_engine,DebugAll,"Stream(%s). Built SASL response '%s' [%p]",
toString().c_str(),response.c_str(),this);
Base64 base64((void*)response.c_str(),response.length());
base64.encode(response);
}
// Parse remote's features and pick an authentication mechanism
// to be used when requesting authentication
void JBStream::setClientAuthMechanism()
{
JIDFeature* f = m_remoteFeatures.get(XMPPNamespace::Sasl);
JIDFeatureSasl* sasl = static_cast<JIDFeatureSasl*>(f);
m_authMech = JIDFeatureSasl::MechNone;
if (!sasl)
return;
// Component or not using SASL: accept SHA1 and plain
if (type() == JBEngine::Component || !flag(UseSasl)) {
if (sasl->mechanism(JIDFeatureSasl::MechSHA1))
m_authMech = JIDFeatureSasl::MechSHA1;
else if (sasl->mechanism(JIDFeatureSasl::MechPlain) && flag(AllowPlainAuth))
m_authMech = JIDFeatureSasl::MechPlain;
return;
}
// SASL: accept Digest MD5
if (sasl->mechanism(JIDFeatureSasl::MechMD5))
m_authMech = JIDFeatureSasl::MechMD5;
else if (sasl->mechanism(JIDFeatureSasl::MechPlain) && flag(AllowPlainAuth))
m_authMech = JIDFeatureSasl::MechPlain;
}
// Build a Digest MD5 SASL to be sent with authentication responses
// See RFC 2831 2.1.2.1
// A1 = H(username:realm:passwd):nonce:cnonce:authzid
// A2 ="AUTHENTICATE:uri
// rsp = HEX(HEX(A1):nonce:nc:cnonce:qop:HEX(A2))
void JBStream::buildDigestMD5Sasl(String& dest, bool authenticate)
{
MD5 md5;
md5 << m_local.node() << ":" << m_realm << ":" << m_password;
MD5 md5A1(md5.rawDigest(),16);
if (m_nonce)
md5A1 << ":" << m_nonce;
md5A1 << ":" << m_cnonce;
MD5 md5A2;
if (authenticate)
md5A2 << "AUTHENTICATE";
md5A2 << ":xmpp/" << m_local.domain();
MD5 md5Rsp;
md5Rsp << md5A1.hexDigest();
if (m_nonce)
md5Rsp << ":" << m_nonce << ":" << m_nc;
md5Rsp << ":" << m_cnonce << ":" << s_qop << ":" << md5A2.hexDigest();
dest = md5Rsp.hexDigest();
}
// Event termination notification
void JBStream::eventTerminated(const JBEvent* event)
{
if (event && event == m_lastEvent) {
m_lastEvent = 0;
DDebug(m_engine,DebugAll,
"Stream. Event (%p,%s) terminated [%p]",event,event->name(),this);
}
}
// Try to send the first element in pending outgoing stanzas list
// Terminate stream on socket error
JBStream::Error JBStream::sendPending()
{
XMLElementOut* eout = 0;
if (state() == Destroy)
return ErrorContext;
if (m_streamXML) {
m_idleTimeout = 0;
// Check if declaration was sent
if (m_declarationSent < s_declaration.length()) {
const char* data = s_declaration.c_str() + m_declarationSent;
unsigned int len = s_declaration.length() - m_declarationSent;
if (!m_socket.send(data,len)) {
Debug(m_engine,DebugNote,"Stream. Failed to send declaration [%p]",this);
terminate(false,0,XMPPError::HostGone,"Failed to send data",false);
return ErrorNoSocket;
}
m_declarationSent += len;
if (m_declarationSent < s_declaration.length())
return ErrorPending;
DDebug(m_engine,DebugAll,"Stream. Sent declaration %s [%p]",
s_declaration.c_str(),this);
}
eout = m_streamXML;
}
else {
ObjList* obj = m_outXML.skipNull();
if (!obj) {
if (!m_idleTimeout)
startIdleTimer();
return ErrorNone;
}
if (state() != Running) {
m_idleTimeout = 0;
return ErrorPending;
}
eout = obj ? static_cast<XMLElementOut*>(obj->get()) : 0;
}
XMLElement* xml = eout->element();
if (!xml) {
if (eout != m_streamXML) {
m_outXML.remove(eout,true);
if (!m_idleTimeout)
startIdleTimer();
}
else
TelEngine::destruct(m_streamXML);
return ErrorNone;
}
// Print the element only if it's the first time
if (!eout->sent())
m_engine->printXml(*xml,this,true);
Error ret = ErrorNone;
u_int32_t len;
const char* data = eout->getData(len);
unsigned int tmp = len;
if (m_socket.send(data,len)) {
if (len != tmp)
ret = ErrorPending;
eout->dataSent(len);
}
else
ret = ErrorNoSocket;
if (ret == ErrorPending) {
m_idleTimeout = 0;
return ret;
}
if (ret == ErrorNone)
DDebug(m_engine,DebugAll,"Stream. Sent element (%p,%s) id='%s [%p]",
xml,xml->name(),eout->id().c_str(),this);
else {
// Don't terminate if the element is stream error or stream end:
// stream is already terminating
bool bye = xml->type() != XMLElement::StreamError &&
xml->type() != XMLElement::StreamEnd;
Debug(m_engine,DebugNote,"Stream. Failed to send (%p,%s) in state=%s [%p]",
xml,xml->name(),lookupState(state()),this);
if (eout->id()) {
JBEvent* ev = new JBEvent(JBEvent::WriteFail,this,
eout->release(),eout->id());
m_events.append(ev);
}
if (bye)
terminate(false,0,XMPPError::HostGone,"Failed to send data",false);
}
if (eout != m_streamXML)
m_outXML.remove(eout,true);
else
TelEngine::destruct(m_streamXML);
startIdleTimer();
return ret;
}
// Remove:
// Pending elements with id if id is not 0
// All elements without id if id is 0
void JBStream::removePending(bool notify, const String* id, bool force)
{
ListIterator iter(m_outXML);
bool first = true;
for (GenObject* o = 0; (o = iter.get());) {
XMLElementOut* eout = static_cast<XMLElementOut*>(o);
// Check if the first element will be removed if partially sent
if (first) {
first = false;
if (eout->dataCount() && !force)
continue;
}
if (id) {
if (*id != eout->id())
continue;
}
else if (eout->id())
continue;
if (notify)
m_events.append(new JBEvent(JBEvent::WriteFail,this,eout->release(),id));
m_outXML.remove(eout,true);
}
}
// Called when a setup state was completed
// Set/reset some stream flags and data
void JBStream::resetStream()
{
// TLS: RFC 3920
// SASL: RFC 3920 Section 7 page 38
switch (state()) {
case Securing:
m_flags |= StreamSecured;
m_id = "";
break;
case Auth:
m_flags |= StreamAuthenticated;
if (flag(UseSasl))
m_id = "";
break;
case Destroy:
case Idle:
m_flags &= ~(StreamAuthenticated | StreamSecured);
m_challengeCount = 2;
m_id = "";
break;
default:
break;
}
m_flags &= ~NoRemoteVersion1;
m_nonce = "";
m_cnonce = "";
m_realm = "";
}
// Set receive count
void JBStream::setRecvCount(int value)
{
Lock lock(m_socket.m_receiveMutex);
if (m_recvCount == value)
return;
DDebug(m_engine,DebugInfo,"Stream. recvCount changed from %d to %d [%p]",
m_recvCount,value,this);
m_recvCount = value;
}
// Start idle timer if there are no pending stanzas
bool JBStream::startIdleTimer(u_int64_t time)
{
if (state() != Running || !m_engine || m_outXML.skipNull() ||
!m_engine->m_streamIdleInterval) {
m_idleTimeout = 0;
return false;
}
m_idleTimeout = time + m_engine->m_streamIdleInterval;
XDebug(m_engine,DebugInfo,"Stream. Started idle timer for " FMT64 "ms [%p]",
m_engine->m_streamIdleInterval,this);
return true;
}
/**
* JBComponentStream
*/
JBComponentStream::JBComponentStream(JBEngine* engine, XMPPServerInfo& info,
const JabberID& localJid, const JabberID& remoteJid)
: JBStream(engine,JBEngine::Component,info,localJid,remoteJid)
{
// Doesn't use SASL auth: just using this structure to set auth mechanism
JIDFeatureSasl* sasl = new JIDFeatureSasl(JIDFeatureSasl::MechMD5 |
JIDFeatureSasl::MechSHA1);
m_remoteFeatures.add(sasl);
}
// Get an object from this stream
void* JBComponentStream::getObject(const String& name) const
{
if (name == "JBComponentStream")
return (void*)this;
return JBStream::getObject(name);
}
// Create stream start element
XMLElement* JBComponentStream::getStreamStart()
{
XMLElement* start = XMPPUtils::createElement(XMLElement::StreamStart,
XMPPNamespace::ComponentAccept);
start->setAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]);
start->setAttribute("to",local());
return start;
}
// Get the authentication element to be sent when authentication starts
XMLElement* JBComponentStream::getAuthStart()
{
setClientAuthMechanism();
if (m_authMech == JIDFeatureSasl::MechSHA1) {
SHA1 auth;
auth << id() << m_password;
return new XMLElement(XMLElement::Handshake,0,auth.hexDigest());
}
else if (m_authMech == JIDFeatureSasl::MechPlain)
return new XMLElement(XMLElement::Handshake,0,m_password);
return 0;
}
// Process a received element in Started state
void JBComponentStream::processStarted(XMLElement* xml)
{
// Expect stream start tag
setRecvCount(-1);
if (xml->type() != XMLElement::StreamStart)
DROP_AND_EXIT
// Check namespaces
if (!(xml->hasAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]) &&
XMPPUtils::hasXmlns(*xml,XMPPNamespace::ComponentAccept)))
INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0);
// Check the from attribute
if (!engine()->checkComponentFrom(this,xml->getAttribute("from")))
INVALIDXML_AND_EXIT(XMPPError::HostUnknown,0);
TelEngine::destruct(xml);
startAuth();
}
// Process a received element in Auth state
void JBComponentStream::processAuth(XMLElement* xml)
{
setRecvCount(-1);
if (xml->type() != XMLElement::Handshake)
DROP_AND_EXIT
TelEngine::destruct(xml);
changeState(Running);
}
/**
* JBClientStream
*/
// Outgoing
JBClientStream::JBClientStream(JBEngine* engine, XMPPServerInfo& info,
const JabberID& localJid, const NamedList& params)
: JBStream(engine,JBEngine::Client,info,localJid,JabberID(0,localJid.domain(),0))
{
m_name = params.getValue("account");
m_roster = new XMPPUserRoster(0,localJid.node(),localJid.domain());
m_resource = new JIDResource(local().resource(),JIDResource::Available,
JIDResource::CapChat|JIDResource::CapAudio);
// Check if we should register this user
m_register = params.getBoolValue("register");
}
// Destructor
JBClientStream::~JBClientStream()
{
TelEngine::destruct(m_roster);
TelEngine::destruct(m_resource);
}
// Get an object from this stream
void* JBClientStream::getObject(const String& name) const
{
if (name == "JBClientStream")
return (void*)this;
return JBStream::getObject(name);
}
// Get a remote user from roster
XMPPUser* JBClientStream::getRemote(const JabberID& jid)
{
return m_roster->getUser(jid,false);
}
// Send a stanza
JBStream::Error JBClientStream::sendStanza(XMLElement* stanza, const char* senderId)
{
if (!stanza)
return ErrorContext;
Lock lock(streamMutex());
// Destroy: call parent's method to put the debug message
if (state() == Destroy)
return JBStream::sendStanza(stanza,senderId);
// Check 'from' attribute
const char* from = stanza->getAttribute("from");
if (from && *from) {
JabberID jid(from);
if (!local().match(jid)) {
Debug(engine(),DebugNote,
"Stream. Can't send stanza (%p,%s) with invalid from=%s [%p]",
stanza,stanza->name(),from,this);
TelEngine::destruct(stanza);
return ErrorContext;
}
}
#if 0
// TODO: Uncomment and implement. We'll need only 'subscribed' and 'unsubscribed'
// elements
if (stanza->type() == XMLElement::Presence) {
// Ignore if the presence is not sent to an actual user (node)
JabberID to(stanza->getAttribute("to"));
if (to.node()) {
Lock lock(m_roster);
// TODO: Update subscriptions for users in roster
}
}
#endif
return JBStream::sendStanza(stanza,senderId);
}
// Stream is running: get roster from server
void JBClientStream::streamRunning()
{
XDebug(engine(),DebugAll,"JBClientStream::streamRunning() [%p]",this);
if (!m_rosterReqId.null())
return;
m_roster->cleanup();
m_rosterReqId = "roster-query";
XMLElement* xml = XMPPUtils::createIq(XMPPUtils::IqGet,0,0,m_rosterReqId);
xml->addChild(XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::Roster));
sendStanza(xml);
}
// Process received data while running
void JBClientStream::processRunning(XMLElement* xml)
{
XDebug(engine(),DebugAll,"JBClientStream::processRunning('%s') [%p]",xml->name(),this);
JBStream::processRunning(xml);
// Check last event for post processing
JBEvent* event = lastEvent();
if (!event)
return;
bool sendPres = true;
switch (event->type()) {
case JBEvent::Presence:
break;
case JBEvent::IqRosterSet:
sendStanza(XMPPUtils::createIq(XMPPUtils::IqResult,event->to(),
event->from(),event->id()));
sendPres = false;
break;
case JBEvent::IqRosterRes:
case JBEvent::IqRosterErr:
if (m_rosterReqId == event->id()) {
// Cleanup roster only if received result or error
m_rosterReqId = "";
m_roster->cleanup();
if (event->type() == JBEvent::IqRosterRes)
break;
// Error
Debug(engine(),DebugNote,"Stream. Received error '%s' on roster request [%p]",
event->text().c_str(),this);
String err, txt;
XMPPUtils::decodeError(event->element(),err,txt);
m_events.remove(event,true);
String tmp;
tmp << "Unable to get roster from server";
if (err)
tmp << " error=" << err;
if (txt)
tmp << " reason=" << txt;
TelEngine::destruct(event);
terminate(false,0,XMPPError::NoError,tmp,false);
}
return;
case JBEvent::IqDiscoInfoGet:
sendStanza(m_roster->createDiscoInfoResult(event->to(),event->from(),event->id()));
m_events.remove(event,true);
return;
case JBEvent::IqDiscoItemsGet:
case JBEvent::IqDiscoInfoSet:
case JBEvent::IqDiscoItemsSet:
sendStanza(event->createError(XMPPError::TypeCancel,XMPPError::SFeatureNotImpl));
m_events.remove(event,true);
return;
case JBEvent::IqDiscoInfoRes:
case JBEvent::IqDiscoInfoErr:
case JBEvent::IqDiscoItemsRes:
case JBEvent::IqDiscoItemsErr:
return;
default:
return;
}
// Presence: update roster and let the event to be processed by a service
// TODO: Presence None and Unavailable: check if we already know it
// If so, remove event to avoid sending too many massages
// or
// Don't do that: someone might rely on those presences (for timeout purposes?)
if (event->type() == JBEvent::Presence) {
JBPresence::Presence pres = JBPresence::presenceType(event->stanzaType());
// Check if it's the same user: update resource list
if (local().bare() &= event->from().bare()) {
Lock2 lock(m_roster,&m_roster->resources());
bool avail = pres == JBPresence::None;
if (avail || pres == JBPresence::Unavailable) {
if (event->from().resource()) {
// Check for our own resource: we've seen that
if (event->from().resource() == local().resource()) {
Debug(engine(),DebugAll,
"Stream. Ignoring presence from the same resource [%p]",this);
m_events.remove(event,true);
return;
}
JIDResource* res = m_roster->resources().get(event->from().resource());
if (pres == JBPresence::Unavailable) {
if (res)
m_roster->resources().remove(res,true);
}
else
if (res)
res->fromXML(event->element());
else {
res = new JIDResource(event->from().resource());
res->fromXML(event->element());
m_roster->resources().add(res);
}
if (res)
Debug(engine(),DebugAll,
"Stream. %s own resource '%s' [%p]",
avail?"Added":"Removed",event->from().resource().c_str(),this);
}
else if (!avail && m_roster->resources().count()) {
m_roster->resources().clear();
Debug(engine(),DebugAll,"Stream. Removed own resources [%p]",this);
}
}
return;
}
XMPPUser* user = getRemote(event->from());
bool error = false;
switch (pres) {
case JBPresence::None:
case JBPresence::Unavailable:
if (user)
user->processPresence(event,pres == JBPresence::None);
else
error = true;
break;
case JBPresence::Subscribed:
case JBPresence::Unsubscribed:
if (user)
user->processSubscribe(event,pres);
else
error = true;
break;
case JBPresence::Subscribe:
case JBPresence::Unsubscribe:
case JBPresence::Error:
break;
case JBPresence::Probe:
dropXML(event->releaseXML());
m_events.remove(event,true);
break;
}
TelEngine::destruct(user);
#ifdef DEBUG
// Don't show message if it's the same jid: it came from another resource
if (error && !(event->to().bare() &= event->from().bare()))
DDebug(engine(),DebugNote,
"Stream. Received presence=%s from=%s. User not in roster [%p]",
event->stanzaType().c_str(),event->from().c_str(),this);
#endif
return;
}
// Roster event: update and change event type
event->m_type = JBEvent::IqClientRosterUpdate;
// Add new resource if not added. Send initial presence
if (sendPres) {
XMLElement* pres = new XMLElement(XMLElement::Presence);
m_resource->setName(local().resource());
m_resource->addTo(pres);
sendStanza(pres);
}
// Process received roster update
XMLElement* item = event->child() ? event->child()->findFirstChild(XMLElement::Item) : 0;
for (; item; item = event->child()->findNextChild(item,XMLElement::Item)) {
JabberID jid = item->getAttribute("jid");
String sub = item->getAttribute("subscription");
XMPPUser* user = m_roster->getUser(jid,false);
bool removeUser = false;
if (sub != "remove") {
XMPPDirVal::Direction subType = (XMPPDirVal::Direction)XMPPDirVal::lookup(sub);
if (user)
user->subscription().replace(subType);
else {
user = new XMPPUser(m_roster,jid.node(),jid.domain(),subType,false,false);
user->ref();
}
removeUser = !user->local();
}
else
removeUser = true;
if (user) {
Debug(engine(),DebugAll,"Stream. Updated roster jid=%s subscription=%s [%p]",
jid.c_str(),sub.c_str(),this);
if (removeUser) {
Debug(engine(),DebugInfo,
"Stream. Removing jid=%s from roster [%p]",jid.c_str(),this);
// deref() the user since we've increased its reference counter
user->deref();
}
}
TelEngine::destruct(user);
}
}
// Check the 'to' attribute of a received element
// Accept empty or bare/full jid match. Set 'to' if empty
bool JBClientStream::checkDestination(XMLElement* xml, bool& respond)
{
respond = false;
if (!xml)
return false;
const char* to = xml->getAttribute("to");
if (to && *to) {
JabberID jid(to);
// Waiting for bid response: accept 'to' with resource if we don't have one
if (state() == Started && m_waitState == WaitBindRsp && !local().resource())
return jid.match(local());
return local().match(jid);
}
xml->setAttribute("to",local());
return true;
}
/* vi: set ts=8 sw=4 sts=4 noet: */