diff --git a/conf.d/yjinglechan.conf.sample b/conf.d/yjinglechan.conf.sample index f8473188..fd144c69 100644 --- a/conf.d/yjinglechan.conf.sample +++ b/conf.d/yjinglechan.conf.sample @@ -3,6 +3,12 @@ [general] ; Global settings of Jabber and Jingle engines +; jingleversion: integer: Jingle session version to use for outgoing calls +; Defaults to the newest version if missing or invalid +; Allowed values are 0 or 1 +; This parameter can be overridden when routing by an 'ojingleversion' parameter +;jingleversion= + ; secure_rtp: boolean: Offer SRTP when negotiating Jingle sessions ; Defaults to disable ;secure_rtp=disable @@ -152,7 +158,7 @@ default=false ; mulaw: bool: Companded-only G711 mu-law (PCMU/8000) -mulaw=default +mulaw=true ; alaw: bool: Companded-only G711 a-law (PCMU/8000) alaw=true diff --git a/libs/yjingle/jbstream.cpp b/libs/yjingle/jbstream.cpp index cc9a7b52..3a64324c 100644 --- a/libs/yjingle/jbstream.cpp +++ b/libs/yjingle/jbstream.cpp @@ -84,17 +84,6 @@ inline bool checkValidXmlns(XMLElement* e, XMPPNamespace::Type ns, return false; } -// Fix some element name conflicts: change type according to their namespace -// Some elements may have the same name in different namespaces -inline void fixXmlType(XMLElement* xml) -{ - if (!xml) - return; - if (xml->type() == XMLElement::Session && - xml->hasAttribute("xmlns",s_ns[XMPPNamespace::Jingle])) - xml->changeType(XMLElement::Jingle); -} - #define DROP_AND_EXIT { dropXML(xml); return; } #define INVALIDXML_AND_EXIT(code,reason) { invalidStreamXML(xml,code,reason); return; } #define ERRORXML_AND_EXIT { errorStreamXML(xml); return; } @@ -1352,8 +1341,6 @@ JBEvent* JBStream::getIqEvent(XMLElement* xml, int iqType, XMPPError::Type& erro } error = XMPPError::NoError; XMLElement* child = xml->findFirstChild(); - if (child) - fixXmlType(child); // Request (type is set or get): check the child (MUST exists) // Result: check it only if it has a child @@ -1369,6 +1356,10 @@ JBEvent* JBStream::getIqEvent(XMLElement* xml, int iqType, XMPPError::Type& erro if (checkValidXmlns(child,XMPPNamespace::Jingle,error)) IQEVENT_SET_REQ(JBEvent::IqJingleGet,JBEvent::IqJingleSet); break; + case XMLElement::Session: + if (checkValidXmlns(child,XMPPNamespace::JingleSession,error)) + IQEVENT_SET_REQ(JBEvent::IqJingleGet,JBEvent::IqJingleSet); + break; case XMLElement::Query: if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoInfo)) IQEVENT_SET_REQ(JBEvent::IqDiscoInfoGet,JBEvent::IqDiscoInfoSet); @@ -1394,6 +1385,10 @@ JBEvent* JBStream::getIqEvent(XMLElement* xml, int iqType, XMPPError::Type& erro if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Jingle)) IQEVENT_SET_RSP(JBEvent::IqJingleRes,JBEvent::IqJingleErr); break; + case XMLElement::Session: + if (XMPPUtils::hasXmlns(*child,XMPPNamespace::JingleSession)) + IQEVENT_SET_RSP(JBEvent::IqJingleRes,JBEvent::IqJingleErr); + break; case XMLElement::Query: if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoInfo)) IQEVENT_SET_RSP(JBEvent::IqDiscoInfoRes,JBEvent::IqDiscoInfoErr); diff --git a/libs/yjingle/jgengine.cpp b/libs/yjingle/jgengine.cpp index 4a544150..824e1f0d 100644 --- a/libs/yjingle/jgengine.cpp +++ b/libs/yjingle/jgengine.cpp @@ -93,7 +93,7 @@ void JGEngine::initialize(const NamedList& params) } // Make an outgoing call -JGSession* JGEngine::call(const String& localJID, const String& remoteJID, +JGSession* JGEngine::call(JGSession::Version ver, const String& localJID, const String& remoteJID, const ObjList& contents, XMLElement* extra, const char* message, const char* subject) { @@ -113,10 +113,19 @@ JGSession* JGEngine::call(const String& localJID, const String& remoteJID, // Create outgoing session bool hasStream = (0 != stream); if (hasStream) { - JGSession* session = new JGSession1(this,stream,localJID,remoteJID, - contents,extra,message,subject); + JGSession* session = 0; + switch (ver) { + case JGSession::Version1: + session = new JGSession1(this,stream,localJID,remoteJID,message); + break; + case JGSession::Version0: + session = new JGSession0(this,stream,localJID,remoteJID,message); + break; + case JGSession::VersionUnknown: + ; + } TelEngine::destruct(stream); - if (session->state() != JGSession::Destroy) { + if (session && session->initiate(contents,extra,subject)) { Lock lock(this); m_sessions.append(session); return (session && session->ref()) ? session : 0; @@ -200,6 +209,10 @@ bool JGEngine::accept(JBEvent* event, bool& processed, bool& insert) ver = JGSession::Version1; sid = child->getAttribute("sid"); } + else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::JingleSession)) { + ver = JGSession::Version0; + sid = child->getAttribute("id"); + } DDebug(this,DebugAll,"Accepting event=%s child=%s sid=%s version=%d", event->name(),child->name(),sid.c_str(),ver); if (sid.null()) { @@ -229,6 +242,9 @@ bool JGEngine::accept(JBEvent* event, bool& processed, bool& insert) case JGSession::Version1: m_sessions.append(new JGSession1(this,event,sid)); break; + case JGSession::Version0: + m_sessions.append(new JGSession0(this,event,sid)); + break; default: Debug(this,DebugStub, "JGEngine::accept(): unhandled session version %d",ver); @@ -336,7 +352,17 @@ void JGEvent::init(JGSession* session) m_session = session; if (m_element) { m_id = m_element->getAttribute("id"); - m_jingle = m_element->findFirstChild(XMLElement::Jingle); + if (m_session) + switch (m_session->version()) { + case JGSession::Version1: + m_jingle = m_element->findFirstChild(XMLElement::Jingle); + break; + case JGSession::Version0: + m_jingle = m_element->findFirstChild(XMLElement::Session); + break; + case JGSession::VersionUnknown: + ; + } } } diff --git a/libs/yjingle/session.cpp b/libs/yjingle/session.cpp index 646503a3..3a15bf19 100644 --- a/libs/yjingle/session.cpp +++ b/libs/yjingle/session.cpp @@ -41,15 +41,82 @@ static void addJingleContents(XMLElement* xml, const ObjList& contents, bool min TelEngine::destruct(jingle); } +// Utility: add session content(s) to an already created stanza's jingle child +// This method is used by the version 0 of the session +static void addJingleContents0(String& name, XMLElement* xml, const ObjList& contents, bool minimal, + bool addDesc, bool addTrans) +{ + if (!xml) + return; + XMLElement* jingle = xml->findFirstChild(XMLElement::Session); + if (!jingle) + return; + for (ObjList* o = contents.skipNull(); o; o = o->skipNext()) { + JGSessionContent* c = static_cast(o->get()); + if (c->type() != JGSessionContent::RtpIceUdp) + continue; + name = c->toString(); + if (addDesc) { + XMLElement* desc = XMPPUtils::createElement(XMLElement::Description, + XMPPNamespace::JingleAudio); + for (ObjList* o = c->m_rtpMedia.skipNull(); o; o = o->skipNext()) { + JGRtpMedia* a = static_cast(o->get()); + desc->addChild(a->toXML()); + } + JGRtpMedia* te = new JGRtpMedia("106","telephone-event","8000","",""); + desc->addChild(te->toXML()); + TelEngine::destruct(te); + jingle->addChild(desc); + } + if (addTrans) { + XMLElement* trans = XMPPUtils::createElement(XMLElement::Transport, + XMPPNamespace::JingleTransport); + if (!minimal) { + for (ObjList* o = c->m_rtpLocalCandidates.skipNull(); o; o = o->skipNext()) { + JGRtpCandidate* rc = static_cast(o->get()); + XMLElement* xml = new XMLElement(XMLElement::Candidate); + xml->setAttribute("name","rtp"); + xml->setAttributeValid("generation",rc->m_generation); + xml->setAttributeValid("address",rc->m_address); + xml->setAttributeValid("port",rc->m_port); + xml->setAttributeValid("network","0"); + xml->setAttributeValid("protocol",rc->m_protocol); + xml->setAttribute("username",c->m_rtpLocalCandidates.m_ufrag); + xml->setAttribute("password",c->m_rtpLocalCandidates.m_password); + xml->setAttributeValid("type","local"); + xml->setAttributeValid("preference","1"); + trans->addChild(xml); + } + } + jingle->addChild(trans); + } + } + TelEngine::destruct(jingle); +} + // Utility: add xml element child to an already created stanza's jingle child static void addJingleChild(XMLElement* xml, XMLElement* child) { if (!(xml && child)) return; XMLElement* jingle = xml->findFirstChild(XMLElement::Jingle); - if (!jingle) + if (jingle) + jingle->addChild(child); + else + TelEngine::destruct(child); + TelEngine::destruct(jingle); +} + +// Utility: add xml element child to an already created stanza's jingle child +static void addJingleChild0(XMLElement* xml, XMLElement* child) +{ + if (!(xml && child)) return; - jingle->addChild(child); + XMLElement* jingle = xml->findFirstChild(XMLElement::Session); + if (jingle) + jingle->addChild(child); + else + TelEngine::destruct(child); TelEngine::destruct(jingle); } @@ -338,7 +405,7 @@ JGRtpCandidate* JGRtpCandidates::findByComponent(unsigned int component) return 0; } -// Generate a random password to be used with ICE-UDP transport +// Generate a random password or username to be used with ICE-UDP transport // Maximum number of characters. The maxmimum value is 256. // The minimum value is 22 for password and 4 for username void JGRtpCandidates::generateIceToken(String& dest, bool pwd, unsigned int max) @@ -357,6 +424,15 @@ void JGRtpCandidates::generateIceToken(String& dest, bool pwd, unsigned int max) dest = dest.substr(0,max); } +// Generate a random password or username to be used with old ICE-UDP transport +void JGRtpCandidates::generateOldIceToken(String& dest) +{ + dest = ""; + while (dest.length() < 16) + dest << (int)random(); + dest = dest.substr(0,16); +} + /** * JGSessionContent @@ -610,6 +686,12 @@ XMLElement* JGStreamHost::buildRsp(const char* jid) * JGSession */ +TokenDict JGSession::s_versions[] = { + {"0", Version0}, + {"1", Version1}, + {0,0} +}; + TokenDict JGSession::s_states[] = { {"Idle", Idle}, {"Pending", Pending}, @@ -634,6 +716,20 @@ TokenDict JGSession::s_reasons[] = { {0,0} }; +TokenDict JGSession::s_actions0[] = { + {"accept", ActAccept}, + {"initiate", ActInitiate}, + {"terminate", ActTerminate}, + {"info", ActInfo}, + {"transport-info", ActTransportInfo}, + {"transport-accept", ActTransportAccept}, + {"content-info", ActContentInfo}, + {"DTMF", ActDtmf}, + {"ringing", ActRinging}, + {"mute", ActMute}, + {0,0} +}; + TokenDict JGSession::s_actions1[] = { {"session-accept", ActAccept}, {"session-initiate", ActInitiate}, @@ -821,7 +917,7 @@ bool JGSession::sendDtmf(const char* dtmf, unsigned int msDuration, String* stan if (!(dtmf && *dtmf)) return false; - XMLElement* iq = createJingle(ActInfo); + XMLElement* iq = createJingle(version() != Version0 ? ActInfo : ActContentInfo); XMLElement* sess = iq->findFirstChild(); if (!sess) { TelEngine::destruct(iq); @@ -1347,7 +1443,9 @@ const char* JGSession::lookupAction(int act, Version ver) switch (ver) { case Version1: return lookup(act,s_actions1); - default: + case Version0: + return lookup(act,s_actions0); + case VersionUnknown: ; } return 0; @@ -1359,33 +1457,302 @@ JGSession::Action JGSession::lookupAction(const char* str, Version ver) switch (ver) { case Version1: return (Action)lookup(str,s_actions1,ActCount); - default: + case Version0: + return (Action)lookup(str,s_actions0,ActCount); + case VersionUnknown: ; } return ActCount; } +/** + * JGSession0 + */ + +// Create an outgoing session +JGSession0::JGSession0(JGEngine* engine, JBStream* stream, + const String& callerJID, const String& calledJID, const char* msg) + : JGSession(Version0,engine,stream,callerJID,calledJID,msg) +{ +} + +// Create an incoming session +JGSession0::JGSession0(JGEngine* engine, JBEvent* event, const String& id) + : JGSession(Version0,engine,event,id) +{ + m_sessContentName = m_localSid + "_content"; +} + +// Destructor +JGSession0::~JGSession0() +{ +} + +// Check if a given XML element is valid jingle one +XMLElement* JGSession0::checkJingle(XMLElement* xml) +{ + if (xml && xml->type() == XMLElement::Session && + XMPPUtils::hasXmlns(*xml,XMPPNamespace::JingleSession)) + return xml; + return 0; +} + +// Accept a Pending incoming session +bool JGSession0::accept(const ObjList& contents, String* stanzaId) +{ + Lock lock(this); + if (outgoing() || state() != Pending) + return false; + XMLElement* xml = createJingle(ActAccept); + addJingleContents0(m_sessContentName,xml,contents,true,true,true); + if (!sendStanza(xml,stanzaId)) + return false; + changeState(Active); + return true; +} + +// Send a stanza with session content(s) +bool JGSession0::sendContent(Action action, const ObjList& contents, String* stanzaId) +{ + Lock lock(this); + if (state() != Pending && state() != Active) + return false; + bool minimal = false; + bool addDesc = true; + bool addTrans = true; + switch (action) { + case ActTransportInfo: + addDesc = false; + break; + case ActTransportAccept: + minimal = true; + addDesc = false; + addTrans = true; + break; + default: + return false; + }; + // Make sure we dont't terminate the session on failure + String tmp; + if (!stanzaId) { + tmp = "Content" + String(Time::secNow()); + stanzaId = &tmp; + } + XMLElement* xml = createJingle(action); + addJingleContents0(m_sessContentName,xml,contents,minimal,addDesc,addTrans); + return sendStanza(xml,stanzaId); +} + +// Build and send the initial message on an outgoing session +bool JGSession0::initiate(const ObjList& contents, XMLElement* extra, const char* subject) +{ + XMLElement* xml = createJingle(ActInitiate); + addJingleContents0(m_sessContentName,xml,contents,true,true,true); + addJingleChild0(xml,extra); + if (!null(subject)) + addJingleChild0(xml,new XMLElement(XMLElement::Subject,0,subject)); + if (sendStanza(xml)) { + changeState(Pending); + return true; + } + changeState(Destroy); + return false; +} + +// Decode a valid jingle set event. Set the event's data on success +JGEvent* JGSession0::decodeJingle(JBEvent* jbev) +{ + XMLElement* jingle = jbev->child(); + if (!jingle) { + confirm(jbev->releaseXML(),XMPPError::SBadRequest); + return 0; + } + + Action act = getAction(jingle); + if (act == ActCount) { + confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable, + "Unknown session action"); + return 0; + } + + // *** ActTerminate + if (act == ActTerminate) { + // Confirm here: this is a final event, + // stanza won't be confirmed in getEvent() + m_recvTerminate = true; + const char* reason = 0; + const char* text = 0; + XMLElement* res = jingle->findFirstChild(XMLElement::Reason); + if (res) { + XMLElement* tmp = res->findFirstChild(); + if (tmp && tmp->type() != XMLElement::Text) + reason = tmp->name(); + TelEngine::destruct(tmp); + tmp = res->findFirstChild(XMLElement::Text); + if (tmp) + text = tmp->getText(); + TelEngine::destruct(tmp); + TelEngine::destruct(res); + } + JGEvent* ev = new JGEvent(JGEvent::Terminated,this,jbev->releaseXML(),reason,text); + ev->setAction(act); + ev->confirmElement(); + return ev; + } + + // *** ActContentInfo --> ActDtmf + if (act == ActContentInfo) { + // Check dtmf + // Expect more then 1 'dtmf' child + XMLElement* tmp = jingle->findFirstChild(XMLElement::Dtmf); + String text; + for (; tmp; tmp = jingle->findNextChild(tmp,XMLElement::Dtmf)) { + String reason = tmp->getAttribute("action"); + if (reason == "button-up") + text << tmp->getAttribute("code"); + } + if (text) + return new JGEvent(ActDtmf,this,jbev->releaseXML(),0,text); + confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable); + return 0; + } + + // *** ActInfo + if (act == ActInfo) { + // Check info element + // Return ActInfo event to signal ping (XEP-0166 6.8) + XMLElement* child = jingle->findFirstChild(); + if (!child) + return new JGEvent(ActInfo,this,jbev->releaseXML()); + + JGEvent* event = 0; + Action a = ActCount; + XMPPNamespace::Type ns = XMPPNamespace::Count; + // Check namespace and build event + switch (child->type()) { + case XMLElement::Ringing: + a = ActRinging; + ns = XMPPNamespace::JingleRtpInfoOld; + break; + case XMLElement::Mute: + a = ActMute; + ns = XMPPNamespace::JingleRtpInfoOld; + break; + default: ; + } + if (a != ActCount && XMPPUtils::hasXmlns(*child,ns)) + event = new JGEvent(a,this,jbev->releaseXML()); + else + confirm(jbev->releaseXML(),XMPPError::SFeatureNotImpl); + TelEngine::destruct(child); + return event; + } + + if (act == ActTransportAccept) { + confirm(jbev->element()); + return 0; + } + + // Get transport + // Get media description + // Create event, update transport and media + XMLElement* media = 0; + XMLElement* trans = jingle; + JGSessionContent* c = 0; + JGEvent* event = 0; + while (true) { + c = new JGSessionContent(JGSessionContent::RtpIceUdp,m_sessContentName, + JGSessionContent::SendBoth,JGSessionContent::CreatorInitiator); + c->m_rtpRemoteCandidates.m_type = JGRtpCandidates::RtpIceUdp; + // Build media + if (act == ActInitiate || act == ActAccept) { + media = jingle->findFirstChild(XMLElement::Description); + if (media && XMPPUtils::hasXmlns(*media,XMPPNamespace::JingleAudio)) { + c->m_rtpMedia.fromXML(media); + c->m_rtpMedia.m_media = JGRtpMediaList::Audio; + } + else + break; + } + // Build transport + trans = trans->findFirstChild(XMLElement::Transport); + if (trans && !XMPPUtils::hasXmlns(*trans,XMPPNamespace::JingleTransport)) { + if (trans != jingle) + TelEngine::destruct(trans); + else + trans = 0; + } + XMLElement* t = trans ? trans->findFirstChild(XMLElement::Candidate) : 0; + if (t) { + JGRtpCandidate* cd = new JGRtpCandidate(m_localSid + "_transport"); + cd->m_component = "1"; + cd->m_generation = t->getAttribute("generation"); + cd->m_address = t->getAttribute("address"); + cd->m_port = t->getAttribute("port"); + cd->m_protocol = t->getAttribute("protocol");; + cd->m_generation = t->getAttribute("generation"); + cd->m_type = t->getAttribute("type"); + c->m_rtpRemoteCandidates.m_ufrag = t->getAttribute("username"); + c->m_rtpRemoteCandidates.m_password = t->getAttribute("password"); + c->m_rtpRemoteCandidates.append(cd); + TelEngine::destruct(t); + } + else if (act == ActTransportInfo) + break; + // Don't set the event's element yet: this would invalidate the 'jingle' variable + event = new JGEvent(act,this,jbev->releaseXML()); + event->m_contents.append(c); + break; + } + if (trans != jingle) + TelEngine::destruct(trans); + TelEngine::destruct(media); + if (!event) { + TelEngine::destruct(c); + confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable); + } + return event; +} + +// Create an 'iq' stanza with a 'jingle' child +XMLElement* JGSession0::createJingle(Action action, XMLElement* element1, + XMLElement* element2, XMLElement* element3) +{ + XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,m_localJID,m_remoteJID,0); + XMLElement* jingle = XMPPUtils::createElement(XMLElement::Session, + XMPPNamespace::JingleSession); + if (action < ActCount) + jingle->setAttribute("type",lookupAction(action,version())); + jingle->setAttribute("initiator",outgoing() ? m_localJID : m_remoteJID); + jingle->setAttribute("responder",outgoing() ? m_remoteJID : m_localJID); + jingle->setAttribute("id",m_sid); + jingle->addChild(element1); + jingle->addChild(element2); + jingle->addChild(element3); + iq->addChild(jingle); + return iq; +} + +// Create a dtmf XML element +XMLElement* JGSession0::createDtmf(const char* dtmf, unsigned int msDuration) +{ + XMLElement* xml = XMPPUtils::createElement(XMLElement::Dtmf,XMPPNamespace::DtmfOld); + xml->setAttribute("action","button-up"); + xml->setAttribute("code",dtmf); + return xml; +} + + /** * JGSession1 */ // Create an outgoing session JGSession1::JGSession1(JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, - const ObjList& contents, XMLElement* extra, const char* msg, - const char* subject) + const String& callerJID, const String& calledJID, const char* msg) : JGSession(Version1,engine,stream,callerJID,calledJID,msg) { - XMLElement* xml = createJingle(ActInitiate); - addJingleContents(xml,contents,false,true,true,true); - addJingleChild(xml,extra); - if (!null(subject)) - addJingleChild(xml,new XMLElement(XMLElement::Subject,0,subject)); - if (sendStanza(xml)) - changeState(Pending); - else - changeState(Destroy); } // Create an incoming session @@ -1399,6 +1766,22 @@ JGSession1::~JGSession1() { } +// Build and send the initial message on an outgoing session +bool JGSession1::initiate(const ObjList& contents, XMLElement* extra, const char* subject) +{ + XMLElement* xml = createJingle(ActInitiate); + addJingleContents(xml,contents,false,true,true,true); + addJingleChild(xml,extra); + if (!null(subject)) + addJingleChild(xml,new XMLElement(XMLElement::Subject,0,subject)); + if (sendStanza(xml)) { + changeState(Pending); + return true; + } + changeState(Destroy); + return false; +} + // Check if a given XML element is valid jingle one XMLElement* JGSession1::checkJingle(XMLElement* xml) { diff --git a/libs/yjingle/xmpputils.cpp b/libs/yjingle/xmpputils.cpp index a90c1855..92e9ab04 100644 --- a/libs/yjingle/xmpputils.cpp +++ b/libs/yjingle/xmpputils.cpp @@ -88,6 +88,11 @@ TokenDict XMPPNamespace::s_value[] = { {"urn:xmpp:jingle:transports:bytestreams:0", JingleTransportByteStreams}, {"urn:xmpp:jingle:transfer:0", JingleTransfer}, {"urn:xmpp:jingle:dtmf:0", Dtmf}, + {"http://www.google.com/session", JingleSession}, + {"http://www.google.com/session/phone", JingleAudio}, + {"http://www.google.com/transport/p2p", JingleTransport}, + {"urn:xmpp:jingle:apps:rtp:info", JingleRtpInfoOld}, + {"http://jabber.org/protocol/jingle/info/dtmf", DtmfOld}, {"http://jabber.org/protocol/commands", Command}, {"http://www.google.com/xmpp/protocol/voice/v1", CapVoiceV1}, {0,0} diff --git a/libs/yjingle/xmpputils.h b/libs/yjingle/xmpputils.h index 9a3af367..fff68d69 100644 --- a/libs/yjingle/xmpputils.h +++ b/libs/yjingle/xmpputils.h @@ -212,6 +212,11 @@ public: JingleTransportByteStreams, // urn:xmpp:jingle:transports:bytestreams:0 JingleTransfer, // urn:xmpp:jingle:transfer:0 Dtmf, // urn:xmpp:jingle:dtmf:0 + JingleSession, // http://www.google.com/session + JingleAudio, // http://www.google.com/session/phone + JingleTransport, // http://www.google.com/transport/p2p + JingleRtpInfoOld, // urn:xmpp:jingle:apps:rtp:info + DtmfOld, // http://jabber.org/protocol/jingle/info/dtmf Command, // http://jabber.org/protocol/command CapVoiceV1, // http://www.google.com/xmpp/protocol/voice/v1 Count, diff --git a/libs/yjingle/yatejingle.h b/libs/yjingle/yatejingle.h index abab3370..9a00ec9b 100644 --- a/libs/yjingle/yatejingle.h +++ b/libs/yjingle/yatejingle.h @@ -40,6 +40,7 @@ class JGRtpCandidates; // A list of RTP transport candidates class JGSessionContent; // A Jingle session content class JGStreamHost; // A Jingle file transfer stream host class JGSession; // A basic Jingle session +class JGSession0; // A session implementing the old jingle protocol class JGSession1; // The version 1 of a jingle session class JGEvent; // An event generated by a Jingle session class JGEngine; // The Jingle engine @@ -215,7 +216,7 @@ public: * @param cryptoMandatory True to require media encryption */ inline JGRtpMediaList(Media m = MediaMissing, bool cryptoMandatory = false) - : m_media(m), m_cryptoMandatory(cryptoMandatory) + : m_media(m), m_cryptoMandatory(cryptoMandatory), m_ready(false) {} /** @@ -289,6 +290,11 @@ public: bool m_cryptoMandatory; ObjList m_cryptoLocal; ObjList m_cryptoRemote; + + /** + * Flag indicating wether media was negotiated + */ + bool m_ready; }; @@ -338,7 +344,7 @@ public: String m_network; // NIC card (diagnostic only) String m_priority; // Candidate priority String m_protocol; // The only allowable value is "udp" - String m_type; // A Candidate Type as defined in ICE-CORE. + String m_type; // A Candidate Type as defined in ICE-CORE }; @@ -381,6 +387,14 @@ public: generateIceToken(m_ufrag,false); } + /** + * Fill password and ufrag data using old transport restrictions (16 bytes length) + */ + inline void generateOldIceAuth() { + generateOldIceToken(m_password); + generateOldIceToken(m_ufrag); + } + /** * Find a candidate by its component value * @param component The value to search @@ -403,7 +417,7 @@ public: void fromXML(XMLElement* element); /** - * Generate a random password to be used with ICE-UDP transport + * Generate a random password or username to be used with ICE-UDP transport * @param dest Destination string * @param pwd True to generate a password, false to generate an username (ufrag) * @param max Maximum number of characters. The maxmimum value is 256. @@ -411,6 +425,12 @@ public: */ static void generateIceToken(String& dest, bool pwd, unsigned int max = 0); + /** + * Generate a random password or username to be used with old ICE-UDP transport + * @param dest Destination string + */ + static void generateOldIceToken(String& dest); + /** * Get the name associated with a list's type * @param t The desired type @@ -676,8 +696,9 @@ public: * Jingle session version */ enum Version { + Version0 = 0, Version1 = 1, - VersionUnknown, + VersionUnknown }; /** @@ -726,6 +747,7 @@ public: ActContentModify, // content-modify ActContentReject, // content-reject ActContentRemove, // content-remove + ActContentInfo, // content-info ActTransfer, // session-info: Transfer ActRinging, // session-info: Ringing ActTrying, // session-info: Trying @@ -981,6 +1003,24 @@ public: static XMLElement* buildTransfer(const String& transferTo, const String& transferFrom, const String& sid = String::empty()); + /** + * Get the session version associated with a text + * @param value The version text + * @param def Default value to return if not found + * @return Session Version value + */ + static inline Version lookupVersion(const char* value, Version def = VersionUnknown) + { return (Version)lookup(value,s_versions,def); } + + /** + * Get the session version name + * @param value The version value + * @param def Default value to return if not found + * @return Session version name or the default value if not found + */ + static inline const char* lookupVersion(int value, const char* def = "unknown") + { return lookup(value,s_versions,def); } + /** * Get the termination code associated with a text * @param value The termination text @@ -1023,6 +1063,11 @@ public: */ static Action lookupAction(const char* str, Version ver); + /** + * Session version names + */ + static TokenDict s_versions[]; + /** * Termination reasons */ @@ -1033,6 +1078,11 @@ public: */ static TokenDict s_states[]; + /** + * Action names for version Version0 + */ + static TokenDict s_actions0[]; + /** * Action names for version Version1 */ @@ -1061,6 +1111,16 @@ protected: */ JGSession(Version ver, JGEngine* engine, JBEvent* event, const String& id); + /** + * Build and send the initial message on an outgoing session + * @param contents The session contents to be sent with session initiate element + * @param extra Optional extra child to be added to the session initiate element + * @param subject Optional session subject + * @return True on success + */ + virtual bool initiate(const ObjList& contents, XMLElement* extra, + const char* subject = 0) = 0; + /** * Get a Jingle event from the queue. * This method is thread safe @@ -1184,6 +1244,107 @@ private: }; +/** + * A session implementing the old jingle protocol + * @short The version 0 of a jingle session + */ +class YJINGLE_API JGSession0 : public JGSession +{ + friend class JGEvent; + friend class JGEngine; +public: + /** + * Destructor + */ + virtual ~JGSession0(); + + /** + * Check if a given XML element is valid jingle one + * @param xml Element to check + * @return The given element if it's a valid jingle element, 0 otherwise + */ + virtual XMLElement* checkJingle(XMLElement* xml); + + /** + * Accept a Pending incoming session. + * This method is thread safe + * @param contents The list of accepted contents + * @param stanzaId Optional string to be filled with sent stanza id (used to track the response) + * @return False if send failed + */ + virtual bool accept(const ObjList& contents, String* stanzaId = 0); + +protected: + /** + * Constructor. Create an outgoing session + * @param engine The engine that owns this session + * @param stream The stream this session is bound to + * @param callerJID The caller's full JID + * @param calledJID The called party's full JID + * @param msg Optional message to be sent before session initiate + */ + JGSession0(JGEngine* engine, JBStream* stream, + const String& callerJID, const String& calledJID, const char* msg = 0); + + /** + * Constructor. Create an incoming session. + * @param engine The engine that owns this session + * @param event A valid Jabber Jingle event with action session initiate + * @param id Session id + */ + JGSession0(JGEngine* engine, JBEvent* event, const String& id); + + /** + * Build and send the initial message on an outgoing session + * @param contents The session contents to be sent with session initiate element + * @param extra Optional extra child to be added to the session initiate element + * @param subject Optional session subject + * @return True on success + */ + virtual bool initiate(const ObjList& contents, XMLElement* extra, + const char* subject = 0); + + /** + * Send a stanza with session content(s) + * This method is thread safe + * @param action Must be a transport- action + * @param contents Non empty list with content(s) to send + * @param stanzaId Optional string to be filled with sent + * stanza id (used to track the response) + * @return False if send failed + */ + virtual bool sendContent(Action action, const ObjList& contents, String* stanzaId = 0); + + /** + * Decode a valid jingle set event. Set the event's data on success + * @param jbev The event to decode + * @return JGEvent pointer or 0 + */ + virtual JGEvent* decodeJingle(JBEvent* jbev); + + /** + * Create an 'iq' of type 'set' with a 'jingle' child + * @param action The action of the Jingle stanza + * @param element1 Optional child element + * @param element2 Optional child element + * @param element3 Optional child element + * @return Valid XMLElement pointer + */ + virtual XMLElement* createJingle(Action action, XMLElement* element1 = 0, + XMLElement* element2 = 0, XMLElement* element3 = 0); + + /** + * Create a dtmf XML element + * @param dtmf The dtmf string + * @param msDuration The tone duration in miliseconds. Ignored if 0 + * @return Valid XMLElement pointer or 0 + */ + virtual XMLElement* createDtmf(const char* dtmf, unsigned int msDuration = 0); + +protected: + String m_sessContentName; // Content name advertised to upper layer +}; + /** * A session implementing the Jingle protocol including session transfer and file transfer * @short The version 1 of a jingle session @@ -1264,15 +1425,10 @@ protected: * @param stream The stream this session is bound to * @param callerJID The caller's full JID * @param calledJID The called party's full JID - * @param contents The session contents to be sent with session initiate element - * @param extra Optional extra child to be added to the session initiate element * @param msg Optional message to be sent before session initiate - * @param subject Optional session subject */ JGSession1(JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, - const ObjList& contents, XMLElement* extra = 0, const char* msg = 0, - const char* subject = 0); + const String& callerJID, const String& calledJID, const char* msg = 0); /** * Constructor. Create an incoming session. @@ -1282,6 +1438,16 @@ protected: */ JGSession1(JGEngine* engine, JBEvent* event, const String& id); + /** + * Build and send the initial message on an outgoing session + * @param contents The session contents to be sent with session initiate element + * @param extra Optional extra child to be added to the session initiate element + * @param subject Optional session subject + * @return True on success + */ + virtual bool initiate(const ObjList& contents, XMLElement* extra, + const char* subject = 0); + /** * Decode a valid jingle set event. Set the event's data on success * @param jbev The event to decode @@ -1313,7 +1479,6 @@ protected: */ virtual void processJabberIqEvent(JBEvent& ev); -private: }; /** @@ -1323,6 +1488,7 @@ private: class YJINGLE_API JGEvent { friend class JGSession; + friend class JGSession0; friend class JGSession1; public: /** @@ -1587,6 +1753,7 @@ public: /** * Make an outgoing call. * 'media' and 'transport' will be invalid on exit. Don't delete them + * @param ver The session version to use * @param callerName The local peer's username * @param remoteJID The remote peer's JID * @param contents The list of session content(s) @@ -1595,7 +1762,7 @@ public: * @param subject Optional session subject * @return Valid JGSession pointer (referenced) on success */ - JGSession* call(const String& callerName, const String& remoteJID, + JGSession* call(JGSession::Version ver, const String& callerName, const String& remoteJID, const ObjList& contents, XMLElement* extra = 0, const char* msg = 0, const char* subject = 0); diff --git a/modules/yjinglechan.cpp b/modules/yjinglechan.cpp index 315c2ce2..2bdce805 100644 --- a/modules/yjinglechan.cpp +++ b/modules/yjinglechan.cpp @@ -388,6 +388,8 @@ private: Mutex m_mutex; // Lock transport and session State m_state; // Connection state JGSession* m_session; // Jingle session attached to this connection + bool m_rtpStarted; // RTP started flag used by version 0 of the jingle session + JGSession::Version m_sessVersion; // Jingle session version JabberID m_local; // Local user's JID JabberID m_remote; // Remote user's JID ObjList m_audioContents; // The list of negotiated audio contents @@ -621,6 +623,7 @@ static bool s_attachPresToCmd = false; // Attach presence to command static bool s_userRoster = false; // Send client roster with user.roster or resource.notify static bool s_useCrypto = false; static bool s_cryptoMandatory = false; +static JGSession::Version s_sessVersion = JGSession::VersionUnknown; // Default jingle session version for outgoing calls static YJBEngine* s_jabber = 0; static YJGEngine* s_jingle = 0; static YJBMessage* s_message = 0; @@ -1310,13 +1313,16 @@ YJGConnection::YJGConnection(Message& msg, const char* caller, const char* calle bool available, const char* file) : Channel(&plugin,0,true), m_mutex(true,"YJGConnection"), - m_state(Pending), m_session(0), m_local(caller), - m_remote(called), m_audioContent(0), + m_state(Pending), m_session(0), m_rtpStarted(false), m_sessVersion(s_sessVersion), + m_local(caller), m_remote(called), m_audioContent(0), m_callerPrompt(msg.getValue("callerprompt")), m_sendRawRtpFirst(true), m_useCrypto(s_useCrypto), m_cryptoMandatory(s_cryptoMandatory), m_hangup(false), m_timeout(0), m_transferring(false), m_recvTransferStanza(0), m_dataFlags(0), m_ftStatus(FTNone), m_ftHostDirection(FTHostNone) { + NamedString* ver = msg.getParam("ojingleversion"); + if (ver) + m_sessVersion = JGSession::lookupVersion(*ver); m_subject = msg.getValue("subject"); String uri = msg.getValue("diverteruri",msg.getValue("diverter")); // Skip protocol if present @@ -1388,7 +1394,8 @@ YJGConnection::YJGConnection(Message& msg, const char* caller, const char* calle YJGConnection::YJGConnection(JGEvent* event) : Channel(&plugin,0,false), m_mutex(true,"YJGConnection"), - m_state(Active), m_session(event->session()), + m_state(Active), m_session(event->session()), m_rtpStarted(false), + m_sessVersion(event->session()->version()), m_local(event->session()->local()), m_remote(event->session()->remote()), m_audioContent(0), m_sendRawRtpFirst(true), m_useCrypto(s_useCrypto), m_cryptoMandatory(s_cryptoMandatory), @@ -1479,6 +1486,13 @@ YJGConnection::YJGConnection(JGEvent* event) m_session->sendContent(JGSession::ActContentRemove,m_ftContents); m_ftContents.clear(); } + // Send transport accept now for version 0 + if (m_sessVersion == JGSession::Version0) { + ObjList* o = m_audioContents.skipNull(); + if (o) + m_session->sendContent(JGSession::ActTransportAccept, + static_cast(o->get())); + } } else { m_state = Pending; @@ -1516,6 +1530,7 @@ bool YJGConnection::route() m->addParam("calleruri",BUILD_XMPP_URI(m_remote)); if (m_subject) m->addParam("subject",m_subject); + m->addParam("jingleversion",JGSession::lookupVersion(m_sessVersion)); m_mutex.lock(); // TODO: add remote ip/port // Fill file transfer data @@ -1603,9 +1618,11 @@ bool YJGConnection::msgAnswered(Message& msg) { Debug(this,DebugCall,"msgAnswered [%p]",this); if (m_ftStatus == FTNone) { - clearEndpoint(); + if (m_sessVersion != JGSession::Version0) + clearEndpoint(); m_mutex.lock(); - resetCurrentAudioContent(true,false,false); + if (m_sessVersion != JGSession::Version0 || !m_rtpStarted) + resetCurrentAudioContent(true,false,true); ObjList tmp; if (m_audioContent) tmp.append(m_audioContent)->setDelete(false); @@ -2107,25 +2124,38 @@ bool YJGConnection::handleEvent(JGEvent* event) return setupSocksFileTransfer(true); // Update media Debug(this,DebugCall,"Remote peer answered the call [%p]",this); - m_state = Active; - removeCurrentAudioContent(); + m_state = Active; + // Remove current content only if not Version0 + // For this version we keep only 1 content + if (m_sessVersion != JGSession::Version0) + removeCurrentAudioContent(); for (ObjList* o = event->m_contents.skipNull(); o; o = o->skipNext()) { JGSessionContent* recv = static_cast(o->get()); JGSessionContent* c = findContent(*recv,m_audioContents); if (!c) continue; // Update credentials for ICE-UDP - c->m_rtpRemoteCandidates.m_password = recv->m_rtpRemoteCandidates.m_password; - c->m_rtpRemoteCandidates.m_ufrag = recv->m_rtpRemoteCandidates.m_ufrag; + // only if not version 0 (this version only sends media in accept) + if (m_sessVersion != JGSession::Version0) { + c->m_rtpRemoteCandidates.m_password = recv->m_rtpRemoteCandidates.m_password; + c->m_rtpRemoteCandidates.m_ufrag = recv->m_rtpRemoteCandidates.m_ufrag; + } // Update media if (!matchMedia(*c,*recv)) { Debug(this,DebugInfo,"No common media for content=%s [%p]", c->toString().c_str(),this); continue; } + c->m_rtpMedia.m_ready = true; // Update transport(s) - bool changed = updateCandidate(1,*c,*recv); - changed = updateCandidate(2,*c,*recv) || changed; + // Force changed for Version0 (we have valid common media for this version) + bool changed = false; + if (m_sessVersion != JGSession::Version0) { + changed = updateCandidate(1,*c,*recv); + changed = updateCandidate(2,*c,*recv) || changed; + } + else + changed = true; if (changed && !m_audioContent && recv->isSession()) resetCurrentAudioContent(true,false,true,c); } @@ -2237,19 +2267,26 @@ bool YJGConnection::presenceChanged(bool available) XMLElement* transfer = 0; if (m_transferFrom) transfer = JGSession::buildTransfer(String::empty(),m_transferFrom); - if (m_sendRawRtpFirst) { - addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp)); - addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp)); + if (m_sessVersion == JGSession::Version1) { + if (m_sendRawRtpFirst) { + addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp)); + addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp)); + } + else { + addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp)); + addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp)); + } } - else { + else if (m_sessVersion == JGSession::Version0) addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp)); - addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp)); - } - m_session = s_jingle->call(m_local,m_remote,m_audioContents,transfer, + m_session = s_jingle->call(m_sessVersion,m_local,m_remote,m_audioContents,transfer, m_callerPrompt,m_subject); + // Init now the transport for version 0 + if (m_session && m_session->version() == JGSession::Version0) + resetCurrentAudioContent(true,false); } else - m_session = s_jingle->call(m_local,m_remote,m_ftContents,0, + m_session = s_jingle->call(m_sessVersion,m_local,m_remote,m_ftContents,0, m_callerPrompt,m_subject); if (!m_session) { hangup("noconn"); @@ -2419,7 +2456,7 @@ void YJGConnection::processActionTransportInfo(JGEvent* event) if (!event) return; - event->confirmElement(); + bool ok = m_sessVersion != JGSession::Version0; bool startAudioContent = false; JGSessionContent* newContent = 0; for (ObjList* o = event->m_contents.skipNull(); o; o = o->skipNext()) { @@ -2430,11 +2467,18 @@ void YJGConnection::processActionTransportInfo(JGEvent* event) event->actionName(),c->toString().c_str(),this); continue; } + // Update transport(s) + bool changed = updateCandidate(1,*cc,*c); + // Version0: the session will give us only 1 content + if (!changed && m_sessVersion == JGSession::Version0) { + ok = false; + break; + } + ok = true; // Update credentials for ICE-UDP cc->m_rtpRemoteCandidates.m_password = c->m_rtpRemoteCandidates.m_password; cc->m_rtpRemoteCandidates.m_ufrag = c->m_rtpRemoteCandidates.m_ufrag; - // Update transport(s) - bool changed = updateCandidate(1,*cc,*c); + // Check RTCP changed = updateCandidate(2,*cc,*c) || changed; if (!changed) continue; @@ -2447,13 +2491,17 @@ void YJGConnection::processActionTransportInfo(JGEvent* event) else newContent = cc; } - - if (newContent) { - if (!dataFlags(OnHold)) - resetCurrentAudioContent(isAnswered(),!isAnswered(),true,newContent); + if (ok) { + event->confirmElement(); + if (newContent) { + if (!dataFlags(OnHold)) + resetCurrentAudioContent(isAnswered(),!isAnswered(),true,newContent); + } + else if ((startAudioContent && !startRtp()) || !(m_audioContent || dataFlags(OnHold))) + resetCurrentAudioContent(isAnswered(),!isAnswered()); } - else if ((startAudioContent && !startRtp()) || !(m_audioContent || dataFlags(OnHold))) - resetCurrentAudioContent(isAnswered(),!isAnswered()); + else + event->confirmElement(XMPPError::SNotAcceptable); enqueueCallProgress(); } @@ -2465,6 +2513,13 @@ bool YJGConnection::updateCandidate(unsigned int component, JGSessionContent& lo if (!rtpRecv) return false; JGRtpCandidate* rtp = local.m_rtpRemoteCandidates.findByComponent(component); + // Version0: check acceptable transport + if (m_sessVersion == JGSession::Version0) { + // Don't accept another transport if we already have one + // Check for valid attributes + if (rtp || rtpRecv->m_protocol != "udp" || rtpRecv->m_type != "local") + return false; + } if (!rtp) { DDebug(this,DebugAll,"Adding remote transport '%s' in content '%s' [%p]", rtpRecv->toString().c_str(),local.toString().c_str(),this); @@ -2496,8 +2551,12 @@ void YJGConnection::addContent(bool local, JGSessionContent* c) c->m_rtpRemoteCandidates.m_type = c->m_rtpLocalCandidates.m_type; else c->m_rtpLocalCandidates.m_type = c->m_rtpRemoteCandidates.m_type; - if (c->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpIceUdp) - c->m_rtpLocalCandidates.generateIceAuth(); + if (c->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpIceUdp) { + if (m_sessVersion != JGSession::Version0) + c->m_rtpLocalCandidates.generateIceAuth(); + else + c->m_rtpLocalCandidates.generateOldIceAuth(); + } // Fill synonym for received media if (!local) { for (ObjList* o = c->m_rtpMedia.skipNull(); o; o = o->skipNext()) { @@ -2610,7 +2669,6 @@ bool YJGConnection::resetCurrentAudioContent(bool session, bool earlyMedia, } else if (!newContent->isValidAudio()) return false; - if (newContent && newContent->ref()) { m_audioContent = newContent; Debug(this,DebugAll,"Using audio content '%s' [%p]", @@ -2633,6 +2691,9 @@ bool YJGConnection::startRtp() return false; } + if (m_sessVersion == JGSession::Version0 && m_rtpStarted) + return true; + JGRtpCandidate* rtpLocal = m_audioContent->m_rtpLocalCandidates.findByComponent(1); JGRtpCandidate* rtpRemote = m_audioContent->m_rtpRemoteCandidates.findByComponent(1); if (!(rtpLocal && rtpRemote)) { @@ -2683,6 +2744,7 @@ bool YJGConnection::startRtp() if (m_audioContent->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpIceUdp && rtpRemote->m_address) { + m_rtpStarted = true; // Start STUN Message* msg = new Message("socket.stun"); msg->userData(m.userData()); @@ -2856,6 +2918,7 @@ bool YJGConnection::processContentAdd(const JGEvent& event, ObjList& ok, ObjList remove.append(c)->setDelete(false); continue; } + c->m_rtpMedia.m_ready = true; // Check crypto bool error = false; @@ -4228,6 +4291,7 @@ void YJGDriver::initialize() s_userRoster = sect->getBoolValue("user.roster",false); s_useCrypto = sect->getBoolValue("secure_rtp",false); s_cryptoMandatory = s_useCrypto; + s_sessVersion = JGSession::lookupVersion(sect->getValue("jingleversion"),JGSession::Version1); // Init codecs in use. Check each codec in known codecs list against the configuration s_usedCodecs.clear(); @@ -4261,6 +4325,7 @@ void YJGDriver::initialize() if (debugAt(dbg)) { String s; s << " localip=" << s_localAddress ? s_localAddress.c_str() : "MISSING"; + s << " jingleversion=" << JGSession::lookupVersion(s_sessVersion); s << " singletone=" << String::boolText(m_singleTone); s << " pending_timeout=" << s_pendingTimeout; s << " anonymous_caller=" << s_anonymousCaller;