yate/libs/yjabber/jbstream.cpp

2510 lines
74 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 <stdlib.h>
using namespace TelEngine;
static const String s_dbVerify = "verify";
static const String s_dbResult = "result";
static inline bool isDbVerify(XmlElement& xml)
{
const String* tag = 0;
const String* ns = 0;
return xml.getTag(tag,ns) && *tag == s_dbVerify &&
ns && *ns == XMPPUtils::s_ns[XMPPNamespace::Dialback];
}
static inline bool isDbResult(XmlElement& xml)
{
const String* tag = 0;
const String* ns = 0;
return xml.getTag(tag,ns) && *tag == s_dbResult &&
ns && *ns == XMPPUtils::s_ns[XMPPNamespace::Dialback];
}
// Decode a Base64 string to a block
static inline bool decodeBase64(DataBlock& buf, const String& str)
{
Base64 b((void*)str.c_str(),str.length(),false);
bool ok = b.decode(buf,false);
b.clear(false);
return ok;
}
// Decode a Base64 string to another string
// Check if decoded data has valid UTF8 characters
static bool decodeBase64(String& buf, const String& str, JBStream* stream)
{
DataBlock d;
if (!decodeBase64(d,str))
return false;
buf.assign((const char*)d.data(),d.length());
if (-1 != buf.lenUtf8())
return true;
Debug(stream,DebugNote,"Received Base64 encoded invalid UTF8 characters [%p]",stream);
return false;
}
static const TokenDict s_location[] = {
{"internal", 0},
{"remote", 1},
{"local", -1},
{0,0},
};
const TokenDict JBStream::s_stateName[] = {
{"Running", Running},
{"Idle", Idle},
{"Connecting", Connecting},
{"WaitStart", WaitStart},
{"Starting", Starting},
{"Features", Features},
{"WaitTlsRsp", WaitTlsRsp},
{"Auth", Auth},
{"Challenge", Challenge},
{"Securing", Securing},
{"Register", Register},
{"Destroy", Destroy},
{0,0},
};
const TokenDict JBStream::s_flagName[] = {
{"noautorestart", NoAutoRestart},
{"tlsrequired", TlsRequired},
{"dialback", DialbackOnly},
{"allowplainauth", AllowPlainAuth},
{"register", RegisterUser},
// Internal flags
{"roster_requested", RosterRequested},
{"online", AvailableResource},
{"secured", StreamTls | StreamSecured},
{"authenticated", StreamAuthenticated},
{"waitbindrsp", StreamWaitBindRsp},
{"waitsessrsp", StreamWaitSessRsp},
{"waitchallenge", StreamWaitChallenge},
{"waitchallengersp", StreamWaitChgRsp},
{0,0}
};
const TokenDict JBStream::s_typeName[] = {
{"c2s", c2s},
{"s2s", s2s},
{0,0}
};
// Retrieve the multiplier for non client stream timers
static inline unsigned int timerMultiplier(JBStream* stream)
{
return stream->type() == JBStream::c2s ? 1 : 1;
}
/*
* JBStream
*/
// Incoming
JBStream::JBStream(JBEngine* engine, Socket* socket, Type t)
: Mutex(true,"JBStream"),
m_sasl(0),
m_state(Idle), m_flags(0), m_xmlns(XMPPNamespace::Count), m_lastEvent(0),
m_setupTimeout(0), m_startTimeout(0),
m_pingTimeout(0), m_nextPing(0),
m_idleTimeout(0), m_connectTimeout(0),
m_restart(0), m_timeToFillRestart(0),
m_engine(engine), m_type(t),
m_incoming(true), m_terminateEvent(0),
m_xmlDom(0), m_socket(0), m_socketFlags(0), m_connectPort(0)
{
m_engine->buildStreamName(m_name);
debugName(m_name);
debugChain(m_engine);
Debug(this,DebugAll,"JBStream::JBStream(%p,%p,%s) incoming [%p]",
engine,socket,typeName(),this);
setXmlns();
// Don't restart incoming streams
m_flags |= NoAutoRestart;
resetConnection(socket);
changeState(WaitStart);
}
// Outgoing
JBStream::JBStream(JBEngine* engine, Type t, const JabberID& local, const JabberID& remote,
const char* name, const NamedList* params)
: Mutex(true,"JBStream"),
m_sasl(0),
m_state(Idle), m_local(local), m_remote(remote),
m_flags(0), m_xmlns(XMPPNamespace::Count), m_lastEvent(0),
m_setupTimeout(0), m_startTimeout(0),
m_pingTimeout(0), m_nextPing(0),
m_idleTimeout(0), m_connectTimeout(0),
m_restart(1), m_timeToFillRestart(0),
m_engine(engine), m_type(t),
m_incoming(false), m_name(name),
m_terminateEvent(0),
m_xmlDom(0), m_socket(0), m_socketFlags(0), m_connectPort(0)
{
if (!m_name)
m_engine->buildStreamName(m_name);
debugName(m_name);
debugChain(m_engine);
if (params) {
int flgs = XMPPUtils::decodeFlags(params->getValue("options"),s_flagName);
m_flags = flgs & StreamFlags;
m_connectAddr = params->getValue("server",params->getValue("address"));
m_connectPort = params->getIntValue("port");
}
else
updateFromRemoteDef();
Debug(this,DebugAll,"JBStream::JBStream(%p,%s,%s,%s) outgoing [%p]",
engine,typeName(),local.c_str(),remote.c_str(),this);
setXmlns();
changeState(Idle);
}
// Destructor
JBStream::~JBStream()
{
DDebug(this,DebugAll,"JBStream::~JBStream() id=%s [%p]",m_name.c_str(),this);
TelEngine::destruct(m_sasl);
}
// Outgoing stream connect terminated notification.
void JBStream::connectTerminated(Socket*& sock)
{
Lock lock(this);
if (m_state == Connecting) {
if (sock) {
resetConnection(sock);
sock = 0;
changeState(Starting);
start();
}
else {
DDebug(this,DebugNote,"Connect failed [%p]",this);
terminate(0,false,0,XMPPError::NoRemote);
}
return;
}
DDebug(this,DebugInfo,"Connect terminated notification in non %s state [%p]",
lookup(Connecting,s_stateName),this);
if (sock) {
delete sock;
sock = 0;
}
}
// Get an object from this stream
void* JBStream::getObject(const String& name) const
{
if (name == "Socket*")
return state() == Securing ? (void*)&m_socket : 0;
if (name == "JBStream")
return (void*)this;
return RefObject::getObject(name);
}
// Get the string representation of this stream
const String& JBStream::toString() const
{
return m_name;
}
// Set/reset RosterRequested flag
void JBStream::setRosterRequested(bool ok)
{
Lock lock(this);
if (ok == flag(RosterRequested))
return;
if (ok)
m_flags |= RosterRequested;
else
m_flags &= ~RosterRequested;
XDebug(this,DebugAll,"%s roster requested flag [%p]",ok ? "Set" : "Reset",this);
}
// Set/reset AvailableResource/PositivePriority flags
bool JBStream::setAvailableResource(bool ok, bool positive)
{
Lock lock(this);
if (ok && positive)
m_flags |= PositivePriority;
else
m_flags &= ~PositivePriority;
if (ok == flag(AvailableResource))
return false;
if (ok)
m_flags |= AvailableResource;
else
m_flags &= ~AvailableResource;
XDebug(this,DebugAll,"%s available resource flag [%p]",ok ? "Set" : "Reset",this);
return true;
}
// Read data from socket. Send it to the parser
bool JBStream::readSocket(char* buf, unsigned int len)
{
if (!(buf && len > 1))
return false;
if (!socketCanRead())
return false;
Lock lock(this);
if (!socketCanRead() || state() == Destroy || state() == Idle || state() == Connecting)
return false;
socketSetReading(true);
if (state() != WaitTlsRsp)
len--;
else
len = 1;
lock.drop();
XMPPError::Type error = XMPPError::NoError;
int read = m_socket->readData(buf,len);
Lock lck(this);
// Check if something changed
if (!(m_socket && socketReading())) {
Debug(this,DebugAll,"Socket deleted while reading [%p]",this);
return false;
}
if (read && read != Socket::socketError()) {
buf[read] = 0;
XDebug(this,DebugInfo,"Received %s [%p]",buf,this);
if (!m_xmlDom->parse(buf)) {
if (m_xmlDom->error() != XmlSaxParser::Incomplete)
error = XMPPError::Xml;
else if (m_xmlDom->buffer().length() > m_engine->m_maxIncompleteXml)
error = XMPPError::Policy;
}
}
socketSetReading(false);
if (read) {
if (read == Socket::socketError()) {
if (m_socket->canRetry()) {
read = 0;
#ifdef XDEBUG
String tmp;
Thread::errorString(tmp,m_socket->error());
Debug(this,DebugAll,"Socket temporary unavailable for read. %d: '%s' [%p]",
m_socket->error(),tmp.c_str(),this);
#endif
}
else
error = XMPPError::SocketError;
}
}
else
error = XMPPError::SocketError;
if (error == XMPPError::NoError) {
// Stop reading if waiting for TLS start and received a complete element
// We'll wait for the stream processor to handle the received element
if (read && state() == WaitTlsRsp && !m_xmlDom->buffer().length() &&
m_xmlDom->unparsed() == XmlSaxParser::None) {
XmlDocument* doc = m_xmlDom->document();
// If received a complete element, the parser's current element is
// the document's root
if (doc && m_xmlDom->isCurrent(doc->root())) {
DDebug(this,DebugAll,"Received complete element in state=%s. Stop reading [%p]",
stateName(),this);
socketSetCanRead(false);
}
}
return read > 0;
}
// Error
int location = 0;
const char* reason = 0;
if (error != XMPPError::SocketError) {
String tmp;
if (error == XMPPError::Xml) {
reason = m_xmlDom->getError();
tmp = m_xmlDom->buffer();
}
else {
tmp << "overflow len=" << m_xmlDom->buffer().length() << " max=" <<
m_engine->m_maxIncompleteXml;
reason = "XML element too long";
}
Debug(this,DebugNote,"Parser error='%s' buffer='%s' [%p]",
reason,tmp.c_str(),this);
}
else if (read) {
String tmp;
Thread::errorString(tmp,m_socket->error());
Debug(this,DebugWarn,"Socket read error: %d: '%s' [%p]",m_socket->error(),
tmp.c_str(),this);
}
else {
Debug(this,DebugInfo,"Stream EOF [%p]",this);
location = 1;
}
lck.drop();
terminate(location,m_incoming,0,error,reason);
return read > 0;
}
// Stream state processor
JBEvent* JBStream::getEvent(u_int64_t time)
{
if (m_lastEvent)
return 0;
Lock lock(this);
if (m_lastEvent)
return 0;
XDebug(this,DebugAll,"JBStream::getEvent() [%p]",this);
checkPendingEvent();
if (!m_lastEvent) {
if (canProcess(time)) {
process(time);
checkPendingEvent();
if (!m_lastEvent)
checkTimeouts(time);
}
else
checkPendingEvent();
}
#ifdef XDEBUG
if (m_lastEvent)
Debug(this,DebugAll,"Generating event (%p,%s) in state '%s' [%p]",
m_lastEvent,m_lastEvent->name(),stateName(),this);
#endif
return m_lastEvent;
}
// Send a stanza ('iq', 'message' or 'presence') in Running state.
bool JBStream::sendStanza(XmlElement*& xml)
{
if (!xml)
return false;
DDebug(this,DebugAll,"sendStanza(%p) '%s' [%p]",xml,xml->tag(),this);
if (!XMPPUtils::isStanza(*xml)) {
Debug(this,DebugNote,"Request to send non stanza xml='%s' [%p]",xml->tag(),this);
TelEngine::destruct(xml);
return false;
}
Lock lock(this);
m_pending.append(new XmlElementOut(xml));
xml = 0;
sendPending();
return true;
}
// Send stream related XML when negotiating the stream
// or some other stanza in non Running state
bool JBStream::sendStreamXml(State newState, XmlElement* first, XmlElement* second,
XmlElement* third)
{
DDebug(this,DebugAll,"sendStreamXml(%s,%p,%p,%p) [%p]",
stateName(),first,second,third,this);
Lock lock(this);
bool ok = false;
XmlFragment frag;
// Use a while() to break to the end: safe cleanup
while (true) {
if (m_state == Idle || m_state == Destroy)
break;
// Check if we have unsent stream xml
if (m_outStreamXml)
sendPending(true);
if (m_outStreamXml)
break;
if (!first)
break;
// Add stream declaration before stream start
if (first->getTag() == XMPPUtils::s_tag[XmlTag::Stream] &&
first->tag()[0] != '/') {
XmlDeclaration* decl = new XmlDeclaration;
decl->toString(m_outStreamXml,true);
frag.addChild(decl);
}
first->toString(m_outStreamXml,true,String::empty(),String::empty(),false);
frag.addChild(first);
if (second) {
second->toString(m_outStreamXml,true,String::empty(),String::empty(),false);
frag.addChild(second);
if (third) {
third->toString(m_outStreamXml,true,String::empty(),String::empty(),false);
frag.addChild(third);
}
}
first = second = third = 0;
m_engine->printXml(this,true,frag);
ok = sendPending(true);
break;
}
TelEngine::destruct(first);
TelEngine::destruct(second);
TelEngine::destruct(third);
if (ok)
changeState(newState);
return ok;
}
// Start the stream. This method should be called by the upper layer
// when processing an incoming stream Start event. For outgoing streams
// this method is called internally on succesfully connect.
void JBStream::start(XMPPFeatureList* features, XmlElement* caps)
{
Lock lock(this);
if (m_state != Starting)
return;
if (outgoing()) {
TelEngine::destruct(caps);
XmlElement* s = buildStreamStart();
sendStreamXml(WaitStart,s);
return;
}
m_features.clear();
if (features)
m_features.add(*features);
// Set secured flag if we don't advertise TLS
if (!(flag(StreamSecured) || m_features.get(XMPPNamespace::Tls)))
setSecured();
// Set authenticated flag if we don't advertise authentication mechanisms
if (flag(StreamSecured)) {
if (flag(StreamAuthenticated))
m_features.remove(XMPPNamespace::Sasl);
else if (!m_features.get(XMPPNamespace::Sasl))
m_flags |= JBStream::StreamAuthenticated;
}
// Send start and features
XmlElement* s = buildStreamStart();
XmlElement* f = 0;
if (flag(StreamRemoteVer1))
f = m_features.buildStreamFeatures();
if (f && caps)
f->addChild(caps);
else
TelEngine::destruct(caps);
State newState = Features;
if (m_type == c2s) {
// Change stream state to Running if authenticated and there is no required
// feature to negotiate
if (flag(StreamAuthenticated) && !firstRequiredFeature())
newState = Running;
}
sendStreamXml(newState,s,f);
}
// Authenticate an incoming stream
bool JBStream::authenticated(bool ok, const String& rsp, XMPPError::Type error)
{
Lock lock(this);
if (m_state != Auth || !incoming())
return false;
DDebug(this,DebugAll,"authenticated(%s,'%s',%s) local=%s [%p]",
String::boolText(ok),rsp.safe(),XMPPUtils::s_error[error].c_str(),
m_local.c_str(),this);
if (ok) {
m_flags |= StreamAuthenticated;
if (m_type == c2s) {
// Set remote party node if not provided
if (m_type == c2s && m_sasl && m_sasl->m_params && !m_remote.node()) {
m_remote.set(m_sasl->m_params->getValue("username"),m_local.domain(),"");
Debug(this,DebugAll,"Remote party set to '%s' [%p]",m_remote.c_str(),this);
}
m_features.remove(XMPPNamespace::Sasl);
String text;
if (m_sasl)
m_sasl->buildAuthRspReply(text,rsp);
XmlElement* s = XMPPUtils::createElement(XmlTag::Success,
XMPPNamespace::Sasl,text);
ok = sendStreamXml(WaitStart,s);
}
else if (m_type == s2s) {
XmlElement* rsp = XMPPUtils::createDialbackResult(m_local,m_remote,true);
ok = sendStreamXml(Running,rsp);
}
}
else {
if (m_type == c2s) {
XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl,error);
ok = sendStreamXml(Features,failure);
}
else if (m_type == s2s) {
XmlElement* rsp = XMPPUtils::createDialbackResult(m_local,m_remote,false);
ok = sendStreamXml(state(),rsp);
if (ok)
terminate(0,true,0,XMPPError::NotAuthorized);
}
}
TelEngine::destruct(m_sasl);
return ok;
}
// Terminate the stream. Send stream end tag or error.
// Reset the stream. Deref stream if destroying.
void JBStream::terminate(int location, bool destroy, XmlElement* xml, int error,
const char* reason, bool final)
{
XDebug(this,DebugAll,"terminate(%d,%u,%p,%u,%s,%u) state=%s [%p]",
location,destroy,xml,error,reason,final,stateName(),this);
Lock lock(this);
m_pending.clear();
// Already in destroy
if (state() == Destroy) {
TelEngine::destruct(xml);
return;
}
bool sendEndTag = true;
destroy = destroy || final || flag(NoAutoRestart);
if (error == XMPPError::NoError && m_engine->exiting())
error = XMPPError::Shutdown;
// Last check for sendEndTag
if (sendEndTag) {
// Prohibitted states or socket read/write error
if (m_state == Destroy || m_state == Securing || m_state == Connecting)
sendEndTag = false;
else if (error == XMPPError::SocketError) {
sendEndTag = false;
reason = "I/O error";
}
}
Debug(this,DebugAll,
"Terminate by '%s' state=%s destroy=%u error=%s reason='%s' final=%u [%p]",
lookup(location,s_location),stateName(),destroy,
XMPPUtils::s_error[error].c_str(),reason,final,this);
if (sendEndTag) {
XmlElement* start = 0;
if (m_state == Starting && incoming())
start = buildStreamStart();
XmlElement* end = new XmlElement(String("/stream:stream"),false);
if (error != XMPPError::NoError && location < 1) {
XmlElement* e = XMPPUtils::createStreamError(error,reason);
if (!start)
sendStreamXml(m_state,e,end);
else
sendStreamXml(m_state,start,e,end);
}
else {
if (!start)
sendStreamXml(m_state,end);
else
sendStreamXml(m_state,start,end);
}
}
resetConnection();
m_outStreamXml.clear();
// Always set termination event, except when called from destructor
if (!(final || m_terminateEvent)) {
// TODO: Cancel all outgoing elements without id
m_terminateEvent = new JBEvent(destroy ? JBEvent::Destroy : JBEvent::Terminated,
this,xml);
xml = 0;
if (!m_terminateEvent->m_text) {
if (TelEngine::null(reason))
m_terminateEvent->m_text = XMPPUtils::s_error[error];
else
m_terminateEvent->m_text = reason;
}
}
TelEngine::destruct(xml);
changeState(destroy ? Destroy : Idle);
}
// Close the stream. Release memory
void JBStream::destroyed()
{
terminate(0,true,0,XMPPError::NoError,"",true);
resetConnection();
if (m_engine)
m_engine->removeStream(this,false);
TelEngine::destruct(m_terminateEvent);
DDebug(this,DebugAll,"Destroyed local=%s remote=%s [%p]",
m_local.safe(),m_remote.safe(),this);
RefObject::destroyed();
}
// Check if stream state processor can continue
// This method is called from getEvent() with the stream locked
bool JBStream::canProcess(u_int64_t time)
{
if (outgoing()) {
// Increase stream restart counter if it's time to and should auto restart
if (!flag(NoAutoRestart) && m_timeToFillRestart < time) {
m_timeToFillRestart = time + m_engine->m_restartUpdInterval;
if (m_restart < m_engine->m_restartMax) {
m_restart++;
Debug(this,DebugAll,"Restart count set to %u max=%u [%p]",
m_restart,m_engine->m_restartMax,this);
}
}
if (state() == Idle) {
// Re-connect
if (m_restart) {
changeState(Connecting);
m_restart--;
m_engine->connectStream(this);
return false;
}
// Destroy if not auto-restarting
if (flag(NoAutoRestart)) {
terminate(0,true,0);
return false;
}
}
}
else if (state() == Idle && flag(NoAutoRestart)) {
terminate(0,true,0);
return false;
}
return true;
}
// Process stream state. Get XML from parser's queue and process it
// This method is called from getEvent() with the stream locked
void JBStream::process(u_int64_t time)
{
if (!m_xmlDom)
return;
XDebug(this,DebugAll,"JBStream::process() [%p]",this);
XmlDocument* doc = m_xmlDom->document();
if (!doc) {
Debug(this,DebugGoOn,"The parser is not a document! [%p]",this);
terminate(0,true,0,XMPPError::Internal);
return;
}
XmlElement* root = doc->root(false);
while (true) {
sendPending();
if (m_terminateEvent || !root)
break;
// Check for stream termination
if (root->completed()) {
bool error = false;
// Check if received an error
XmlElement* xml = 0;
while (0 != (xml = root->pop())) {
if (streamError(xml)) {
error = true;
break;
}
TelEngine::destruct(xml);
}
if (error)
break;
Debug(this,DebugAll,"Remote closed the stream in state %s [%p]",
stateName(),this);
terminate(1,false,0);
break;
}
if (m_state == WaitStart) {
// Print the declaration
XmlDeclaration* dec = doc->declaration();
if (dec)
m_engine->printXml(this,false,*dec);
// Print the root. Make sure we don't print its children
if (!root->getChildren().skipNull())
m_engine->printXml(this,false,*root);
else {
XmlElement tmp(*root);
tmp.clearChildren();
m_engine->printXml(this,false,tmp);
}
// Check if valid
if (!XMPPUtils::isTag(*root,XmlTag::Stream,XMPPNamespace::Stream)) {
String* ns = root->xmlns();
Debug(this,DebugMild,"Received invalid stream root '%s' namespace='%s' [%p]",
root->tag(),TelEngine::c_safe(ns),this);
terminate(0,true,0);
break;
}
// Check 'from' and 'to'
JabberID from;
JabberID to;
if (!getJids(root,from,to))
break;
XDebug(this,DebugAll,"Processing (%p,%s) in state %s [%p]",
root,root->tag(),stateName(),this);
processStart(root,from,to);
break;
}
XmlElement* xml = root->pop();
if (!xml)
break;
// Process received element
// Print it
m_engine->printXml(this,false,*xml);
// Check stream termination
if (streamError(xml))
break;
// Check 'from' and 'to'
JabberID from;
JabberID to;
if (!getJids(xml,from,to))
break;
// Restart the idle timer
if (m_state == Running && m_engine->m_idleTimeout)
m_idleTimeout = time + m_engine->m_idleTimeout;
// Check if a received stanza is valid and allowed in current state
if (!checkStanzaRecv(xml,from,to))
break;
XDebug(m_engine,DebugAll,"Processing (%p,%s) in state %s [%p]",
xml,xml->tag(),stateName(),this);
// Process here dialback verify
if (m_type == s2s && isDbVerify(*xml)) {
switch (state()) {
case Running:
case Features:
case Starting:
case Challenge:
case Auth:
m_events.append(new JBEvent(JBEvent::DbVerify,this,xml,from,to));
break;
default:
dropXml(xml,"dialback verify in unsupported state");
}
continue;
}
switch (m_state) {
case Running:
processRunning(xml,from,to);
break;
case Features:
if (m_incoming)
processFeaturesIn(xml,from,to);
else
processFeaturesOut(xml,from,to);
break;
case WaitStart:
case Starting:
processStart(xml,from,to);
TelEngine::destruct(xml);
break;
case Challenge:
processChallenge(xml,from,to);
break;
case Auth:
processAuth(xml,from,to);
break;
case WaitTlsRsp:
processWaitTlsRsp(xml,from,to);
break;
case Register:
processRegister(xml,from,to);
break;
default:
dropXml(xml,"unhandled stream state in process()");
}
break;
}
XDebug(this,DebugAll,"JBStream::process() exiting [%p]",this);
}
// Process elements in Running state
bool JBStream::processRunning(XmlElement* xml, const JabberID& from, const JabberID& to)
{
if (!xml)
return true;
int t, ns;
if (!XMPPUtils::getTag(*xml,t,ns))
return dropXml(xml,"failed to retrieve element tag");
switch (t) {
case XmlTag::Message:
if (ns != m_xmlns)
break;
m_events.append(new JBEvent(JBEvent::Message,this,xml,from,to));
return true;
case XmlTag::Presence:
if (ns != m_xmlns)
break;
m_events.append(new JBEvent(JBEvent::Presence,this,xml,from,to));
return true;
case XmlTag::Iq:
if (ns != m_xmlns)
break;
m_events.append(new JBEvent(JBEvent::Iq,this,xml,from,to,xml->findFirstChild()));
return true;
default:
m_events.append(new JBEvent(JBEvent::Unknown,this,xml,from,to));
return true;
}
// Invalid stanza namespace
XmlElement* rsp = XMPPUtils::createError(xml,XMPPError::TypeModify,
XMPPError::InvalidNamespace,"Only stanzas in default namespace are allowed");
sendStanza(rsp);
return true;
}
// Check stream timeouts
// This method is called from getEvent() with the stream locked, after
void JBStream::checkTimeouts(u_int64_t time)
{
// Running: check ping and idle timers
if (m_state == Running) {
if (m_pingTimeout) {
if (m_pingTimeout < time)
terminate(0,false,0,XMPPError::ConnTimeout,"Ping timeout");
}
else if (m_nextPing && time >= m_nextPing) {
m_pingId = (unsigned int)time;
// TODO: Send it
Debug(this,DebugStub,"JBStream::checkTimeouts() sendPing() not implemented");
}
else if (m_idleTimeout && m_idleTimeout < time)
terminate(0,true,0,XMPPError::ConnTimeout,"Stream idle");
return;
}
// Stream setup timer
if (m_setupTimeout && m_setupTimeout < time) {
terminate(0,m_incoming,0,XMPPError::Policy,"Stream setup timeout");
return;
}
// Stream start timer
if (m_startTimeout && m_startTimeout < time) {
terminate(0,m_incoming,0,XMPPError::Policy,"Stream start timeout");
return;
}
// Stream connect timer
if (m_connectTimeout && m_connectTimeout < time) {
terminate(0,m_incoming,0,XMPPError::ConnTimeout,"Stream connect timeout");
return;
}
}
// Reset the stream's connection. Build a new XML parser if the socket is valid
void JBStream::resetConnection(Socket* sock)
{
XDebug(this,DebugAll,"JBStream::resetConnection(%p) [%p]",sock,this);
// Release the old one
if (m_socket) {
// Wait for the socket to become available (not reading or writing)
Socket* tmp = 0;
while (true) {
Lock lock(this);
if (!(m_socket && (socketReading() || socketWriting()))) {
tmp = m_socket;
m_socket = 0;
m_socketFlags = 0;
if (m_xmlDom) {
delete m_xmlDom;
m_xmlDom = 0;
}
break;
}
lock.drop();
Thread::yield(false);
}
if (tmp) {
tmp->setLinger(-1);
tmp->terminate();
delete tmp;
}
}
if (sock) {
Lock lock(this);
if (m_socket) {
Debug(this,DebugWarn,"Duplicate attempt to set socket! [%p]",this);
delete sock;
return;
}
m_xmlDom = new XmlDomParser(debugName());
m_xmlDom->debugChain(this);
m_socket = sock;
if (debugAt(DebugAll)) {
SocketAddr l, r;
localAddr(l);
remoteAddr(r);
Debug(this,DebugAll,"Connection set local=%s:%d remote=%s:%d [%p]",
l.host().c_str(),l.port(),r.host().c_str(),r.port(),this);
}
m_socket->setReuse(true);
m_socket->setBlocking(false);
socketSetCanRead(true);
}
}
// Build a stream start XML element
XmlElement* JBStream::buildStreamStart()
{
XmlElement* start = new XmlElement(XMPPUtils::s_tag[XmlTag::Stream],false);
if (incoming())
start->setAttribute("id",m_id);
XMPPUtils::setStreamXmlns(*start);
start->setAttribute(XmlElement::s_ns,XMPPUtils::s_ns[m_xmlns]);
start->setAttributeValid("from",m_local.bare());
start->setAttributeValid("to",m_remote.bare());
start->setAttribute("version","1.0");
start->setAttribute("xml:lang","en");
return start;
}
// Process received elements in WaitStart state
// WaitStart: Incoming: waiting for stream start
// Outgoing: idem (our stream start was already sent)
// Return false if stream termination was initiated
bool JBStream::processStart(const XmlElement* xml, const JabberID& from,
const JabberID& to)
{
Debug(this,DebugStub,"JBStream::processStart(%s) [%p]",xml->tag(),this);
return true;
}
// Process elements in Register state
bool JBStream::processRegister(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
dropXml(xml,"can't process in this state");
terminate(0,true,0,XMPPError::Internal);
return false;
}
// Process elements in Auth state
bool JBStream::processAuth(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
return dropXml(xml,"can't process in this state");
}
// Check if a received start start element's namespaces are correct.
bool JBStream::processStreamStart(const XmlElement* xml)
{
if (m_state == Starting)
return true;
changeState(Starting);
if (!XMPPUtils::hasDefaultXmlns(*xml,m_xmlns)) {
Debug(this,DebugNote,"Received '%s' with invalid xmlns='%s' [%p]",
xml->tag(),TelEngine::c_safe(xml->xmlns()),this);
terminate(0,m_incoming,0,XMPPError::InvalidNamespace);
return false;
}
XMPPError::Type error = XMPPError::NoError;
const char* reason = 0;
while (true) {
if (m_type != c2s && m_type != s2s) {
Debug(this,DebugStub,"processStreamStart() type %u not handled!",m_type);
error = XMPPError::Internal;
break;
}
// Check xmlns:stream
String* nsStr = xml->getAttribute("xmlns:stream");
if (!nsStr || *nsStr != XMPPUtils::s_ns[XMPPNamespace::Stream]) {
Debug(this,DebugNote,"Received '%s' with invalid xmlns:stream='%s' [%p]",
xml->tag(),TelEngine::c_safe(nsStr),this);
error = XMPPError::InvalidNamespace;
break;
}
// Check version
String ver(xml->getAttribute("version"));
int remoteVersion = -1;
if (ver) {
int pos = ver.find('.');
if (pos > 0)
remoteVersion = ver.substr(0,pos).toInteger(-1);
}
if (remoteVersion == 1)
m_flags |= StreamRemoteVer1;
else if (remoteVersion < 1) {
if (m_type == c2s)
error = XMPPError::UnsupportedVersion;
else if (m_type == s2s) {
// Accept invalid/unsupported version on if TLS is not required
if (!flag(TlsRequired)) {
// Check dialback
if (!xml->hasAttribute("xmlns:db",XMPPUtils::s_ns[XMPPNamespace::Dialback])) {
Debug(this,DebugNote,"Received non dialback '%s' [%p]",
xml->tag(),this);
error = XMPPError::InvalidNamespace;
}
}
else
error = XMPPError::EncryptionRequired;
}
else
error = XMPPError::Internal;
}
else if (remoteVersion > 1)
error = XMPPError::UnsupportedVersion;
if (error != XMPPError::NoError) {
Debug(this,DebugNote,"Received '%s' with unacceptable version='%s' [%p]",
xml->tag(),ver.c_str(),this);
break;
}
// Set stream id: generate one for incoming, get it from xml if outgoing
if (incoming()) {
// Generate a random, variable length stream id
MD5 md5(String((int)(int64_t)this));
md5 << m_name << String((int)Time::msecNow());
m_id = md5.hexDigest();
m_id << "_" << String((int)::random());
}
else {
m_id = xml->getAttribute("id");
if (!m_id)
reason = "Missing stream id";
else if (m_engine->checkDupId(this))
reason = "Duplicate stream id";
if (reason) {
Debug(this,DebugNote,"Received '%s' with invalid stream id='%s' [%p]",
xml->tag(),m_id.c_str(),this);
error = XMPPError::InvalidId;
break;
}
}
XDebug(this,DebugAll,"Stream id set to '%s' [%p]",m_id.c_str(),this);
break;
}
if (error == XMPPError::NoError)
return true;
terminate(0,m_incoming,0,error,reason);
return false;
}
// Check if a received element is a stream error one
bool JBStream::streamError(XmlElement* xml)
{
if (!(xml && XMPPUtils::isTag(*xml,XmlTag::Error,XMPPNamespace::Stream)))
return false;
String text;
String error;
XMPPUtils::decodeError(xml,false,error,text);
Debug(this,DebugAll,"Received stream error '%s' text='%s' in state %s [%p]",
error.c_str(),text.c_str(),stateName(),this);
int err = XMPPUtils::s_error[error];
if (err >= XMPPError::Count)
err = XMPPError::NoError;
terminate(1,false,xml,err,text);
return true;
}
// Check if a received element has valid 'from' and 'to' jid attributes
bool JBStream::getJids(XmlElement* xml, JabberID& from, JabberID& to)
{
if (!xml)
return true;
from = xml->getAttribute("from");
to = xml->getAttribute("to");
XDebug(this,DebugAll,"Got jids xml='%s' from='%s' to='%s' [%p]",
xml->tag(),from.c_str(),to.c_str(),this);
if (to.valid() && from.valid())
return true;
Debug(this,DebugNote,"Received '%s' with bad from='%s' or to='%s' [%p]",
xml->tag(),from.c_str(),to.c_str(),this);
terminate(0,m_incoming,xml,XMPPError::BadAddressing);
return false;
}
// Check if a received element is a presence, message or iq qualified by the stream
// namespace and the stream is not authenticated
// Validate 'from' for c2s streams
// Validate s2s 'to' domain and 'from' jid
bool JBStream::checkStanzaRecv(XmlElement* xml, JabberID& from, JabberID& to)
{
if (!XMPPUtils::isStanza(*xml))
return true;
// RFC 3920bis 5.2: Accept stanzas only if the stream was authenticated
// Accept IQs in jabber:iq:register namespace
// They might be received on a non authenticated stream)
if (!flag(StreamAuthenticated)) {
bool isIq = XMPPUtils::isTag(*xml,XmlTag::Iq,m_xmlns);
bool valid = isIq && XMPPUtils::findFirstChild(*xml,XmlTag::Count,
XMPPNamespace::IqRegister);
// Outgoing client stream: check register responses
if (!valid && outgoing()) {
JBClientStream* c2s = clientStream();
valid = c2s && c2s->isRegisterId(*xml);
}
if (!valid) {
terminate(0,false,xml,XMPPError::NotAuthorized,
"Can't accept stanza on non authorized stream");
return false;
}
}
switch (m_type) {
case c2s:
if (m_incoming) {
// Check for valid from
if (from && !m_remote.match(from)) {
XmlElement* e = XMPPUtils::createError(xml,
XMPPError::TypeModify,XMPPError::BadAddressing);
sendStanza(e);
return false;
}
// Make sure the upper layer always has the full jid
if (!from)
from = m_remote;
else if (!from.resource())
from.resource(m_remote.resource());
}
else {
XDebug(this,DebugStub,
"Possible checkStanzaRecv() unhandled outgoing c2s stream [%p]",this);
}
break;
case s2s:
// RFC 3920bis 9.1.1.2 and 9.1.2.1:
// Validate 'to' and 'from'
if (!(to && from)) {
terminate(0,m_incoming,xml,XMPPError::BadAddressing);
return false;
}
if (!m_engine->hasDomain(to.domain())) {
terminate(0,m_incoming,xml,XMPPError::HostUnknown);
return false;
}
if (from.domain() != m_remote.domain()) {
terminate(0,m_incoming,xml,XMPPError::InvalidFrom);
return false;
}
break;
default:
Debug(this,DebugStub,"checkStanzaRecv() unhandled stream type=%s [%p]",
typeName(),this);
}
return true;
}
// Change stream state. Reset state depending data
void JBStream::changeState(State newState, u_int64_t time)
{
if (newState == m_state)
return;
DDebug(this,DebugAll,"Changing state from '%s' to '%s' [%p]",
stateName(),lookup(newState,s_stateName),this);
// Always reset the idle timer: something happened
m_idleTimeout = m_engine->m_idleTimeout ? time + m_engine->m_idleTimeout : 0;
// Set/reset state depending data
switch (m_state) {
case WaitStart:
m_startTimeout = 0;
break;
case Securing:
m_flags |= StreamSecured;
socketSetCanRead(true);
break;
case Connecting:
m_connectTimeout = 0;
break;
case Register:
if (type() == c2s)
clientStream()->m_registerReq = 0;
break;
default: ;
}
switch (newState) {
case WaitStart:
if (m_engine->m_setupTimeout)
m_setupTimeout = time + timerMultiplier(this) * m_engine->m_setupTimeout;
else
m_setupTimeout = 0;
m_startTimeout = time + timerMultiplier(this) * m_engine->m_startTimeout;
DDebug(this,DebugAll,"Set timeouts start=" FMT64 " setup=" FMT64 " [%p]",
m_startTimeout,m_setupTimeout,this);
if (m_xmlDom) {
m_xmlDom->reset();
DDebug(this,DebugAll,"XML parser reset [%p]",this);
}
break;
case Idle:
m_events.clear();
case Destroy:
m_id = "";
m_setupTimeout = 0;
m_startTimeout = 0;
// Reset all internal flags
m_flags &= ~InternalFlags;
if (type() == c2s)
clientStream()->m_registerReq = 0;
break;
case Running:
m_flags |= StreamSecured | StreamAuthenticated;
m_setupTimeout = 0;
m_startTimeout = 0;
if (m_state != Running)
m_events.append(new JBEvent(JBEvent::Running,this,0));
break;
case Connecting:
if (m_engine->m_connectTimeout)
m_connectTimeout = time + m_engine->m_connectTimeout;
else
m_connectTimeout = 0;
DDebug(this,DebugAll,"Set connect timeout " FMT64 " [%p]",m_connectTimeout,this);
break;
case Securing:
socketSetCanRead(false);
break;
default: ;
}
m_state = newState;
}
// Check for pending events. Set the last event
void JBStream::checkPendingEvent()
{
if (m_lastEvent)
return;
if (!m_terminateEvent) {
GenObject* gen = m_events.remove(false);
if (gen)
m_lastEvent = static_cast<JBEvent*>(gen);
return;
}
// Check for register events and raise them before the terminate event
for (ObjList* o = m_events.skipNull(); o; o = o->skipNext()) {
JBEvent* ev = static_cast<JBEvent*>(o->get());
if (ev->type() == JBEvent::RegisterOk || ev->type() == JBEvent::RegisterFailed) {
m_lastEvent = ev;
m_events.remove(ev,false);
return;
}
}
m_lastEvent = m_terminateEvent;
m_terminateEvent = 0;
}
// Send pending stream XML or stanzas
bool JBStream::sendPending(bool streamOnly)
{
if (!m_socket)
return false;
XDebug(this,DebugAll,"JBStream::sendPending() [%p]",this);
// Always try to send pending stream XML first
if (m_outStreamXml) {
unsigned int len = m_outStreamXml.length();
if (!writeSocket(m_outStreamXml.c_str(),len)) {
terminate(0,m_incoming,0,XMPPError::SocketError);
return false;
}
bool all = (len == m_outStreamXml.length());
if (all)
m_outStreamXml.clear();
else
m_outStreamXml = m_outStreamXml.substr(len);
// Start TLS now for incoming streams
if (m_incoming && m_state == Securing) {
if (all) {
m_engine->encryptStream(this);
m_flags |= StreamTls;
socketSetCanRead(true);
}
return true;
}
if (streamOnly || !all)
return true;
}
// Send pending stanzas
if (m_state != Running || streamOnly)
return true;
ObjList* obj = m_pending.skipNull();
if (!obj)
return true;
XmlElementOut* eout = static_cast<XmlElementOut*>(obj->get());
XmlElement* xml = eout->element();
if (!xml) {
m_pending.remove(eout,true);
return true;
}
// Print the element only if it's the first time we try to send it
if (!eout->sent())
m_engine->printXml(this,true,*xml);
u_int32_t len;
const char* data = eout->getData(len);
if (writeSocket(data,len)) {
// Adjust element's buffer. Remove it from list on completion
eout->dataSent(len);
unsigned int rest = eout->dataCount();
if (!rest) {
DDebug(this,DebugAll,"Sent element (%p,%s) [%p]",xml,xml->tag(),this);
m_pending.remove(eout,true);
}
else
DDebug(this,DebugAll,"Partially sent element (%p,%s) sent=%u rest=%u [%p]",
xml,xml->tag(),len,rest,this);
return true;
}
// Error
Debug(this,DebugNote,"Failed to send (%p,%s) in state=%s [%p]",
xml,xml->tag(),stateName(),this);
terminate(0,m_incoming,0,XMPPError::SocketError);
return false;
}
// Write data to socket
bool JBStream::writeSocket(const char* data, unsigned int& len)
{
if (!(data && m_socket)) {
len = 0;
return m_socket != 0;
}
Lock lock(this);
if (!m_socket) {
len = 0;
return false;
}
socketSetWriting(true);
lock.drop();
XDebug(this,DebugInfo,"Sending %s [%p]",data,this);
int w = m_socket->writeData(data,len);
if (w != Socket::socketError())
len = w;
else
len = 0;
#ifdef XDEBUG
String sent(data,len);
Debug(this,DebugInfo,"Sent %s [%p]",sent.c_str(),this);
#endif
Lock lck(this);
// Check if something changed
if (!(m_socket && socketWriting())) {
Debug(this,DebugAll,"Socket deleted while writing [%p]",this);
return true;
}
socketSetWriting(false);
if (w != Socket::socketError() || m_socket->canRetry())
return true;
String tmp;
Thread::errorString(tmp,m_socket->error());
Debug(this,DebugWarn,"Socket send error: %d: '%s' [%p]",
m_socket->error(),tmp.c_str(),this);
lck.drop();
// Terminate the connection now: avoid loop back
resetConnection();
return false;
}
// Update stream flags and remote connection data from engine
void JBStream::updateFromRemoteDef()
{
m_engine->lock();
JBRemoteDomainDef* domain = m_engine->remoteDomainDef(m_remote.domain());
// Update flags
m_flags = (domain->m_flags & StreamFlags);
// Update connection data
if (outgoing() && state() == Idle) {
m_connectAddr = domain->m_address;
m_connectPort = domain->m_port;
}
m_engine->unlock();
}
// Retrieve the first required feature in the list
XMPPFeature* JBStream::firstRequiredFeature()
{
for (ObjList* o = m_features.skipNull(); o; o = o->skipNext()) {
XMPPFeature* f = static_cast<XMPPFeature*>(o->get());
if (f->required())
return f;
}
return 0;
}
// Drop (delete) received XML element
bool JBStream::dropXml(XmlElement*& xml, const char* reason)
{
if (!xml)
return true;
Debug(this,DebugStub,"Dropping xml=(%p,%s) ns=%s in state=%s reason='%s' [%p]",
xml,xml->tag(),TelEngine::c_safe(xml->xmlns()),stateName(),reason,this);
TelEngine::destruct(xml);
return true;
}
// Process incoming elements in Challenge state
// Return false if stream termination was initiated
bool JBStream::processChallenge(XmlElement* xml, const JabberID& from, const JabberID& to)
{
int t, n;
if (!XMPPUtils::getTag(*xml,t,n))
return dropXml(xml,"failed to retrieve element tag");
if (n != XMPPNamespace::Sasl)
return dropXml(xml,"expecting sasl namespace");
if (t == XmlTag::Abort) {
TelEngine::destruct(xml);
TelEngine::destruct(m_sasl);
XmlElement* rsp = XMPPUtils::createFailure(XMPPNamespace::Sasl,XMPPError::Aborted);
sendStreamXml(Features,rsp);
return true;
}
if (t != XmlTag::Response) {
Debug(this,DebugStub,"Unhandled SASL '%s' in %s state [%p]",
xml->tag(),stateName(),this);
TelEngine::destruct(xml);
return true;
}
XMPPError::Type error = XMPPError::NoError;
// Use a while() to set error and break to the end
while (true) {
// Decode non empty auth data
const String& text = xml->getText();
if (text) {
String tmp;
if (!decodeBase64(tmp,text,this)) {
error = XMPPError::IncorrectEnc;
break;
}
if (m_sasl && !m_sasl->parseMD5ChallengeRsp(tmp)) {
error = XMPPError::MalformedRequest;
break;
}
}
else if (m_sasl)
TelEngine::destruct(m_sasl->m_params);
break;
}
if (error == XMPPError::NoError) {
changeState(Auth);
m_events.append(new JBEvent(JBEvent::Auth,this,xml,from,to));
}
else {
Debug(this,DebugNote,"Received challenge response error='%s' [%p]",
XMPPUtils::s_error[error].c_str(),this);
XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl,error);
sendStreamXml(Features,failure);
TelEngine::destruct(xml);
}
return true;
}
// Process incoming 'auth' elements qualified by SASL namespace
// Return false if stream termination was initiated
bool JBStream::processSaslAuth(XmlElement* xml, const JabberID& from, const JabberID& to)
{
if (!xml)
return true;
if (!XMPPUtils::isTag(*xml,XmlTag::Auth,XMPPNamespace::Sasl))
return dropXml(xml,"expecting 'auth' in sasl namespace");
XMPPFeatureSasl* sasl = static_cast<XMPPFeatureSasl*>(m_features.get(XMPPNamespace::Sasl));
TelEngine::destruct(m_sasl);
XMPPError::Type error = XMPPError::NoError;
const char* mName = xml->attribute("mechanism");
int mech = XMPPUtils::authMeth(mName);
// Use a while() to set error and break to the end
while (true) {
if (!sasl->mechanism(mech)) {
error = XMPPError::InvalidMechanism;
break;
}
if (mech == XMPPUtils::AuthMD5) {
// Ignore auth text: we will challenge the client
m_sasl = new SASL(false,m_local.domain());
String buf;
if (m_sasl->buildMD5Challenge(buf)) {
XDebug(this,DebugAll,"Sending challenge=%s [%p]",buf.c_str(),this);
Base64 b((void*)buf.c_str(),buf.length());
b.encode(buf);
XmlElement* chg = XMPPUtils::createElement(XmlTag::Challenge,
XMPPNamespace::Sasl,buf);
if (!sendStreamXml(Challenge,chg)) {
TelEngine::destruct(xml);
return false;
}
}
else {
TelEngine::destruct(m_sasl);
error = XMPPError::TempAuthFailure;
break;
}
}
else if (mech == XMPPUtils::AuthPlain) {
// Decode non empty auth data
DataBlock d;
const String& text = xml->getText();
if (text && text != "=" && !decodeBase64(d,text)) {
error = XMPPError::IncorrectEnc;
break;
}
m_sasl = new SASL(true);
if (!m_sasl->parsePlain(d)) {
error = XMPPError::MalformedRequest;
break;
}
}
else {
// This should never happen: we don't handle a mechanism sent
// to the remote party!
Debug(this,DebugWarn,"Unhandled advertised auth mechanism='%s' [%p]",
mName,this);
error = XMPPError::TempAuthFailure;
break;
}
break;
}
if (error == XMPPError::NoError) {
// Challenge state: we've challenged the remote party
// Otherwise: request auth from upper layer
if (state() == Challenge)
TelEngine::destruct(xml);
else {
changeState(Auth);
m_events.append(new JBEvent(JBEvent::Auth,this,xml,from,to));
}
}
else {
Debug(this,DebugNote,"Received auth request mechanism='%s' error='%s' [%p]",
mName,XMPPUtils::s_error[error].c_str(),this);
XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl,error);
sendStreamXml(m_state,failure);
TelEngine::destruct(xml);
}
return true;
}
// Process received elements in Features state (incoming stream)
// Return false if stream termination was initiated
bool JBStream::processFeaturesIn(XmlElement* xml, const JabberID& from, const JabberID& to)
{
if (!xml)
return true;
const String* t = 0;
const String* nsName = 0;
if (!xml->getTag(t,nsName))
return dropXml(xml,"invalid tag namespace prefix");
int ns = nsName ? XMPPUtils::s_ns[*nsName] : XMPPNamespace::Count;
// Check if received unexpected feature
if (!m_features.get(ns)) {
// Check for some features that can be negotiated via 'iq' elements
if (m_type == c2s && *t == XMPPUtils::s_tag[XmlTag::Iq] && ns == m_xmlns) {
XmlElement* child = xml->findFirstChild();
int chNs = child ? XMPPUtils::ns(*child) : XMPPNamespace::Count;
bool bindOk = chNs == XMPPNamespace::Bind && m_features.get(XMPPNamespace::Bind);
bool regOk = !bindOk && chNs == XMPPNamespace::IqRegister;
// Bind
if (bindOk) {
// We've sent bind feature
// Don't accept it if not authenticated and TLS/SASL must be negotiated
if (!flag(StreamAuthenticated)) {
XMPPFeature* tls = m_features.get(XMPPNamespace::Tls);
XMPPFeature* sasl = m_features.get(XMPPNamespace::Sasl);
if ((tls && tls->required()) || (sasl && sasl->required())) {
XmlElement* e = XMPPUtils::createError(xml,XMPPError::TypeAuth,
XMPPError::NotAllowed);
sendStreamXml(m_state,e);
return true;
}
}
// Remove TLS/SASL features from list: they can't be negotiated anymore
m_flags |= StreamSecured | StreamAuthenticated;
m_features.remove(XMPPNamespace::Tls);
m_features.remove(XMPPNamespace::Sasl);
changeState(Running);
return processRunning(xml,from,to);
}
// Register
if (regOk) {
m_events.append(new JBEvent(JBEvent::Iq,this,xml,xml->findFirstChild()));
return true;
}
}
// s2s waiting for dialback
if (m_type == s2s) {
if (flag(TlsRequired) && !flag(StreamSecured))
return destroyDropXml(xml,XMPPError::EncryptionRequired,
"required encryption not supported by remote");
if (*t != s_dbResult || ns != XMPPNamespace::Dialback)
return dropXml(xml,"expecting dialback result");
// Auth data
m_local = to;
m_remote = from;
if (!(m_local && engine()->hasDomain(m_local)))
return destroyDropXml(xml,XMPPError::HostUnknown,
"dialback result with unknown 'to' domain");
if (!m_remote)
return destroyDropXml(xml,XMPPError::BadAddressing,
"dialback result with empty 'from' domain");
const char* key = xml->getText();
if (TelEngine::null(key))
return destroyDropXml(xml,XMPPError::NotAcceptable,
"dialback result with empty key");
m_flags |= StreamSecured;
changeState(Auth);
JBEvent* ev = new JBEvent(JBEvent::DbResult,this,xml,from,to);
ev->m_text = key;
m_events.append(ev);
return true;
}
// Check if all remaining features are optional
XMPPFeature* req = firstRequiredFeature();
if (req) {
Debug(this,DebugInfo,
"Received '%s' while having '%s' required feature not negotiated [%p]",
xml->tag(),req->c_str(),this);
// TODO: terminate the stream?
return dropXml(xml,"required feature negotiation not completed");
}
// No more required features: change state to Running
// Remove TLS/SASL features from list: they can't be negotiated anymore
m_flags |= StreamSecured | StreamAuthenticated;
m_features.remove(XMPPNamespace::Tls);
m_features.remove(XMPPNamespace::Sasl);
changeState(Running);
return processRunning(xml,from,to);
}
// Stream enchryption
if (ns == XMPPNamespace::Tls) {
if (*t != XMPPUtils::s_tag[XmlTag::Starttls])
return dropXml(xml,"expecting tls 'starttls' element");
if (!flag(StreamSecured)) {
// Change state before trying to send the element
// to signal to sendPending() to enchrypt the stream after sending it
changeState(Securing);
sendStreamXml(WaitStart,
XMPPUtils::createElement(XmlTag::Proceed,XMPPNamespace::Tls));
}
else {
Debug(this,DebugNote,"Received '%s' element while already secured [%p]",
xml->tag(),this);
// We shouldn't have Starttls in features list
// Something went wrong: terminate the stream
terminate(0,true,xml,XMPPError::Internal,"Stream already secured");
return false;
}
return true;
}
// Stream auth
if (ns == XMPPNamespace::Sasl) {
if (*t != XMPPUtils::s_tag[XmlTag::Auth])
return dropXml(xml,"expecting sasl 'auth' element");
if (!flag(StreamAuthenticated)) {
// Check if we must negotiate TLS before authentication
XMPPFeature* tls = m_features.get(XMPPNamespace::Tls);
if (tls) {
if (!flag(StreamSecured) && tls->required()) {
TelEngine::destruct(xml);
XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl,
XMPPError::EncryptionRequired);
sendStreamXml(m_state,failure);
return true;
}
setSecured();
}
}
else {
// Remote party requested authentication while already done:
// Reset our flag and let it authenticate again
Debug(this,DebugNote,
"Received auth request while already authenticated [%p]",
this);
m_flags &= ~StreamAuthenticated;
}
return processSaslAuth(xml,from,to);
}
return dropXml(xml,"unhandled stream feature");
}
// Process received elements in Features state (outgoing stream)
// Return false if stream termination was initiated
bool JBStream::processFeaturesOut(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
if (!xml)
return true;
if (!XMPPUtils::isTag(*xml,XmlTag::Features,XMPPNamespace::Stream))
return dropXml(xml,"expecting stream features");
m_features.fromStreamFeatures(*xml);
// Check TLS
if (!flag(StreamSecured)) {
XMPPFeature* tls = m_features.get(XMPPNamespace::Tls);
if (tls) {
TelEngine::destruct(xml);
XmlElement* x = XMPPUtils::createElement(XmlTag::Starttls,
XMPPNamespace::Tls);
return sendStreamXml(WaitTlsRsp,x);
}
if (flag(TlsRequired))
return destroyDropXml(xml,XMPPError::EncryptionRequired,
"required encryption not supported by remote");
m_flags |= StreamSecured;
}
// Check auth
if (!flag(StreamAuthenticated)) {
JBServerStream* server = serverStream();
if (server) {
TelEngine::destruct(xml);
return server->sendDialback();
}
JBClientStream* client = clientStream();
if (client) {
// Start auth or request registration data
TelEngine::destruct(xml);
if (!flag(RegisterUser))
return client->startAuth();
return client->requestRegister(false);
}
}
JBClientStream* client = clientStream();
if (client) {
TelEngine::destruct(xml);
return client->bind();
}
return dropXml(xml,"incomplete features process for outgoing stream");
}
// Process received elements in WaitTlsRsp state (outgoing stream)
// The element will be consumed
// Return false if stream termination was initiated
bool JBStream::processWaitTlsRsp(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
if (!xml)
return true;
int t,n;
const char* reason = 0;
if (XMPPUtils::getTag(*xml,t,n)) {
if (n == XMPPNamespace::Tls) {
// Accept proceed and failure
if (t != XmlTag::Proceed && t != XmlTag::Failure)
reason = "expecting tls 'proceed' or 'failure'";
}
else
reason = "expecting tls namespace";
}
else
reason = "failed to retrieve element tag";
if (reason) {
// TODO: Unacceptable response to starttls request
// Restart socket read or terminate the stream ?
socketSetCanRead(true);
return dropXml(xml,reason);
}
if (t == XmlTag::Proceed) {
TelEngine::destruct(xml);
changeState(Securing);
m_engine->encryptStream(this);
socketSetCanRead(true);
m_flags |= StreamTls;
XmlElement* s = buildStreamStart();
return sendStreamXml(WaitStart,s);
}
// TODO: Implement TLS usage reset if the stream is going to re-connect
terminate(1,false,xml,XMPPError::NoError,"Server can't start TLS");
return false;
}
// Set stream namespace from type
void JBStream::setXmlns()
{
switch (m_type) {
case c2s:
m_xmlns = XMPPNamespace::Client;
break;
case s2s:
m_xmlns = XMPPNamespace::Server;
break;
}
}
// Event termination notification
void JBStream::eventTerminated(const JBEvent* ev)
{
if (ev && ev == m_lastEvent) {
m_lastEvent = 0;
XDebug(this,DebugAll,"Event (%p,%s) terminated [%p]",ev,ev->name(),this);
}
}
/*
* JBClientStream
*/
JBClientStream::JBClientStream(JBEngine* engine, Socket* socket)
: JBStream(engine,socket,c2s),
m_userData(0), m_registerReq(0)
{
}
JBClientStream::JBClientStream(JBEngine* engine, const JabberID& jid, const String& account,
const NamedList& params)
: JBStream(engine,c2s,jid,jid.domain(),account,&params),
m_userData(0), m_registerReq(0)
{
m_password = params.getValue("password");
}
// Bind a resource to an incoming stream
void JBClientStream::bind(const String& resource, const char* id, XMPPError::Type error)
{
DDebug(this,DebugAll,"bind(%s,'%s') [%p]",resource.c_str(),
XMPPUtils::s_error[error].c_str(),this);
Lock lock(this);
if (!incoming() || m_remote.resource())
return;
XmlElement* xml = 0;
if (resource) {
m_remote.resource(resource);
xml = XMPPUtils::createIq(XMPPUtils::IqResult,0,0,id);
XmlElement* bind = XMPPUtils::createElement(XmlTag::Bind,
XMPPNamespace::Bind);
bind->addChild(XMPPUtils::createElement(XmlTag::Jid,m_remote));
xml->addChild(bind);
}
else {
if (error == XMPPError::NoError)
error = XMPPError::NotAllowed;
xml = XMPPUtils::createError(XMPPError::TypeModify,error);
}
// Remove non-negotiable bind feature on success
if (sendStanza(xml) && resource)
m_features.remove(XMPPNamespace::Bind);
}
// Request account setup (or info) an outgoing stream
bool JBClientStream::requestRegister(bool data, bool set, const String& newPass)
{
if (incoming())
return true;
Lock lock(this);
DDebug(this,DebugAll,"requestRegister(%u,%u) [%p]",data,set,this);
XmlElement* req = 0;
if (data) {
// Register new user, change the account or remove it
if (set) {
// TODO: Allow user account register/change through unsecured streams ?
String* pass = 0;
if (!flag(StreamAuthenticated))
pass = &m_password;
else if (newPass) {
m_newPassword = newPass;
pass = &m_newPassword;
}
if (!pass)
return false;
m_registerReq = '2';
req = XMPPUtils::createRegisterQuery(0,0,String(m_registerReq),
m_local.node(),*pass);
}
else if (flag(StreamAuthenticated)) {
m_registerReq = '3';
req = XMPPUtils::createRegisterQuery(XMPPUtils::IqSet,0,0,
String(m_registerReq),XMPPUtils::createElement(XmlTag::Remove));
}
else
return false;
}
else {
// Request register info
m_registerReq = '1';
req = XMPPUtils::createRegisterQuery(XMPPUtils::IqGet,0,0,String(m_registerReq));
}
if (!flag(StreamAuthenticated) || state() != Running)
return sendStreamXml(Register,req);
return sendStanza(req);
}
// Process elements in Running state
bool JBClientStream::processRunning(XmlElement* xml, const JabberID& from, const JabberID& to)
{
if (!xml)
return true;
// Check if a resource was bound to an incoming stream
// Accept only 'iq' with bind namespace only if we've sent 'bind' feature
if (incoming()) {
if (!m_remote.resource()) {
if (XMPPUtils::isTag(*xml,XmlTag::Iq,m_xmlns)) {
XmlElement* child = XMPPUtils::findFirstChild(*xml,XmlTag::Bind,XMPPNamespace::Bind);
if (child && m_features.get(XMPPNamespace::Bind)) {
m_events.append(new JBEvent(JBEvent::Bind,this,xml,from,to,child));
return true;
}
}
XmlElement* e = XMPPUtils::createError(xml,XMPPError::TypeCancel,
XMPPError::NotAllowed,"No resource bound to the stream");
sendStanza(e);
return true;
}
}
else if (m_registerReq && XMPPUtils::isTag(*xml,XmlTag::Iq,m_xmlns) &&
isRegisterId(*xml) && XMPPUtils::isResponse(*xml))
return processRegister(xml,from,to);
return JBStream::processRunning(xml,from,to);
}
// Process received elements in WaitStart state
// WaitStart: Incoming: waiting for stream start
// Outgoing: idem (our stream start was already sent)
// Return false if stream termination was initiated
bool JBClientStream::processStart(const XmlElement* xml, const JabberID& from,
const JabberID& to)
{
XDebug(this,DebugAll,"JBClientStream::processStart(%s) [%p]",xml->tag(),this);
// Check element
if (!processStreamStart(xml))
return false;
// RFC3920 5.3.1:
// The 'from' attribute must be set for response stream start
if (outgoing()) {
if (from.null()) {
Debug(this,DebugNote,"Received '%s' with empty 'from' [%p]",xml->tag(),this);
terminate(0,false,0,XMPPError::BadAddressing,"Missing 'from' attribute");
return false;
}
}
else {
if (!flag(StreamAuthenticated)) {
m_remote.set(from);
m_local.set(to);
}
}
m_remote.resource("");
// RFC3920 5.3.1: The 'to' attribute must always be set
// RFC3920: The 'to' attribute is optional
bool validTo = !to.null();
if (validTo) {
if (outgoing())
validTo = (m_local.bare() == to);
else
validTo = engine()->hasDomain(to.domain());
}
#ifdef RFC3920
else
validTo = outgoing();
#endif
if (!validTo) {
Debug(this,DebugNote,"Received '%s' with invalid to='%s' [%p]",
xml->tag(),to.c_str(),this);
terminate(0,false,0,
to.null() ? XMPPError::BadAddressing : XMPPError::HostUnknown,
"Invalid 'to' attribute");
return false;
}
if (incoming()) {
m_events.append(new JBEvent(JBEvent::Start,this,0,from,to));
return true;
}
// Wait features ?
if (flag(StreamRemoteVer1)) {
changeState(Features);
return true;
}
Debug(this,DebugStub,"Outgoing client stream: unsupported remote version (expecting 1.x)");
terminate(0,true,0,XMPPError::Internal,"Unsupported version");
return false;
}
// Process elements in Auth state
bool JBClientStream::processAuth(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
if (!xml)
return true;
if (incoming())
return destroyDropXml(xml,XMPPError::Internal,"invalid state for incoming stream");
int t,n;
if (!XMPPUtils::getTag(*xml,t,n))
return destroyDropXml(xml,XMPPError::Internal,"failed to retrieve element tag");
// Authenticating
if (!flag(StreamAuthenticated)) {
// TODO: The server might challenge us again
// Implement support for multiple challenge/response steps
if (n != XMPPNamespace::Sasl)
return destroyDropXml(xml,XMPPError::InvalidNamespace,
"element with non SASL namespace");
if (!m_sasl)
return destroyDropXml(xml,XMPPError::Internal,"no SASL data");
if (t == XmlTag::Failure) {
terminate(0,true,xml);
return false;
}
if (!m_sasl->m_plain) {
// Digest MD5
if (flag(StreamWaitChallenge)) {
if (t != XmlTag::Challenge)
return destroyDropXml(xml,XMPPError::BadRequest,"expecting challenge");
String tmp;
if (!decodeBase64(tmp,xml->getText(),this))
return destroyDropXml(xml,XMPPError::IncorrectEnc,
"challenge with incorrect encoding");
if (!m_sasl->parseMD5Challenge(tmp))
return destroyDropXml(xml,XMPPError::MalformedRequest,
"invalid challenge format");
TelEngine::destruct(xml);
m_sasl->setAuthParams(m_local.node(),m_password);
tmp.clear();
m_sasl->buildAuthRsp(tmp,"xmpp/" + m_local.domain());
m_flags &= ~StreamWaitChallenge;
m_flags |= StreamWaitChgRsp;
XmlElement* rsp = XMPPUtils::createElement(XmlTag::Response,XMPPNamespace::Sasl,tmp);
return sendStreamXml(state(),rsp);
}
// Digest MD5 response reply
if (flag(StreamWaitChgRsp)) {
#ifdef RFC3920
// Expect success or challenge
// challenge is accepted if not already received one
if (t != XmlTag::Success && (t != XmlTag::Challenge || flag(StreamRfc3920Chg)))
#else
// Expect success
if (t != XmlTag::Success)
#endif
return dropXml(xml,"unexpected element");
if (!flag(StreamRfc3920Chg)) {
String rspAuth;
if (!decodeBase64(rspAuth,xml->getText(),this))
return destroyDropXml(xml,XMPPError::IncorrectEnc,
"challenge response reply with incorrect encoding");
if (!rspAuth.startSkip("rspauth=",false))
return destroyDropXml(xml,XMPPError::BadFormat,
"invalid challenge response reply");
if (!m_sasl->validAuthReply(rspAuth))
return destroyDropXml(xml,XMPPError::InvalidAuth,
"incorrect challenge response reply auth");
}
#ifdef RFC3920
// Send empty response to challenge
if (t == XmlTag::Challenge) {
m_flags |= StreamRfc3920Chg;
TelEngine::destruct(xml);
XmlElement* rsp = XMPPUtils::createElement(XmlTag::Response,
XMPPNamespace::Sasl);
return sendStreamXml(state(),rsp);
}
#endif
m_flags &= ~(StreamWaitChgRsp | StreamRfc3920Chg);
}
else
return dropXml(xml,"unhandled sasl digest md5 state");
}
else {
// Plain
if (t != XmlTag::Success)
return dropXml(xml,"unexpected element");
}
// Authenticated. Bind a resource
Debug(this,DebugAll,"Authenticated [%p]",this);
TelEngine::destruct(xml);
TelEngine::destruct(m_sasl);
m_flags |= StreamAuthenticated;
XmlElement* start = buildStreamStart();
return sendStreamXml(WaitStart,start);
}
XMPPUtils::IqType iq = XMPPUtils::iqType(xml->attribute("type"));
String* id = xml->getAttribute("id");
// Waiting for bind response
if (flag(StreamWaitBindRsp)) {
// Expecting 'iq' result or error
if (t != XmlTag::Iq ||
(iq != XMPPUtils::IqResult && iq != XMPPUtils::IqError) ||
!id || *id != "bind_1")
return dropXml(xml,"unexpected element");
if (iq == XMPPUtils::IqError) {
Debug(this,DebugNote,"Resource binding failed [%p]",this);
terminate(0,true,xml);
return false;
}
// Check it
bool ok = false;
while (true) {
XmlElement* bind = XMPPUtils::findFirstChild(*xml,XmlTag::Bind,XMPPNamespace::Bind);
if (!bind)
break;
XmlElement* tmp = bind->findFirstChild(&XMPPUtils::s_tag[XmlTag::Jid]);
if (!tmp)
break;
JabberID jid(tmp->getText());
if (jid.bare() != m_local.bare())
break;
ok = true;
if (m_local.resource() != jid.resource()) {
m_local.resource(jid.resource());
Debug(this,DebugAll,"Resource set to '%s' [%p]",
local().resource().c_str(),this);
}
break;
}
if (!ok)
return destroyDropXml(xml,XMPPError::UndefinedCondition,
"unacceptable bind response");
m_flags &= ~StreamWaitBindRsp;
TelEngine::destruct(xml);
if (!m_features.get(XMPPNamespace::Session)) {
changeState(Running);
return true;
}
// Send session
XmlElement* sess = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"sess_1");
sess->addChild(XMPPUtils::createElement(XmlTag::Session,XMPPNamespace::Session));
m_flags |= StreamWaitSessRsp;
return sendStreamXml(state(),sess);
}
// Waiting for session response
if (flag(StreamWaitSessRsp)) {
// Expecting 'iq' result or error
if (t != XmlTag::Iq ||
(iq != XMPPUtils::IqResult && iq != XMPPUtils::IqError) ||
!id || *id != "sess_1")
return dropXml(xml,"unexpected element");
if (iq == XMPPUtils::IqError) {
Debug(this,DebugNote,"Session failed [%p]",this);
terminate(0,true,xml);
return false;
}
TelEngine::destruct(xml);
m_flags &= ~StreamWaitBindRsp;
changeState(Running);
return true;
}
return dropXml(xml,"unhandled");
}
// Process elements in Register state
bool JBClientStream::processRegister(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
if (!xml)
return true;
int t, ns;
if (!XMPPUtils::getTag(*xml,t,ns))
return dropXml(xml,"failed to retrieve element tag");
if (t != XmlTag::Iq)
return dropXml(xml,"expecting 'iq'");
XMPPUtils::IqType iq = XMPPUtils::iqType(xml->attribute("type"));
if (iq != XMPPUtils::IqResult && iq != XMPPUtils::IqError)
return dropXml(xml,"expecting 'iq' response");
if (!isRegisterId(*xml))
return dropXml(xml,"unexpected response id");
if (iq == XMPPUtils::IqError) {
m_events.append(new JBEvent(JBEvent::RegisterFailed,this,xml,from,to));
// Don't terminate if the user requested account change after authentication
if (!flag(StreamAuthenticated))
terminate(0,true,0,XMPPError::NoError);
return flag(StreamAuthenticated);
}
// Requested registration data
if (m_registerReq == '1') {
// XEP-0077: check for username and password children or
// instructions
XmlElement* query = XMPPUtils::findFirstChild(*xml,XmlTag::Query,
XMPPNamespace::IqRegister);
if (query && XMPPUtils::findFirstChild(*query,XmlTag::Username) &&
XMPPUtils::findFirstChild(*query,XmlTag::Password)) {
TelEngine::destruct(xml);
return requestRegister(true);
}
m_events.append(new JBEvent(JBEvent::RegisterFailed,this,xml,from,to));
// Don't terminate if the user requested account change after authentication
if (!flag(StreamAuthenticated))
terminate(0,true,0,XMPPError::NoError);
return flag(StreamAuthenticated);
}
// Requested registration/change
if (m_registerReq == '2') {
m_events.append(new JBEvent(JBEvent::RegisterOk,this,xml,from,to));
// Reset register user flag
m_flags &= ~RegisterUser;
// Done if account changed after authentication
if (flag(StreamAuthenticated)) {
m_password = m_newPassword;
return true;
}
// Start auth
changeState(Features);
return startAuth();
}
// Requested account removal
if (m_registerReq == '3') {
terminate(0,true,xml,XMPPError::Reg,"Account removed");
return false;
}
return destroyDropXml(xml,XMPPError::Internal,"unhandled state");
}
// Release memory
void JBClientStream::destroyed()
{
userData(0);
JBStream::destroyed();
}
// Start outgoing stream authentication
bool JBClientStream::startAuth()
{
if (incoming() || state() != Features)
return false;
TelEngine::destruct(m_sasl);
XMPPFeature* f = m_features.get(XMPPNamespace::Sasl);
XMPPFeatureSasl* sasl = 0;
if (f)
sasl = static_cast<XMPPFeatureSasl*>(f->getObject("XMPPFeatureSasl"));
if (!sasl) {
terminate(0,true,0,XMPPError::NoError,"Missing authentication data");
return false;
}
// RFC 3920 SASL auth
int mech = XMPPUtils::AuthNone;
if (sasl->mechanism(XMPPUtils::AuthMD5))
mech = XMPPUtils::AuthMD5;
else if (sasl->mechanism(XMPPUtils::AuthPlain) && flag(AllowPlainAuth))
mech = XMPPUtils::AuthPlain;
else {
terminate(0,true,0,XMPPError::NoError,"Unsupported authentication mechanism");
return false;
}
m_sasl = new SASL(mech == XMPPUtils::AuthPlain);
String rsp;
if (m_sasl->m_plain) {
m_sasl->setAuthParams(m_local.node(),m_password);
if (!m_sasl->buildAuthRsp(rsp)) {
terminate(0,true,0,XMPPError::NoError,"Invalid auth data length for plain auth");
return false;
}
}
else
m_flags |= StreamWaitChallenge;
// MD5: send auth element, wait challenge
// Plain auth: send auth element with credentials and wait response (success/failure)
XmlElement* xml = XMPPUtils::createElement(XmlTag::Auth,XMPPNamespace::Sasl,rsp);
xml->setAttribute("mechanism",lookup(mech,XMPPUtils::s_authMeth));
return sendStreamXml(Auth,xml);
}
// Start resource binding on outgoing stream
bool JBClientStream::bind()
{
Debug(this,DebugAll,"Binding resource [%p]",this);
XmlElement* bind = XMPPUtils::createElement(XmlTag::Bind,XMPPNamespace::Bind);
if (m_local.resource())
bind->addChild(XMPPUtils::createElement(XmlTag::Resource,m_local.resource()));
XmlElement* b = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"bind_1");
b->addChild(bind);
m_flags |= StreamWaitBindRsp;
return sendStreamXml(Auth,b);
}
/*
* JBServerStream
*/
// Build an incoming stream from a socket
JBServerStream::JBServerStream(JBEngine* engine, Socket* socket)
: JBStream(engine,socket,s2s),
m_dbKey(0)
{
}
// Build an outgoing stream
JBServerStream::JBServerStream(JBEngine* engine, const JabberID& local,
const JabberID& remote, const char* dbId, const char* dbKey, bool dbOnly)
: JBStream(engine,s2s,local,remote),
m_dbKey(0)
{
if (!(TelEngine::null(dbId) || TelEngine::null(dbKey)))
m_dbKey = new NamedString(dbId,dbKey);
if (dbOnly)
m_flags |= DialbackOnly | NoAutoRestart;
}
// Send a dialback key response.
// If the stream is in Dialback state change it's state to Running if valid or
// terminate it if invalid
bool JBServerStream::sendDbResult(const JabberID& from, const JabberID& to, bool valid)
{
if (incoming() && state() == Auth && from == m_local && to == m_remote)
return authenticated(valid);
XmlElement* rsp = XMPPUtils::createDialbackResult(from,to,valid);
return sendStanza(rsp);
}
// Send dialback data (key/verify)
bool JBServerStream::sendDialback()
{
State newState = Running;
XmlElement* result = 0;
if (!flag(DialbackOnly)) {
if (flag(StreamAuthenticated))
newState = Running;
else {
String key;
engine()->buildDialbackKey(id(),key);
result = XMPPUtils::createDialbackKey(m_local,m_remote,key);
newState = Auth;
}
}
else if (!m_dbKey) {
// Dialback only with no key?
Debug(this,DebugGoOn,"Outgoing dialback stream with no key! [%p]",this);
terminate(0,true,0,XMPPError::Internal);
return false;
}
if (m_dbKey) {
XmlElement* db = XMPPUtils::createDialbackVerify(m_local,m_remote,
m_dbKey->name(),*m_dbKey);
if (result)
return sendStreamXml(newState,result,db);
return sendStreamXml(newState,db);
}
if (result)
return sendStreamXml(newState,result);
changeState(newState);
return true;
}
// Release memory
void JBServerStream::destroyed()
{
TelEngine::destruct(m_dbKey);
JBStream::destroyed();
}
// Process elements in Running state
bool JBServerStream::processRunning(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
if (!xml)
return true;
// Check the tags of known dialback elements:
// there are servers who don't stamp them with the namespace
// Let other elements stamped with dialback namespace go the upper layer
if (isDbResult(*xml)) {
if (outgoing())
return dropXml(xml,"dialback result on outgoing stream");
const char* key = xml->getText();
// Result: accept already authenticated
if (m_local == to && m_remote == from) {
if (TelEngine::null(key))
return destroyDropXml(xml,XMPPError::BadFormat,
"dialback result with empty key");
String tmp;
engine()->buildDialbackKey(id(),tmp);
if (tmp == key) {
TelEngine::destruct(xml);
return sendDbResult(m_local,m_remote,true);
}
return destroyDropXml(xml,XMPPError::NotAuthorized,
"dialback result with invalid key");
}
JBEvent* ev = new JBEvent(JBEvent::DbResult,this,xml,from,to);
ev->m_text = key;
m_events.append(ev);
return true;
}
// Call default handler
return JBStream::processRunning(xml,from,to);
}
// Build a stream start XML element
XmlElement* JBServerStream::buildStreamStart()
{
XmlElement* start = new XmlElement(XMPPUtils::s_tag[XmlTag::Stream],false);
if (incoming())
start->setAttribute("id",m_id);
XMPPUtils::setStreamXmlns(*start);
start->setAttribute(XmlElement::s_ns,XMPPUtils::s_ns[m_xmlns]);
start->setAttribute(XmlElement::s_nsPrefix + "db",XMPPUtils::s_ns[XMPPNamespace::Dialback]);
if (!dialback()) {
start->setAttributeValid("from",m_local.bare());
start->setAttributeValid("to",m_remote.bare());
if (!flag(StreamSecured) || flag(TlsRequired)) {
start->setAttribute("version","1.0");
start->setAttribute("xml:lang","en");
}
}
return start;
}
// Process received elements in WaitStart state
// WaitStart: Incoming: waiting for stream start
// Outgoing: idem (our stream start was already sent)
// Return false if stream termination was initiated
bool JBServerStream::processStart(const XmlElement* xml, const JabberID& from,
const JabberID& to)
{
if (!processStreamStart(xml))
return true;
if (outgoing()) {
// Wait features ?
if (flag(StreamRemoteVer1)) {
changeState(Features);
return true;
}
// Stream not secured
if (!flag(StreamSecured)) {
// Accept dialback auth stream
// The namspace presence was already checked in checkStreamStart()
if (flag(TlsRequired)) {
terminate(0,false,0,XMPPError::EncryptionRequired);
return false;
}
m_flags |= StreamSecured;
}
m_flags |= StreamSecured;
return sendDialback();
}
// Incoming stream
m_local = to;
m_remote = from;
if (m_local && !engine()->hasDomain(m_local)) {
terminate(0,true,0,XMPPError::HostUnknown);
return false;
}
updateFromRemoteDef();
m_events.append(new JBEvent(JBEvent::Start,this,0,from,to));
return true;
}
// Process elements in Auth state
bool JBServerStream::processAuth(XmlElement* xml, const JabberID& from,
const JabberID& to)
{
if (incoming())
return dropXml(xml,"invalid state for incoming stream");
// Waiting for db:result
if (!isDbResult(*xml))
return dropXml(xml,"expecting dialback result");
// Result
// Outgoing stream waiting for dialback key response
if (outgoing()) {
if (m_remote != from || m_local != to)
return destroyDropXml(xml,XMPPError::BadAddressing,
"dialback response with invalid 'from'");
// Expect dialback key response
String type(xml->getAttribute("type"));
if (type != "valid") {
terminate(1,false,xml,XMPPError::NoError);
return false;
}
// Stream authenticated
TelEngine::destruct(xml);
m_flags |= StreamAuthenticated;
changeState(Running);
return true;
}
return dropXml(xml,"incomplete state process");
}
/* vi: set ts=8 sw=4 sts=4 noet: */