Implemented support for Jingle session transfer (XEP-0251).

git-svn-id: http://yate.null.ro/svn/yate/trunk@2358 acf43c95-373e-0410-b603-e72c3f656dc1
This commit is contained in:
marian 2008-11-20 16:59:42 +00:00
parent ecaf73b91c
commit 9ff2bae307
1 changed files with 339 additions and 8 deletions

View File

@ -48,6 +48,7 @@ class YJBPresence; // Presence service
class YJBIqService; // Handle 'iq' stanzas not processed by other services
class YJGData; // Handle the transport and formats for a connection
class YJGConnection; // Jingle channel
class YJGTransfer; // Transfer thread (route and execute)
class ResNotifyHandler; // resource.notify handler
class ResSubscribeHandler; // resource.subscribe handler
class UserLoginHandler; // user.login handler
@ -221,6 +222,7 @@ protected:
class YJGConnection : public Channel
{
YCLASS(YJGConnection,Channel)
friend class YJGTransfer;
public:
enum State {
Pending,
@ -244,6 +246,19 @@ public:
{ return m_local; }
inline const JabberID& remote() const
{ return m_remote; }
// Check session id
inline bool isSid(const String& sid) {
Lock lock(m_mutex);
return m_session && sid == m_session->sid();
}
// Get jingle session id
inline bool getSid(String& buf) {
Lock lock(m_mutex);
if (!m_session)
return false;
buf = m_session->sid();
return true;
}
// Overloaded methods from Channel
virtual void callAccept(Message& msg);
virtual void callRejected(const char* error, const char* reason, const Message* msg);
@ -255,6 +270,7 @@ public:
virtual bool msgText(Message& msg, const char* text);
virtual bool msgDrop(Message& msg, const char* reason);
virtual bool msgTone(Message& msg, const char* tone);
virtual bool msgTransfer(Message& msg);
inline bool disconnect(const char* reason) {
setReason(reason);
return Channel::disconnect(m_reason);
@ -269,6 +285,15 @@ public:
// Hangup if the remote user is unavailbale
// Return true to disconnect
bool presenceChanged(bool available);
// Process a transfer request
// Return true if the event was accepted
bool processTransferRequest(JGEvent* event);
// Transfer terminated notification from transfer thread
void transferTerminated(bool ok, const char* reason = 0);
// Check if a transfer can be initiated
inline bool canTransfer() const
{ return m_session && !m_transferring && isAnswered(); }
inline void updateResource(const String& resource) {
if (!m_remote.resource() && resource)
@ -304,12 +329,36 @@ private:
String m_reason; // Hangup reason
// Timeouts
u_int64_t m_timeout; // Timeout for not answered outgoing connections
// Transfer
bool m_transferring; // The call is already involved in a transfer
String m_transferStanzaId; // Sent transfer stanza id used to track the result
JabberID m_transferTo; // Transfer target
JabberID m_transferFrom; // Transfer source
String m_transferSid; // Session id for attended transfer
// On hold data
int m_dataFlags; // The data status
String m_onHoldOutId; // The id of the hold stanza sent to remote
String m_activeOutId; // The id of the active stanza sent to remote
};
/**
* Transfer thread (route and execute)
*/
class YJGTransfer : public Thread
{
public:
YJGTransfer(YJGConnection* conn);
virtual void run(void);
private:
String m_transferorID; // Transferor channel's id
String m_transferredID; // Transferred channel's id
Driver* m_transferredDrv; // Transferred driver's pointer
JabberID m_to; // Transfer target
JabberID m_from; // Transfer source
String m_sid; // Session id for unattended transfer
Message m_msg;
};
/**
* resource.notify message handler
*/
@ -430,6 +479,11 @@ public:
// is not subscribed to the remote user (or the remote user is not found).
// Return false if user or resource is not found
bool getClientTargetResource(JBClientStream* stream, JabberID& target, bool* noSub = 0);
// Find a channel by its sid
YJGConnection* findBySid(const String& sid);
// Get a channel's session id. Return false if not found
bool getSid(const String& id, String& sid);
// Check if this module handles a given protocol
static bool canHandleProtocol(const String& proto) {
for (unsigned int i = 0; i < ProtoCount; i++)
@ -1366,9 +1420,18 @@ YJGConnection::YJGConnection(Message& msg, const char* caller, const char* calle
m_mutex(true), m_state(Pending), m_session(0), m_local(caller),
m_remote(called), m_callerPrompt(msg.getValue("callerprompt")),
m_data(0), m_remoteSuportRing(true), m_hangup(false), m_timeout(0),
m_dataFlags(0)
m_transferring(false), m_dataFlags(0)
{
Debug(this,DebugCall,"Outgoing. caller='%s' called='%s' [%p]",caller,called,this);
String uri = msg.getValue("diverteruri",msg.getValue("diverter"));
// Skip protocol if present
if (uri) {
int pos = uri.find(':');
m_transferFrom.set((pos >= 0) ? uri.substr(pos + 1) : uri);
}
Debug(this,DebugCall,"Outgoing. caller='%s' called='%s'%s%s [%p]",
caller,called,
m_transferFrom?". Transferred from=":"",
m_transferFrom.safe(),this);
// Init transport
m_data = new YJGData(this,&msg);
// Set timeout and maxcall
@ -1410,10 +1473,20 @@ YJGConnection::YJGConnection(JGEvent* event)
m_mutex(true), m_state(Active), m_session(event->session()),
m_local(event->session()->local()), m_remote(event->session()->remote()),
m_data(0), m_remoteSuportRing(true), m_hangup(false), m_timeout(0),
m_dataFlags(0)
m_transferring(false), m_dataFlags(0)
{
Debug(this,DebugCall,"Incoming. caller='%s' called='%s' [%p]",
m_remote.c_str(),m_local.c_str(),this);
// Check if this call is transferred
if (event->jingle()) {
XMLElement* trans = event->jingle()->findFirstChild(XMLElement::Transfer);
if (trans) {
m_transferFrom.set(trans->getAttribute("from"));
TelEngine::destruct(trans);
}
}
Debug(this,DebugCall,"Incoming. caller='%s' called='%s'%s%s [%p]",
m_remote.c_str(),m_local.c_str(),
m_transferFrom?". Transferred from=":"",
m_transferFrom.safe(),this);
// Set session
m_session->userData(this);
// Init transport
@ -1670,6 +1743,57 @@ bool YJGConnection::msgTone(Message& msg, const char* tone)
return true;
}
// Send a transfer request
bool YJGConnection::msgTransfer(Message& msg)
{
Lock lock(m_mutex);
if (!canTransfer())
return false;
// Get transfer destination
// Try to get a resource for transfer target if incomplete
m_transferTo.set(msg.getValue("to"));
if (!m_transferTo) {
DDebug(this,DebugNote,"Transfer request with empty target [%p]",this);
return false;
}
if (!m_transferTo.isFull()) {
const JBStream* stream = m_session ? m_session->stream() : 0;
if (stream && stream->type() == JBEngine::Client)
plugin.getClientTargetResource((JBClientStream*)stream,m_transferTo);
}
// Check attended transfer request
NamedString* chanId = msg.getParam("channelid");
if (chanId) {
bool ok = plugin.getSid(*chanId,m_transferSid);
if (!m_transferSid) {
Debug(this,DebugNote,"Attended transfer failed for conn=%s 'no %s' [%p]",
chanId->c_str(),ok ? "session" : "connection",this);
return false;
}
// Don't transfer the same channel
if (m_transferSid == m_session->sid()) {
Debug(this,DebugNote,
"Attended transfer request for the same session! [%p]",this);
return false;
}
}
// Send the transfer request
XMLElement* trans = m_session->buildTransfer(m_transferTo,
m_transferSid ? m_session->local() : String::empty(),m_transferSid);
m_transferring = m_session->sendInfo(trans,&m_transferStanzaId);
Debug(this,m_transferring?DebugCall:DebugNote,"%s transfer to=%s sid=%s [%p]",
m_transferring ? "Sent" : "Failed to send",m_transferTo.c_str(),
m_transferSid.c_str(),this);
if (!m_transferring)
m_transferStanzaId = "";
return m_transferring;
}
// Hangup the call. Send session terminate if not already done
void YJGConnection::hangup(const char* reason, const char* text)
{
@ -1771,6 +1895,23 @@ void YJGConnection::handleEvent(JGEvent* event)
}
return;
}
// Check if this is a transfer request result
if (m_transferring && m_transferStanzaId &&
m_transferStanzaId == event->id()) {
// Reset transfer
m_transferStanzaId = "";
m_transferring = false;
if (rspOk) {
Debug(this,DebugStub,"Transfer succeedded !!!!! [%p]",this);
// TODO: implement
}
else {
Debug(this,DebugMild,"Transfer failed error=%s [%p]",
event->text().c_str(),this);
}
return;
}
return;
}
@ -1827,8 +1968,7 @@ void YJGConnection::handleEvent(JGEvent* event)
Engine::enqueue(message("call.answered",false,true));
break;
case JGSession::ActTransfer:
if (m_session)
m_session->confirm(event->releaseXML(),XMPPError::SFeatureNotImpl);
processTransferRequest(event);
break;
case JGSession::ActRinging:
m_session->confirm(event->element());
@ -1871,8 +2011,11 @@ bool YJGConnection::presenceChanged(bool available)
Debug(this,DebugCall,"Calling. caller=%s called=%s [%p]",
m_local.c_str(),m_remote.c_str(),this);
m_state = Active;
XMLElement* transfer = 0;
if (m_transferFrom)
transfer = JGSession::buildTransfer(String::empty(),m_transferFrom);
m_session = s_jingle->call(m_local,m_remote,m_data->JGAudioList::toXML(),
JGTransport::createTransport(),0,m_callerPrompt);
JGTransport::createTransport(),transfer,m_callerPrompt);
if (!m_session) {
hangup("noconn");
return true;
@ -1889,6 +2032,74 @@ bool YJGConnection::presenceChanged(bool available)
return false;
}
// Process a transfer request
bool YJGConnection::processTransferRequest(JGEvent* event)
{
Lock lock(m_mutex);
// Check if we can accept a transfer and if it is a valid request
XMLElement* trans = 0;
const char* reason = 0;
XMPPError::Type error = XMPPError::SBadRequest;
while (true) {
if (!canTransfer()) {
error = XMPPError::SRequest;
reason = "Unacceptable in current state";
break;
}
trans = event->jingle() ? event->jingle()->findFirstChild(XMLElement::Transfer) : 0;
if (!trans) {
reason = "Transfer element is misssing";
break;
}
m_transferTo.set(trans->getAttribute("to"));
// Check transfer target
if (!m_transferTo) {
reason = "Transfer target is misssing or incomplete";
break;
}
// Check sid: don't accept the replacement of the same session
m_transferSid = trans->getAttribute("sid");
if (m_transferSid && isSid(m_transferSid)) {
reason = "Can't replace the same session";
break;
}
m_transferFrom.set(trans->getAttribute("from"));
break;
}
TelEngine::destruct(trans);
if (!reason) {
m_transferring = true;
Debug(this,DebugCall,"Starting transfer to=%s from=%s sid=%s [%p]",
m_transferTo.c_str(),m_transferFrom.c_str(),m_transferSid.c_str(),this);
(new YJGTransfer(this))->startup();
return true;
}
// Not acceptable
Debug(this,DebugNote,
"Refusing transfer request reason='%s' (transferring=%u answered=%u) [%p]",
reason,m_transferring,isAnswered(),this);
m_session->confirm(event->releaseXML(),error,reason);
return false;
}
// Transfer terminated notification from transfer thread
void YJGConnection::transferTerminated(bool ok, const char* reason)
{
if (ok)
Debug(this,DebugCall,"Transfer succeedded [%p]",this);
else
Debug(this,DebugNote,"Transfer failed error='%s' [%p]",reason,this);
// Reset transfer data
Lock lock(m_mutex);
m_transferring = false;
m_transferStanzaId = "";
m_transferTo = "";
m_transferFrom = "";
m_transferSid = "";
}
// Handle hold/active/mute actions
// Confirm the received element
void YJGConnection::handleAudioInfoEvent(JGEvent* event)
@ -1966,6 +2177,101 @@ void YJGConnection::handleAudioInfoEvent(JGEvent* event)
}
}
/**
* Transfer thread (route and execute)
*/
YJGTransfer::YJGTransfer(YJGConnection* conn)
: Thread("Jingle transfer"),
m_msg("call.route")
{
if (!conn)
return;
m_transferorID = conn->id();
Channel* ch = YOBJECT(Channel,conn->getPeer());
if (!(ch && ch->driver()))
return;
m_transferredID = ch->id();
m_transferredDrv = ch->driver();
// Set transfer data from channel
m_to.set(conn->m_transferTo.node(),conn->m_transferTo.domain(),conn->m_transferTo.resource());
m_from.set(conn->m_transferFrom.node(),conn->m_transferFrom.domain(),conn->m_transferFrom.resource());
m_sid = conn->m_transferSid;
if (!m_from)
m_from.set(conn->remote().node(),conn->remote().domain(),conn->remote().resource());
// Build the routing message if unattended
if (!m_sid) {
m_msg.addParam("id",m_transferredID);
if (conn->billid())
m_msg.addParam("billid",conn->billid());
m_msg.addParam("caller",m_from.node());
m_msg.addParam("called",m_to.node());
m_msg.addParam("calleduri",BUILD_XMPP_URI(m_to));
m_msg.addParam("diverter",m_from.bare());
m_msg.addParam("diverteruri",BUILD_XMPP_URI(m_from));
m_msg.addParam("reason",lookup(JGSession::ReasonTransfer,s_errMap));
}
}
void YJGTransfer::run()
{
DDebug(&plugin,DebugAll,"'%s' thread transferror=%s transferred=%s to=%s [%p]",
name(),m_transferorID.c_str(),m_transferredID.c_str(),m_to.c_str(),this);
String error;
// Attended
if (m_sid) {
plugin.lock();
RefPointer<Channel> chan = plugin.findBySid(m_sid);
plugin.unlock();
String peer = chan ? chan->getPeerId() : "";
if (peer) {
Message m("chan.connect");
m.addParam("id",m_transferredID);
m.addParam("targetid",peer);
m.addParam("reason","transferred");
if (!Engine::dispatch(m))
error = m.getValue("error","Failed to connect");
}
else
error << "No peer for sid=" << m_sid;
}
else {
error = m_transferredDrv ? "" : "No driver for transferred connection";
while (m_transferredDrv) {
// Unattended: route the call
#define SET_ERROR(err) { error << err; break; }
bool ok = Engine::dispatch(m_msg);
m_transferredDrv->lock();
RefPointer<Channel> chan = m_transferredDrv->find(m_transferredID);
m_transferredDrv->unlock();
if (!chan)
SET_ERROR("Connection vanished while routing");
if (!ok || (m_msg.retValue() == "-") || (m_msg.retValue() == "error"))
SET_ERROR("call.route failed error=" << m_msg.getValue("error"));
// Execute the call
m_msg = "call.execute";
m_msg.setParam("callto",m_msg.retValue());
m_msg.clearParam("error");
m_msg.retValue().clear();
m_msg.userData(chan);
if (Engine::dispatch(m_msg))
break;
SET_ERROR("'call.execute' failed error=" << m_msg.getValue("error"));
#undef SET_ERROR
}
}
// Notify termination to transferor
plugin.lock();
YJGConnection* conn = static_cast<YJGConnection*>(plugin.Driver::find(m_transferorID));
if (conn)
conn->transferTerminated(!error,error);
else
DDebug(&plugin,DebugInfo,
"%s thread transfer terminated trans=%s error=%s [%p]",
name(),m_transferredID.c_str(),error.c_str(),this);
plugin.unlock();
}
/**
* resource.notify message handler
*/
@ -2640,6 +2946,7 @@ void YJGDriver::initialize()
installRelay(Halt);
installRelay(Route);
installRelay(Update);
installRelay(Transfer);
installRelay(ImExecute);
Engine::install(new ResNotifyHandler);
Engine::install(new ResSubscribeHandler);
@ -3395,6 +3702,30 @@ bool YJGDriver::getClientTargetResource(JBClientStream* stream,
return !target.resource().null();
}
// Get a channel's session id. Return false if not found
bool YJGDriver::getSid(const String& id, String& sid)
{
Lock lock(this);
YJGConnection* conn = static_cast<YJGConnection*>(Driver::find(id));
if (conn)
conn->getSid(sid);
return 0 != conn;
}
// Find a channel by its sid
YJGConnection* YJGDriver::findBySid(const String& sid)
{
if (!sid)
return 0;
Lock lock(this);
for (ObjList* o = channels().skipNull(); o; o = o->skipNext()) {
YJGConnection* conn = static_cast<YJGConnection*>(o->get());
if (conn->isSid(sid))
return conn;
}
return 0;
}
}; // anonymous namespace
/* vi: set ts=8 sw=4 sts=4 noet: */