/** * session.cpp * Yet Another Jingle 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 using namespace TelEngine; static XMPPNamespace s_ns; static XMPPError s_err; /** * JGAudio */ XMLElement* JGAudio::toXML() { XMLElement* p = new XMLElement(XMLElement::PayloadType); p->setAttribute("id",id); p->setAttributeValid("name",name); p->setAttributeValid("clockrate",clockrate); p->setAttributeValid("bitrate",bitrate); return p; } void JGAudio::fromXML(XMLElement* xml) { if (!xml) { set("","","","",""); return; } xml->getAttribute("id",id); xml->getAttribute("name",name); xml->getAttribute("clockrate",clockrate); xml->getAttribute("bitrate",bitrate); } /** * JGAudioList */ // Find a data payload by its synonym JGAudio* JGAudioList::findSynonym(const String& value) { for (ObjList* o = skipNull(); o; o = o->skipNext()) { JGAudio* a = static_cast(o->get()); if (value == a->synonym) return a; } return 0; } // Create a 'description' element and add payload children to it XMLElement* JGAudioList::toXML(bool telEvent) { XMLElement* desc = XMPPUtils::createElement(XMLElement::Description, XMPPNamespace::JingleAudio); for (ObjList* o = skipNull(); o; o = o->skipNext()) { JGAudio* a = static_cast(o->get()); desc->addChild(a->toXML()); } if (telEvent) { JGAudio* te = new JGAudio("106","telephone-event","8000","",""); desc->addChild(te->toXML()); TelEngine::destruct(te); } return desc; } // Fill this list from an XML element's children. Clear before attempting to fill void JGAudioList::fromXML(XMLElement* xml) { clear(); XMLElement* m = xml ? xml->findFirstChild(XMLElement::PayloadType) : 0; for (; m; m = xml->findNextChild(m,XMLElement::PayloadType)) ObjList::append(new JGAudio(m)); } // Create a list from data payloads bool JGAudioList::createList(String& dest, bool synonym, const char* sep) { dest = ""; for (ObjList* o = skipNull(); o; o = o->skipNext()) { JGAudio* a = static_cast(o->get()); dest.append(synonym?a->synonym:a->name,sep); } return (0 != dest.length()); } /** * JGTransport */ JGTransport::JGTransport(const JGTransport& src) { name = src.name; address = src.address; port = src.port; preference = src.preference; username = src.username; protocol = src.protocol; generation = src.generation; password = src.password; type = src.type; network = src.network; } XMLElement* JGTransport::createTransport() { return XMPPUtils::createElement(XMLElement::Transport, XMPPNamespace::JingleTransport); } XMLElement* JGTransport::toXML() { XMLElement* p = new XMLElement(XMLElement::Candidate); p->setAttribute("name",name); p->setAttribute("address",address); p->setAttribute("port",port); p->setAttributeValid("preference",preference); p->setAttributeValid("username",username); p->setAttributeValid("protocol",protocol); p->setAttributeValid("generation",generation); p->setAttributeValid("password",password); p->setAttributeValid("type",type); p->setAttributeValid("network",network); return p; } void JGTransport::fromXML(XMLElement* element) { element->getAttribute("name",name); element->getAttribute("address",address); element->getAttribute("port",port); element->getAttribute("preference",preference); element->getAttribute("username",username); element->getAttribute("protocol",protocol); element->getAttribute("generation",generation); element->getAttribute("password",password); element->getAttribute("type",type); element->getAttribute("network",network); } /** * JGSession */ TokenDict JGSession::s_states[] = { {"Idle", Idle}, {"Pending", Pending}, {"Active", Active}, {"Ending", Ending}, {"Destroy", Destroy}, {0,0} }; TokenDict JGSession::s_actions[] = { {"accept", ActAccept}, {"initiate", ActInitiate}, {"reject", ActReject}, {"terminate", ActTerminate}, {"candidates", ActTransportCandidates}, {"transport-info", ActTransportInfo}, {"transport-accept", ActTransportAccept}, {"content-info", ActContentInfo}, {"Transport", ActTransport}, {"DTMF", ActDtmf}, {"DTMF method", ActDtmfMethod}, {0,0} }; // Create an outgoing session JGSession::JGSession(JGEngine* engine, JBStream* stream, const String& callerJID, const String& calledJID, XMLElement* media, XMLElement* transport, bool sid, const char* msg) : Mutex(true), m_state(Idle), m_transportType(TransportUnknown), m_engine(engine), m_stream(stream), m_outgoing(true), m_localJID(callerJID), m_remoteJID(calledJID), m_sidAttr(sid?"sid":"id"), m_lastEvent(0), m_private(0), m_stanzaId(1) { m_engine->createSessionId(m_localSid); m_sid = m_localSid; Debug(m_engine,DebugAll,"Call(%s). Outgoing msg=%s [%p]",m_sid.c_str(),msg,this); if (msg) sendMessage(msg); XMLElement* xml = createJingle(ActInitiate,media,transport); if (sendStanza(xml)) changeState(Pending); else changeState(Destroy); } // Create an incoming session JGSession::JGSession(JGEngine* engine, JBEvent* event, const String& id, bool sid) : Mutex(true), m_state(Idle), m_transportType(TransportUnknown), m_engine(engine), m_stream(event->stream()), m_outgoing(false), m_sid(id), m_sidAttr(sid?"sid":"id"), m_lastEvent(0), m_private(0), m_stanzaId(1) { m_events.append(event); m_engine->createSessionId(m_localSid); Debug(m_engine,DebugAll,"Call(%s). Incoming [%p]",m_sid.c_str(),this); } // Destructor: hangup, cleanup, remove from engine's list JGSession::~JGSession() { XDebug(m_engine,DebugAll,"JGSession::~JGSession() [%p]",this); } // Release this session and its memory void JGSession::destroyed() { lock(); // Cancel pending outgoing. Hangup. Cleanup if (m_stream) { m_stream->removePending(m_localSid,false); hangup(); TelEngine::destruct(m_stream); } m_events.clear(); unlock(); // Remove from engine Lock lock(m_engine); m_engine->m_sessions.remove(this,false); lock.drop(); DDebug(m_engine,DebugAll,"Call(%s). Destroyed [%p]",m_sid.c_str(),this); } // Accept a Pending incoming session bool JGSession::accept(XMLElement* description) { Lock lock(this); if (outgoing() || state() != Pending) return false; XMLElement* xml = createJingle(ActAccept,description,JGTransport::createTransport()); if (!sendStanza(xml)) return false; changeState(Active); return true; } // Close a Pending or Active session bool JGSession::hangup(bool reject, const char* msg) { Lock lock(this); if (state() != Pending && state() != Active) return false; DDebug(m_engine,DebugAll,"Call(%s). %s('%s') [%p]",m_sid.c_str(), reject?"Reject":"Hangup",msg,this); if (msg) sendMessage(msg); // Clear sent stanzas list. We will wait for this element to be confirmed m_sentStanza.clear(); XMLElement* xml = createJingle(reject ? ActReject : ActTerminate); bool ok = sendStanza(xml); changeState(Ending); return ok; } // Confirm a received element. If the error is NoError a result stanza will be sent // Otherwise, an error stanza will be created and sent bool JGSession::confirm(XMLElement* xml, XMPPError::Type error, const char* text, XMPPError::ErrorType type) { if (!xml) return false; String id = xml->getAttribute("id"); XMLElement* iq = 0; if (error == XMPPError::NoError) { iq = XMPPUtils::createIq(XMPPUtils::IqResult,m_localJID,m_remoteJID,id); // The receiver will detect which stanza is confirmed by id // If missing, make a copy of the received element and attach it to the error if (!id) { XMLElement* copy = new XMLElement(*xml); iq->addChild(copy); } } else { iq = XMPPUtils::createIq(XMPPUtils::IqError,m_localJID,m_remoteJID,id); iq->addChild(xml); iq->addChild(XMPPUtils::createError(type,error,text)); } return sendStanza(iq,false); } // Send a dtmf string to remote peer bool JGSession::sendDtmf(const char* dtmf, bool buttonUp) { XMLElement* iq = createJingle(ActContentInfo); XMLElement* sess = iq->findFirstChild(); if (!(dtmf && *dtmf && sess)) return sendStanza(iq); char s[2] = {0,0}; const char* action = buttonUp ? "button-up" : "button-down"; while (*dtmf) { s[0] = *dtmf++; XMLElement* xml = XMPPUtils::createElement(XMLElement::Dtmf,XMPPNamespace::Dtmf); xml->setAttribute("action",action); xml->setAttribute("code",s); sess->addChild(xml); } TelEngine::destruct(sess); return sendStanza(iq); } // Send a dtmf method to remote peer bool JGSession::sendDtmfMethod(const char* method) { XMLElement* xml = XMPPUtils::createElement(XMLElement::DtmfMethod, XMPPNamespace::Dtmf); xml->setAttribute("method",method); return sendStanza(createJingle(ActContentInfo,xml)); } // Deny a dtmf method request from remote peer bool JGSession::denyDtmfMethod(XMLElement* element) { if (!element) return false; String id = element->getAttribute("id"); XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqError,m_localJID,m_remoteJID,id); iq->addChild(element); XMLElement* err = XMPPUtils::createError(XMPPError::TypeCancel,XMPPError::SFeatureNotImpl); err->addChild(XMPPUtils::createElement(s_err[XMPPError::DtmfNoMethod],XMPPNamespace::DtmfError)); iq->addChild(err); return sendStanza(iq,false); } // Enqueue a Jabber engine event void JGSession::enqueue(JBEvent* event) { Lock lock(this); if (event->type() == JBEvent::Terminated || event->type() == JBEvent::Destroy) m_events.insert(event); else m_events.append(event); DDebug(m_engine,DebugAll,"Call(%s). Accepted event (%p,%s) [%p]", m_sid.c_str(),event,event->name(),this); } // Process received events. Generate Jingle events JGEvent* JGSession::getEvent(u_int64_t time) { Lock lock(this); if (m_lastEvent) return 0; if (state() == Destroy) return 0; // Deque and process event(s) // Loop until a jingle event is generated or no more events in queue JBEvent* jbev = 0; while (true) { TelEngine::destruct(jbev); jbev = static_cast(m_events.remove(false)); if (!jbev) break; DDebug(m_engine,DebugAll, "Call(%s). Dequeued Jabber event (%p,%s) in state %s [%p]", m_sid.c_str(),jbev,jbev->name(),lookupState(state()),this); // Process Jingle 'set' stanzas if (jbev->type() == JBEvent::IqJingleSet) { // Filter some conditions in which we can't accept any jingle stanza // Incoming pending sessions are waiting for the user to accept/reject them // Outgoing idle sessions are waiting for the user to initiate them if ((state() == Pending && !outgoing()) || (state() == Idle && outgoing())) { confirm(jbev->releaseXML(),XMPPError::SRequest); continue; } m_lastEvent = decodeJingle(jbev); if (!m_lastEvent) { // Destroy incoming session if session initiate stanza contains errors if (!outgoing() && state() == Idle) { m_lastEvent = new JGEvent(JGEvent::Destroy,this,0,"failure"); break; } continue; } DDebug(m_engine,DebugInfo, "Call(%s). Processing action (%u,'%s') state=%s [%p]", m_sid.c_str(),m_lastEvent->action(), lookup(m_lastEvent->action(),s_actions),lookupState(state()),this); // Check for termination events if (m_lastEvent->final()) break; bool error = false; bool fatal = false; switch (state()) { case Active: if (m_lastEvent->action() == ActAccept || m_lastEvent->action() == ActInitiate) error = true; break; case Pending: // Accept session-accept or transport stanzas switch (m_lastEvent->action()) { case ActAccept: changeState(Active); break; case ActTransportAccept: case ActTransport: case ActTransportInfo: case ActTransportCandidates: case ActContentInfo: break; default: error = true; } break; case Idle: // Update data. Terminate if not a session initiating event if (m_lastEvent->action() == ActInitiate) { m_localJID.set(jbev->to()); m_remoteJID.set(jbev->from()); changeState(Pending); } else error = fatal = true; break; default: error = true; } if (!error) { // Automatically confirm some actions // Don't confirm actions that need session user's interaction: // transport and dtmf method negotiation if (m_lastEvent->action() != ActTransport && m_lastEvent->action() != ActDtmfMethod) confirm(m_lastEvent->element()); } else { confirm(m_lastEvent->releaseXML(),XMPPError::SRequest); delete m_lastEvent; m_lastEvent = 0; if (fatal) m_lastEvent = new JGEvent(JGEvent::Destroy,this); else continue; } break; } // Check for responses or failures bool response = jbev->type() == JBEvent::IqJingleRes || jbev->type() == JBEvent::IqJingleErr || jbev->type() == JBEvent::IqResult || jbev->type() == JBEvent::WriteFail; while (response) { bool notSent = true; // Don't use iterator: we stop searching the list at first item removal for (ObjList* o = m_sentStanza.skipNull(); o; o = o->skipNext()) { JGSentStanza* sent = static_cast(o->get()); if (jbev->id() == *sent) { m_sentStanza.remove(sent,true); notSent = false; if (state() == Ending) m_lastEvent = new JGEvent(JGEvent::Destroy,this); break; } } // Ignore it if this event is not a result of a known sent stanza // We didn't expect a result anyway if (notSent) break; // Write fail: Terminate if failed stanza is a Jingle one // Ignore all other write failures if (jbev->type() == JBEvent::WriteFail) { // Check if failed stanza is a jingle one XMLElement* e = jbev->element() ? jbev->element()->findFirstChild() : 0; if (e && e->hasAttribute("xmlns",s_ns[XMPPNamespace::Jingle])) { Debug(m_engine,DebugInfo, "Call(%s). Write stanza failure. Terminating [%p]", m_sid.c_str(),this); m_lastEvent = new JGEvent(JGEvent::Terminated,this,0,"noconn"); } TelEngine::destruct(e); break; } #ifdef DEBUG String error; if (jbev->text()) error << ". Error: " << jbev->text(); Debug(m_engine,DebugAll, "Call(%s). Sent element with id '%s' confirmed%s [%p]", m_sid.c_str(),jbev->id().c_str(),error.safe(),this); #endif // Terminate pending outgoing sessions if session initiate stanza received error if (state() == Pending && outgoing() && jbev->type() == JBEvent::IqJingleErr) m_lastEvent = new JGEvent(JGEvent::Terminated,this,jbev->releaseXML(), jbev->text()?jbev->text().c_str():"failure"); break; } if (response) if (!m_lastEvent) continue; else break; // Silently ignore temporary stream down if (jbev->type() == JBEvent::Terminated) { DDebug(m_engine,DebugInfo, "Call(%s). Stream disconnected in state %s [%p]", m_sid.c_str(),lookupState(state()),this); continue; } // Terminate on stream destroy if (jbev->type() == JBEvent::Destroy) { Debug(m_engine,DebugInfo, "Call(%s). Stream destroyed in state %s [%p]", m_sid.c_str(),lookupState(state()),this); m_lastEvent = new JGEvent(JGEvent::Terminated,this,0,"noconn"); break; } Debug(m_engine,DebugStub,"Call(%s). Unhandled event type %u '%s' [%p]", m_sid.c_str(),jbev->type(),jbev->name(),this); continue; } TelEngine::destruct(jbev); // No event: check first sent stanza's timeout if (!m_lastEvent) { ObjList* o = m_sentStanza.skipNull(); if (o) { JGSentStanza* tmp = static_cast(o->get()); if (tmp->timeout(time)) { Debug(m_engine,DebugNote,"Call(%s). Sent stanza ('%s') timed out [%p]", m_sid.c_str(),tmp->c_str(),this); // Notify the peer anyway (something may be wrong) hangup(false,"Timeout"); m_lastEvent = new JGEvent(JGEvent::Terminated,this,0,"timeout"); } } } if (m_lastEvent) { // Deref the session for final events if (m_lastEvent->final()) { changeState(Destroy); deref(); } DDebug(m_engine,DebugAll, "Call(%s). Raising event (%p,%u) action=%u final=%s [%p]", m_sid.c_str(),m_lastEvent,m_lastEvent->type(), m_lastEvent->action(),String::boolText(m_lastEvent->final()),this); return m_lastEvent; } return 0; } // Send a stanza to the remote peer bool JGSession::sendStanza(XMLElement* stanza, bool confirmation) { Lock lock(this); if (!(state() != Ending && state() != Destroy && stanza && m_stream)) { Debug(m_engine,DebugNote, "Call(%s). Can't send stanza (%p,'%s') in state %s [%p]", m_sid.c_str(),stanza,stanza->name(),lookupState(m_state),this); TelEngine::destruct(stanza); return false; } DDebug(m_engine,DebugAll,"Call(%s). Sending stanza (%p,'%s') [%p]", m_sid.c_str(),stanza,stanza->name(),this); // Check if the stanza should be added to the list of stanzas requiring confirmation if (confirmation && stanza->type() == XMLElement::Iq) { // Create id String id = m_localSid; id << "_" << (unsigned int)m_stanzaId; m_stanzaId++; stanza->setAttribute("id",id); // Append to sent stanzas m_sentStanza.append(new JGSentStanza(id,m_engine->stanzaTimeout() + Time::msecNow())); } // Send. If it fails leave it in the sent items to timeout JBStream::Error res = m_stream->sendStanza(stanza,m_localSid); if (res == JBStream::ErrorNoSocket || res == JBStream::ErrorContext) return false; return true; } // Decode a jingle stanza JGEvent* JGSession::decodeJingle(JBEvent* jbev) { XMLElement* jingle = jbev->child(); if (!jingle) { confirm(jbev->releaseXML(),XMPPError::SBadRequest); return 0; } Action act = (Action)lookup(jingle->getAttribute("type"),s_actions,ActCount); if (act == ActCount) { confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable, "Unknown jingle type"); return 0; } // *** ActTerminate or ActReject if (act == ActTerminate || act == ActReject) { confirm(jbev->element()); return new JGEvent(JGEvent::Terminated,this,jbev->releaseXML(), act==ActTerminate?"hangup":"rejected"); } // *** ActContentInfo: ActDtmf or ActDtmfMethod if (act == ActContentInfo) { // Check dtmf XMLElement* tmp = jingle->findFirstChild(XMLElement::Dtmf); if (tmp) { String reason = tmp->getAttribute("action"); // Expect more then 1 'dtmf' child String text; for (; tmp; tmp = jingle->findNextChild(tmp,XMLElement::Dtmf)) text << tmp->getAttribute("code"); if (!text || (reason != "button-up" && reason != "button-down")) { confirm(jbev->releaseXML(),XMPPError::SBadRequest,"Unknown action"); return 0; } return new JGEvent(ActDtmf,this,jbev->releaseXML(),reason,text); } // Check dtmf method tmp = jingle->findFirstChild(XMLElement::DtmfMethod); if (tmp) { String text = tmp->getAttribute("method"); TelEngine::destruct(tmp); if (text != "rtp" && text != "xmpp") { confirm(jbev->releaseXML(),XMPPError::SBadRequest,"Unknown method"); return 0; } return new JGEvent(ActDtmfMethod,this,jbev->releaseXML(),0,text); } confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable); return 0; } // *** ActAccept ActInitiate ActModify // *** ActTransport: ActTransportInfo/ActTransportCandidates // *** ActTransportAccept // Detect transport type if (act == ActTransportCandidates) { m_transportType = TransportCandidates; act = ActTransport; DDebug(m_engine,DebugInfo,"Call(%s). Set transport='candidates' [%p]", m_sid.c_str(),this); } else if (act == ActTransportInfo || act == ActTransportAccept) { m_transportType = TransportInfo; // Don't set action for transport-accept. Use it only to get transport info if any if (act == ActTransportInfo) act = ActTransport; DDebug(m_engine,DebugInfo,"Call(%s). Set transport='transport-info' [%p]", m_sid.c_str(),this); } // Get transport candidates parent: // transport-info: A 'transport' child element // candidates: The 'session' element // Get media description // Create event, update transport and media XMLElement* trans = jingle; XMLElement* media = 0; JGEvent* event = 0; while (true) { if (m_transportType == TransportInfo) { trans = trans->findFirstChild(XMLElement::Transport); if (trans && !trans->hasAttribute("xmlns",s_ns[XMPPNamespace::JingleTransport])) break; } media = jingle->findFirstChild(XMLElement::Description); if (media && !media->hasAttribute("xmlns",s_ns[XMPPNamespace::JingleAudio])) break; // Don't set the event's element yet: this would invalidate the 'jingle' variable event = new JGEvent(act,this,0); XMLElement* t = trans ? trans->findFirstChild(XMLElement::Candidate) : 0; for (; t; t = trans->findNextChild(t,XMLElement::Candidate)) event->m_transport.append(new JGTransport(t)); event->m_audio.fromXML(media); event->m_id = jbev->id(); event->m_element = jbev->releaseXML(); break; } if (trans != jingle) TelEngine::destruct(trans); TelEngine::destruct(media); if (!event) confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable); return event; } // Create an 'iq' stanza with a 'jingle' child XMLElement* JGSession::createJingle(Action action, XMLElement* element1, XMLElement* element2) { XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,m_localJID,m_remoteJID,0); XMLElement* jingle = XMPPUtils::createElement(XMLElement::Jingle, XMPPNamespace::Jingle); if (action < ActCount) jingle->setAttribute("type",lookup(action,s_actions)); jingle->setAttribute("initiator",outgoing()?m_localJID:m_remoteJID); // jingle->setAttribute("responder",outgoing()?m_remoteJID:m_localJID); jingle->setAttribute(m_sidAttr,m_sid); jingle->addChild(element1); jingle->addChild(element2); iq->addChild(jingle); return iq; } // Send a transport related element to the remote peer bool JGSession::sendTransport(JGTransport* transport, Action act) { if (act != ActTransport && act != ActTransportAccept) return false; // Accept received transport if (act == ActTransportAccept) { TelEngine::destruct(transport); // Clients negotiating transport as 'candidates' don't expect transport-accept if (m_transportType == TransportCandidates) return true; XMLElement* child = JGTransport::createTransport(); return sendStanza(createJingle(ActTransportAccept,0,child)); } // Sent transport if (!transport) return false; // TransportUnknown: send both transport types // TransportInfo: A 'transport' child element of the session element // TransportCandidates: Transport candidates are direct children of the 'session' element XMLElement* child = 0; bool ok = false; switch (m_transportType) { case TransportUnknown: // Fallthrough to send both transport types case TransportInfo: child = JGTransport::createTransport(); transport->addTo(child); ok = sendStanza(createJingle(ActTransportInfo,0,child)); if (!ok || m_transportType == TransportInfo) break; // Fallthrough to send candidates if unknown and succedded case TransportCandidates: child = transport->toXML(); ok = sendStanza(createJingle(ActTransportCandidates,0,child)); } TelEngine::destruct(transport); return ok; } // Event termination notification void JGSession::eventTerminated(JGEvent* event) { lock(); if (event == m_lastEvent) { DDebug(m_engine,DebugAll,"Call(%s). Event (%p,%u) terminated [%p]", m_sid.c_str(),event,event->type(),this); m_lastEvent = 0; } else if (m_lastEvent) Debug(m_engine,DebugNote, "Call(%s). Event (%p,%u) replaced while processed [%p]", m_sid.c_str(),event,event->type(),this); unlock(); } // Change session state void JGSession::changeState(State newState) { if (m_state == newState) return; Debug(m_engine,DebugInfo,"Call(%s). Changing state from %s to %s [%p]", m_sid.c_str(),lookup(m_state,s_states),lookup(newState,s_states),this); m_state = newState; } /* vi: set ts=8 sw=4 sts=4 noet: */