yate/modules/yjinglechan.cpp

4743 lines
154 KiB
C++

/**
* yjinglechan.cpp
* This file is part of the YATE Project http://YATE.null.ro
*
* Jingle channel
*
* Yet Another Telephony Engine - a fully featured software PBX and IVR
* Copyright (C) 2004-2014 Null Team
* Author: Marian Podgoreanu
*
* This software is distributed under multiple licenses;
* see the COPYING file in the main directory for licensing
* information for this specific distribution.
*
* This use of this software may be subject to additional restrictions.
* See the LEGAL file in the main directory for details.
*
* 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.
*/
/*
============================================================================
TODO:
Check SRTP handling. Check if secure (mandatory) is handled properly
============================================================================
*/
#include <yatephone.h>
#include <yatemime.h>
#include <yateversn.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <yatejingle.h>
using namespace TelEngine;
namespace { // anonymous
class YJGEngine; // Jingle engine
class YJGEngineWorker; // Jingle engine worker
class YJGConnection; // Jingle channel
class YJGTransfer; // Transfer thread (route and execute)
class YJGMessageHandler; // Module message handlers
class YJGDriver; // The driver
// URI
#define BUILD_XMPP_URI(jid) (plugin.name() + ":" + jid)
/*
* YJGEngine
*/
class YJGEngine : public JGEngine
{
public:
// Send a session's stanza (dispatch a jabber.iq message)
virtual bool sendStanza(JGSession* session, XmlElement*& stanza);
// Event processor
virtual void processEvent(JGEvent* event);
};
/*
* YJGEngineWorker
*/
class YJGEngineWorker : public Thread
{
public:
inline YJGEngineWorker(Thread::Priority prio = Thread::Normal)
: Thread("YJGEngineWorker",prio)
{}
virtual void run();
};
/*
* YJGConnection
*/
class YJGConnection : public Channel
{
YCLASS(YJGConnection,Channel)
friend class YJGTransfer;
public:
enum State {
Pending,
Active,
Terminated,
};
// Flags controlling the state of the data source/consumer
enum DataFlags {
OnHoldRemote = 0x0001, // Put on hold by remote party
OnHoldLocal = 0x0002, // Put on hold by peer
OnHold = OnHoldRemote | OnHoldLocal,
};
// File transfer status
enum FileTransferStatus {
FTNone, // No file transfer allowed
FTIdle, // Nothing done yet
FTWaitEstablish, // Waiting for SOCKS to be negotiated
FTEstablished, // Transport succesfully setup
FTRunning, // Running
FTTerminated // Terminated
};
// File transfer host sender
enum FileTransferHostSender {
FTHostNone = 0,
FTHostLocal,
FTHostRemote,
};
// Ringing flags
enum RingFlags {
// Internal
RingRinging = 0x01, // call.ringing was handled
RingGotEarlyMedia = 0x02, // Gor early media from peer
RingContentSent = 0x04, // Ring content sent
// Settable
RingNone = 0x04, // Don't send ringing
RingNoEarlySession = 0x10, // Don't use early session content
RingWithContent = 0x20, // Attach session audio content if possible
RingWithContentOnly = 0x40, // Send ringing only if we have a content to sent
};
// Outgoing constructor
YJGConnection(Message& msg, const char* caller, const char* called, bool available,
const NamedList& caps, const char* file, const char* localip);
// Incoming contructor
YJGConnection(JGEvent* event);
virtual ~YJGConnection();
inline State state() const
{ return m_state; }
inline const JabberID& local() const
{ return m_local; }
inline const JabberID& remote() const
{ return m_remote; }
inline const String& reason() const
{ return m_reason; }
// 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;
}
// Check ring flag
inline bool ringFlag(int mask) const
{ return 0 != (m_ringFlags & mask); }
// Overloaded methods from Channel
virtual void callAccept(Message& msg);
virtual void callRejected(const char* error, const char* reason, const Message* msg);
virtual bool callRouted(Message& msg);
virtual void disconnected(bool final, const char* reason);
virtual bool msgProgress(Message& msg);
virtual bool msgRinging(Message& msg);
virtual bool msgAnswered(Message& msg);
virtual bool msgUpdate(Message& msg);
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,parameters());
}
// Route an incoming call
bool route();
// Process Jingle and Terminated events
// Return false to terminate
bool handleEvent(JGEvent* event);
void hangup(const char* reason = 0, const char* text = 0,
JGSession::Reason send = JGSession::ReasonUnknown);
// Process remote user's presence changes.
// Make the call if outgoing and in Pending (waiting for presence information) state
// Hangup if the remote user is unavailbale
// Return true to disconnect
bool presenceChanged(bool available, NamedList* params = 0);
// 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);
// Process chan.notify messages
// Handle SOCKS status changes for file transfer
bool processChanNotify(Message& msg);
// Check if a transfer can be initiated
inline bool canTransfer() const
{ return m_session && !m_transferring && isAnswered() && m_ftStatus == FTNone; }
inline void updateResource(const String& resource) {
if (!m_remote.resource() && resource)
m_remote.resource(resource);
}
inline void setReason(const char* reason) {
if (!m_reason)
m_reason = reason;
}
// Check the status of the given data flag(s)
inline bool dataFlags(int mask)
{ return 0 != (m_dataFlags & mask); }
// Ring flags names
static const TokenDict s_ringFlgName[];
// Retrieve ringing flags from string
// defVal: default value if flags list is empty
static int getRinging(const String& flags, DebugEnabler* enabler, int defVal = 0);
static inline int getRinging(NamedList& params, DebugEnabler* enabler, int defVal = 0)
{ return getRinging(params[YSTRING("jingle_ring")],enabler,defVal); }
protected:
// Process an ActContentAdd event
void processActionContentAdd(JGEvent* event);
// Process an ActContentAdd event
void processActionTransportInfo(JGEvent* event);
// Handle answer (session accept) events for non file transfer
void processActionAccept(JGEvent* ev);
// Handle stream hosts events
// Return false if the session was terminated
bool processStreamHosts(JGEvent* ev);
// Update a received candidate. Return true if changed
bool updateCandidate(unsigned int component, JGSessionContent& local,
JGSessionContent& recv);
// Add a new content to the list
void addContent(bool local, JGSessionContent* c);
// Remove a content from list
void removeContent(JGSessionContent* c);
// Reset the current audio content
// If the content is not re-usable (SRTP with local address),
// add a new identical content and remove the old old one from the session
// Clear the endpoint
void removeCurrentAudioContent(bool removeReq = false);
// This method is used to set the current audio content
// Clear the endpoint if the current content is replaced
// Reset the current content. Try to use the given content
// Else, find the first available content and try to use it
// Send a transport info for the new current content
// Send ringing if requested
// Return false on error
bool resetCurrentAudioContent(bool session, bool earlyMedia,
bool sendTransInfo = true, JGSessionContent* newContent = 0, bool sendRing = true);
// Start RTP for the current content
// For raw udp transports, sends a 'trying' session info
bool startRtp();
// Check a received candidate's parameters
// Return false if some parameter's value is incorrect
bool checkRecvCandidate(JGSessionContent& content, JGRtpCandidate& candidate);
// Check a received content(s). Fill received lists with accepted/rejected content(s)
// The lists don't own their pointers
// Return false on error
bool processContentAdd(const JGEvent& event, ObjList& ok, ObjList& remove);
// Remove contents. Confirm the received event
// Return false if there are no more contents
bool removeContents(JGEvent* event);
// Build a RTP audio content. Add used codecs to the list
// Build and init the candidate(s) if the content is a raw udp one
JGSessionContent* buildAudioContent(JGRtpCandidates::Type type,
JGSessionContent::Senders senders = JGSessionContent::SendBoth,
bool rtcp = false, bool useFormats = true);
// Build a file transfer content
JGSessionContent* buildFileTransferContent(bool send, const char* filename,
NamedList& params);
// Reserve local port for a RTP session content
bool initLocalCandidates(JGSessionContent& content, bool sendTransInfo);
// Match a local content agaist a received one
// Return false if there is no common media
bool matchMedia(JGSessionContent& local, JGSessionContent& recv,
bool& firstChanged, bool& telEvChanged) const;
// Find a session content in a list
JGSessionContent* findContent(JGSessionContent& recv, const ObjList& list) const;
// Set early media to remote
void setEarlyMediaOut(Message& msg);
// Enqueue a call.progress message from the current audio content
// Used for early media
void enqueueCallProgress();
// Init/start file transfer. Try to change host direction on failure
// If host dir succeeds, still return false, but don't terminate transfer
bool setupSocksFileTransfer(bool start);
// Change host sender. Return false on failure
bool changeFTHostDir(bool resetState = true);
// Drop file transfer data. Remove the first host in list
void dropFT(bool removeFirst);
// Drop file transfer hosts
void dropFTHosts(bool local, const char* reason = 0);
// Drop file transfer host
void dropFTHost(JGStreamHost* sh, ObjList* remove, const char* reason = 0);
// Get the RTP direction param from a content
// FIXME: ignore content senders for early media ?
inline const char* rtpDir(const JGSessionContent& c) {
if (c.senders() == JGSessionContent::SendInitiator)
return isOutgoing() ? "send" : "receive";
if (c.senders() == JGSessionContent::SendResponder)
return isOutgoing() ? "receive" : "send";
return "bidir";
}
// Build a RTP candidate
JGRtpCandidate* buildCandidate(bool nonP2P = true, bool rtp = true);
// Get the first file transfer content
inline JGSessionContent* firstFTContent() {
ObjList* o = m_ftContents.skipNull();
return o ? static_cast<JGSessionContent*>(o->get()) : 0;
}
private:
// Handle hold/active/mute actions
// Confirm the received element
void handleAudioInfoEvent(JGEvent* event);
// Check jingle version override from call.execute or resource caps
void overrideJingleVersion(const NamedList& list, bool caps);
// Override session flags
void overrideJingleFlags(const NamedList& list, const char* param);
// Copy chan/session parameters to a destination list
void copySessionParams(NamedList& list, bool redirect = true);
// Check media for a received content
bool checkMedia(const JGEvent& event, JGSessionContent& c);
// Clear and reset data related to a given type: audio ...
void resetEp(const String& what, bool releaseContent = true);
// Hangup and drop the call if failed to setup encryption
void dropNoCrypto();
// Send ringing
void sendRinging(NamedList* params = 0);
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
bool m_acceptRelay; // Accept to replace with a relay candidate
JGSession::Version m_sessVersion; // Jingle session version
int m_sessFlags; // Session flags
int m_ringFlags; // Ring flags
JabberID m_local; // Local user's JID
JabberID m_remote; // Remote user's JID
ObjList m_audioContents; // The list of negotiated audio contents
JGSessionContent* m_audioContent; // The current audio content
JGRtpMediaList m_audioFormats; // Audio formats used by this channel
String m_callerPrompt; // Text to be sent to called before calling it
String m_subject; // Connection subject
String m_line; // Connection line
String m_localip; // Local address
bool m_offerRawTransport; // Offer RAW transport on outgoing session
bool m_offerIceTransport; // Offer ICE transport on outgoing session
bool m_offerP2PTransport; // Offer P2P transport on outgoing session
bool m_offerGRawTransport; // Offer Google raw transport on outgoing session
unsigned int m_redirectCount; // Redirect counter
int m_dtmfMeth; // Used DMTF method
String m_rtpId; // Started RTP id
// Crypto (for contents created by us)
bool m_secure; // The channel is using crypto
bool m_secureRequired; // Crypto is mandatory
// Termination
bool m_hangup; // Hang up flag: True - already hung up
String m_reason; // Hangup reason
// Timeouts
int64_t m_presTimeout; // Maxcall after waiting for presence
// 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
XmlElement* m_recvTransferStanza; // Received iq transfer element
// 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
// File transfer
FileTransferStatus m_ftStatus; // File transfer status
int m_ftHostDirection; // Which endpoint can send file transfer hosts
String m_ftNotifier; // The notifier expected in chan.notify
String m_ftStanzaId;
String m_dstAddrDomain; // SHA1(SID + local + remote) used by SOCKS
ObjList m_ftContents; // The list of negotiated file transfer contents
ObjList m_streamHosts; // The list of negotiated SOCKS stream hosts
bool m_connSocksServer; // Try to build a socks listener if not configured
};
/*
* Transfer thread (route and execute)
*/
class YJGTransfer : public Thread
{
public:
YJGTransfer(YJGConnection* conn, const char* subject = 0);
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;
};
/*
* Module message handlers
*/
class YJGMessageHandler : public MessageHandler
{
public:
enum {
JabberIq = 50, // handleJabberIq()
ChanNotify = -2, // handleChanNotify()
EngineStart = -3, // handleEngineStart()
ResNotify = -4, // handleResNotify()
ResSubscribe = 10, // handleResSubscribe()
UserNotify = -5, // handleUserNotify()
};
YJGMessageHandler(int handler, int prio);
protected:
virtual bool received(Message& msg);
private:
int m_handler;
};
/*
* YJGDriver
*/
class YJGDriver : public Driver
{
public:
// Dtmf type
enum DtmfType {
DtmfUnknown = 0,
DtmfRfc2833, // Send RFC 2833 tones
DtmfInband, // Send inband tones
DtmfJingle, // Use the jingle protocol
DtmfChat // Send chat
};
YJGDriver();
virtual ~YJGDriver();
// Check if a message was sent by us
inline bool isModule(Message& msg) {
String* module = msg.getParam("module");
return module && *module == name();
}
// Build a message to be sent by us
inline Message* message(const char* msg) const {
Message* m = new Message(msg);
m->addParam("module",name());
return m;
}
// Add local ip to a list of parameters
inline bool addLocalIp(NamedList& list) {
Lock lock(this);
if (!m_localAddress)
return false;
list.addParam("localip",m_localAddress);
return true;
}
// Set local ip from a list of parameter or configured address
inline void setLocalIp(String& addr, NamedList& list) {
Lock lock(this);
addr = list.getValue("localip",m_localAddress);
}
// Check if a domain is handled by the module
inline bool handleDomain(const String& domain) {
Lock lock(this);
return m_domains.find(domain) != 0;
}
// Retrieve the default resource
inline void defaultResource(String& buf) {
Lock lock(this);
ObjList* o = m_resources.skipNull();
if (o)
buf = static_cast<String*>(o->get());
}
// Check if a resource can be handled by the module
inline bool handleResource(const String& name) {
if (m_handleAllRes)
return true;
Lock lock(this);
return !m_resources.skipNull() || m_resources.find(name);
}
// Inherited methods
virtual void initialize();
virtual bool hasLine(const String& line) const;
virtual bool msgExecute(Message& msg, String& dest);
// Message handler: Disconnect channels, destroy streams, clear rosters
virtual bool received(Message& msg, int id);
// Handle jabber.iq messages
bool handleJabberIq(Message& msg);
// Handle resource.notify messages
bool handleResNotify(Message& msg);
// Handle resource.subscribe messages
bool handleResSubscribe(Message& msg);
// Handle user.notify messages
bool handleUserNotify(Message& msg);
// Handle chan.notify messages
bool handleChanNotify(Message& msg);
// Handle msg.execute messages. Send chan.text if enabled
bool handleImExecute(Message& msg);
// Handle engine.start message
void handleEngineStart(Message& msg);
// Search a client's roster to get a resource
// (with audio capabilities) for a subscribed user.
// Set noSub to true if false is returned and the client
// 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 id. Return a referenced pointer
inline YJGConnection* findChan(const String& id) {
Lock lock(this);
YJGConnection* ch = static_cast<YJGConnection*>(find(id));
return (ch && ch->ref()) ? ch : 0;
}
// Find a connection by local and remote jid, optionally ignore local
// resource (always ignore if local has no resource)
YJGConnection* findByJid(const JabberID& local, const JabberID& remote,
bool anyResource = false);
// Find a channel by its sid
YJGConnection* findBySid(const String& sid);
// Get a copy of the default file transfer proxy
inline JGStreamHost* defFTProxy() {
Lock lock(this);
return m_ftProxy ? new JGStreamHost(*m_ftProxy) : 0;
}
// Notify presence
void notifyPresence(const JabberID& from, const char* to, bool online);
// Build and dispatch a 'jabber.account' message. Returns it on success
Message* checkAccount(const String& line, bool query = false,
const JabberID* contact = 0) const;
private:
// Update the list of domains
void setDomains(const String& list);
bool m_init;
String m_localAddress; // The local machine's address
String m_anonymousCaller; // Caller username when missing
JGStreamHost* m_ftProxy; // Default file transfer proxy
ObjList m_handlers; // Message handlers list
ObjList m_domains; // Domains handled by the module
bool m_handleAllRes; // Handle all resources (ignore the list)
ObjList m_resources; // Resources handled by the module
XMPPFeatureList m_features; // Domain or resource features to advertise
XmlElement* m_entityCaps; // ntity capabilities element built from features
};
/*
* Local data
*/
static Configuration s_cfg; // The configuration file
static JGRtpMediaList s_knownCodecs(JGRtpMediaList::Audio); // List of all known codecs
static JGRtpMediaList s_usedCodecs(JGRtpMediaList::Audio); // List of used audio codecs
static unsigned int s_pendingTimeout = 10000; // Outgoing call pending timeout
static bool s_requestSubscribe = true; // Request subscribe before making a non client
// call with target without resource
static bool s_autoSubscribe = false; // Automatically respond to (un)subscribe requests
static bool s_imToChanText = false; // Send received IM messages as chan.text if a channel is found
static bool s_singleTone = true; // Send single/batch DTMFs
static bool s_useCrypto = false; // Offer crypto on outgoing calls
static bool s_cryptoMandatory = false; // Offer mandatory crypto on outgoing calls
static bool s_acceptRelay = false;
static bool s_offerRawTransport = true; // Offer RAW UDP transport on outgoing sessions
static bool s_offerIceTransport = true; // Offer ICE UDP transport on outgoing sessions
static bool s_offerP2PTransport = false; // Offer P2P UDP transport on outgoing sessions
static bool s_offerGRawTransport = false; // Offer Google RAW UDP transport on outgoing sessions
static int s_priority = 0; // Resource priority for presence generated by this module
static unsigned int s_redirectCount = 0; // Redirect counter
static int s_dtmfMeth = YJGDriver::DtmfJingle; // Default DTMF method to use
static bool s_clearFilePath = false; // Clear file path when sending a file transfer
static JGSession::Version s_sessVersion = JGSession::VersionUnknown; // Default jingle session version for outgoing calls
static int s_ringFlags = 0; // Default channel ring flags
static String s_capsNode = "http://yate.null.ro/yate/jingle/caps"; // node for entity capabilities
static bool s_serverMode = true; // Server/client mode
static YJGEngine* s_jingle = 0;
static YJGDriver plugin; // The driver
static bool s_ilbcDefault30 = true; // Default ilbc format when ptime is unknown (30 or 20)
// Channel ring flags
const TokenDict YJGConnection::s_ringFlgName[] = {
{"none", RingNone},
{"noearlysession", RingNoEarlySession},
{"sessioncontent", RingWithContent},
{"sessioncontentonly", RingWithContentOnly},
{0,0}
};
// Message handlers installed by the module
static const TokenDict s_msgHandler[] = {
{"jabber.iq", YJGMessageHandler::JabberIq},
{"chan.notify", YJGMessageHandler::ChanNotify},
{"engine.start", YJGMessageHandler::EngineStart},
{"resource.notify", YJGMessageHandler::ResNotify},
{"resource.subscribe", YJGMessageHandler::ResSubscribe},
{"user.notify", YJGMessageHandler::UserNotify},
{0,0}
};
// Error mapping
static TokenDict s_errMap[] = {
{"normal", JGSession::ReasonOk},
{"normal-clearing", JGSession::ReasonOk},
{"hangup", JGSession::ReasonOk},
{"busy", JGSession::ReasonBusy},
{"rejected", JGSession::ReasonDecline},
{"nomedia", JGSession::ReasonMedia},
{"cancelled", JGSession::ReasonCancel},
{"failure", JGSession::ReasonGeneral},
{"noroute", JGSession::ReasonDecline},
{"noconn", JGSession::ReasonDecline},
{"noauth", JGSession::ReasonGeneral},
{"nocall", JGSession::ReasonGeneral},
{"noanswer", JGSession::ReasonGeneral},
{"forbidden", JGSession::ReasonGeneral},
{"congestion", JGSession::ReasonGeneral},
{"looping", JGSession::ReasonGeneral},
{"shutdown", JGSession::ReasonGone},
{"notransport", JGSession::ReasonTransport},
{"offline", JGSession::ReasonGone},
{"gone", JGSession::ReasonGone},
{"shutdown", JGSession::ReasonGone},
{"timeout", JGSession::ReasonExpired},
{"timeout", JGSession::ReasonTimeout},
// Remote termination only
{"failure", JGSession::ReasonConn},
{"failure", JGSession::ReasonTransport},
{"failure", JGSession::ReasonApp},
{"failure", JGSession::ReasonAltSess},
{"failure", JGSession::ReasonConn},
{"failure", JGSession::ReasonFailApp},
{"failure", JGSession::ReasonFailTransport},
{"failure", JGSession::ReasonParams},
{"failure", JGSession::ReasonSecurity},
// Non jingle reasons
{"transferred", JGSession::Transferred},
{"crypto-required", JGSession::CryptoRequired},
{"invalid-crypto", JGSession::InvalidCrypto},
{0,0}
};
// Error mapping
static const TokenDict s_dictDtmfMeth[] = {
{"rfc2833", YJGDriver::DtmfRfc2833},
{"inband", YJGDriver::DtmfInband},
{"jingle", YJGDriver::DtmfJingle},
{"chat", YJGDriver::DtmfChat},
{0,0}
};
// Check if a payload name is telephone event one
static inline bool isTelEvent(const String& name)
{
return (name &= "telephone-event") || (name &= "tone") ||
(name &= "audio/telephone-event");
};
// Add a parameter to a list.
// Optionally add it to a copy params string
static inline void jingleAddParam(NamedList& list, const char* param, const char* value,
String* copy, bool emptyOk = true)
{
if (TelEngine::null(param))
return;
list.addParam(param,value,emptyOk);
if (copy)
copy->append(param,",");
}
// Add secure parameters from crypto
static void addSecure(NamedList& list, JGCrypto* crypto)
{
if (!crypto)
return;
list.addParam("secure",String::boolText(true));
list.addParam("crypto_suite",crypto->m_suite);
list.addParam("crypto_key",crypto->m_keyParams);
// TODO: add session params
}
// Replace 'ilbc' to used ilbc20/30
static void adjustUsedIlbc(String& fmts)
{
if (!fmts)
return;
ObjList* list = fmts.split(',',false);
ObjList* o = list->find("ilbc");
if (o) {
JGRtpMedia* m = 0;
plugin.lock();
for (ObjList* l = s_usedCodecs.skipNull(); l; l = l->skipNext()) {
m = static_cast<JGRtpMedia*>(l->get());
if (m->m_name == "iLBC")
break;
m = 0;
}
if (m)
*(static_cast<String*>(o->get())) = m->m_synonym;
else
o->remove();
plugin.unlock();
fmts.clear();
fmts.append(list,",");
}
TelEngine::destruct(list);
}
#ifdef DEBUG
// Utility function needed for debug: dump a candidate to a string
static void dumpCandidate(String& buf, JGRtpCandidate* c, char sep = ' ')
{
if (!c)
return;
buf << "name=" << *c;
buf << sep << "addr=" << c->m_address;
buf << sep << "port=" << c->m_port;
buf << sep << "component=" << c->m_component;
buf << sep << "generation=" << c->m_generation;
buf << sep << "network=" << c->m_network;
buf << sep << "priority=" << c->m_priority;
buf << sep << "protocol=" << c->m_protocol;
buf << sep << "type=" << c->m_type;
JGRtpCandidateP2P* p2p = YOBJECT(JGRtpCandidateP2P,c);
if (p2p) {
buf << sep << "username=" << p2p->m_username;
buf << sep << "password=" << p2p->m_password;
}
}
#endif
/*
* YJGEngine
*/
// Send a session's stanza (dispatch a jabber.iq message)
bool YJGEngine::sendStanza(JGSession* session, XmlElement*& stanza)
{
if (!(session && stanza)) {
TelEngine::destruct(stanza);
return false;
}
bool iq = stanza->toString() == XMPPUtils::s_tag[XmlTag::Iq];
if (!(iq || stanza->toString() == XMPPUtils::s_tag[XmlTag::Message])) {
TelEngine::destruct(stanza);
return false;
}
DDebug(this,DebugAll,"sendStanza() session=(%p,%s) stanza=(%p,%s)",
session,session->sid().c_str(),stanza,stanza->tag());
Message m(iq ? "jabber.iq" : "msg.execute");
m.addParam("module",plugin.name());
if (session->line())
m.addParam("line",session->line());
if (iq) {
m.addParam("from",session->local().bare());
m.addParam("to",session->remote().bare());
m.addParam("from_instance",session->local().resource());
m.addParam("to_instance",session->remote().resource());
}
else {
m.addParam("caller",session->local().bare());
m.addParam("called",session->remote().bare());
m.addParam("caller_instance",session->local().resource());
m.addParam("called_instance",session->remote().resource());
}
m.addParam(new NamedPointer("xml",stanza));
return Engine::dispatch(m);
}
// Process jingle events
void YJGEngine::processEvent(JGEvent* event)
{
if (!event)
return;
JGSession* session = event->session();
// This should never happen !!!
if (!session) {
DDebug(this,DebugStub,"Received event without session");
delete event;
return;
}
plugin.lock();
YJGConnection* conn = static_cast<YJGConnection*>(session->userData());
if (conn && !conn->ref()) {
plugin.unlock();
delete event;
return;
}
plugin.unlock();
if (conn) {
if (!conn->handleEvent(event) || event->final())
conn->disconnect(event->reason());
TelEngine::destruct(conn);
}
else {
if (event->type() == JGEvent::Jingle &&
event->action() == JGSession::ActInitiate) {
bool ok = plugin.canAccept(true);
if (ok && event->session()->ref()) {
conn = new YJGConnection(event);
conn->initChan();
// Constructor failed ?
if (conn->state() == YJGConnection::Pending)
TelEngine::destruct(conn);
else if (!conn->route()) {
Lock lck(plugin);
event->session()->userData(0);
}
}
else if (!ok) {
Debug(&plugin,DebugWarn,"Refusing new Jingle call, full or exiting");
event->session()->hangup(event->session()->createReason(JGSession::ReasonGeneral));
}
else {
Debug(this,DebugWarn,"Session ref failed for new connection");
event->session()->hangup(event->session()->createReason(JGSession::ReasonGeneral));
}
}
else {
DDebug(this,DebugAll,"Invalid (non initiate) event for new session");
event->confirmElement(XMPPError::Request,"Unknown session");
}
}
delete event;
}
/*
* YJGEngineWorker
*/
void YJGEngineWorker::run()
{
Debug(&plugin,DebugAll,"%s start running",currentName());
while (true) {
if (Thread::check(false) || Engine::exiting())
break;
JGEvent* ev = s_jingle->getEvent(Time::msecNow());
if (ev)
s_jingle->processEvent(ev);
else
Thread::idle(false);
}
Debug(&plugin,DebugAll,"%s stop running",currentName());
}
/*
* YJGConnection
*/
// Outgoing call
YJGConnection::YJGConnection(Message& msg, const char* caller, const char* called,
bool available, const NamedList& caps, const char* file, const char* localip)
: Channel(&plugin,0,true),
m_mutex(true,"YJGConnection"),
m_state(Pending), m_session(0), m_rtpStarted(false), m_acceptRelay(s_acceptRelay),
m_sessVersion(s_sessVersion), m_sessFlags(s_jingle->sessionFlags()),
m_ringFlags(s_ringFlags),
m_local(caller), m_remote(called), m_audioContent(0),
m_audioFormats(JGRtpMediaList::Audio),
m_callerPrompt(msg.getValue("callerprompt")),
m_localip(localip),
m_offerRawTransport(true), m_offerIceTransport(true),
m_offerP2PTransport(false), m_offerGRawTransport(false),
m_redirectCount(s_redirectCount), m_dtmfMeth(s_dtmfMeth),
m_secure(s_useCrypto), m_secureRequired(s_cryptoMandatory),
m_hangup(false), m_presTimeout(-1), m_transferring(false), m_recvTransferStanza(0),
m_dataFlags(0), m_ftStatus(FTNone), m_ftHostDirection(FTHostNone),
m_connSocksServer(msg.getBoolValue("socksserver",true))
{
int redir = msg.getIntValue("redirectcount",m_redirectCount);
m_redirectCount = (redir >= 0) ? redir : 0;
m_dtmfMeth = msg.getIntValue("dtmfmethod",s_dictDtmfMeth,s_dtmfMeth);
m_secure = msg.getBoolValue("secure",m_secure);
m_secureRequired = msg.getBoolValue("secure_required",m_secureRequired);
overrideJingleVersion(msg,false);
if (available)
overrideJingleVersion(caps,true);
overrideJingleFlags(msg,"ojingle_flags");
if (m_sessVersion != JGSession::Version0) {
m_offerRawTransport = msg.getBoolValue("offerrawudp",s_offerRawTransport);
m_offerIceTransport = msg.getBoolValue("offericeudp",s_offerIceTransport);
m_offerP2PTransport = msg.getBoolValue("offerp2p",s_offerP2PTransport);
m_offerGRawTransport = msg.getBoolValue("offergraw",s_offerGRawTransport);
}
else
m_offerRawTransport = false;
m_subject = msg.getValue("subject");
m_line = msg.getValue("line");
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);
}
// Get formats. Check if this is a file transfer session
if (null(file)) {
String audio = msg["formats"];
plugin.lock();
if (audio)
adjustUsedIlbc(audio);
else if (!s_usedCodecs.createList(audio,true))
audio = "alaw,mulaw";
m_audioFormats.setMedia(s_usedCodecs,audio);
plugin.unlock();
}
else {
m_secure = false;
m_ftStatus = FTIdle;
m_ftHostDirection = FTHostLocal;
NamedString* oper = msg.getParam("operation");
bool send = (oper && *oper == "send");
m_ftContents.append(buildFileTransferContent(send,file,msg));
// Add default proxy stream host if we have one
JGStreamHost* sh = plugin.defFTProxy();
if (sh)
m_streamHosts.append(sh);
}
Debug(this,DebugCall,"Outgoing%s. caller='%s' called='%s'%s%s [%p]",
m_ftStatus != FTNone ? " file transfer" : "",caller,called,
m_transferFrom ? ". Transferred from=": "",
m_transferFrom.safe(),this);
// Set timeout and maxcall
setMaxcall(msg);
setMaxPDD(msg);
setChanParams(msg);
if (!available) {
u_int64_t timeNow = Time::now();
// Save maxcall for later, set presence retrieval timeout instead
m_presTimeout = maxcall() ? maxcall() - timeNow : 0;
if (s_pendingTimeout)
maxcall(s_pendingTimeout * (u_int64_t)1000 + timeNow);
}
XDebug(this,DebugInfo,"Time: " FMT64 ". Maxcall set to " FMT64 " us. [%p]",
Time::now(),maxcall(),this);
// Startup
Message* m = message("chan.startup",msg);
m->setParam("direction",status());
m_targetid = msg.getValue("id");
m->copyParams(msg,"caller,callername,called,billid,callto,username");
Engine::enqueue(m);
// Make the call
if (available)
presenceChanged(true);
}
// Incoming call
YJGConnection::YJGConnection(JGEvent* event)
: Channel(&plugin,0,false),
m_mutex(true,"YJGConnection"),
m_state(Active), m_session(event->session()), m_rtpStarted(false), m_acceptRelay(s_acceptRelay),
m_sessVersion(event->session()->version()), m_sessFlags(s_jingle->sessionFlags()),
m_ringFlags(s_ringFlags),
m_local(event->session()->local()), m_remote(event->session()->remote()),
m_audioContent(0),
m_audioFormats(JGRtpMediaList::Audio),
m_offerRawTransport(true), m_offerIceTransport(true),
m_offerP2PTransport(false), m_offerGRawTransport(false),
m_redirectCount(0), m_dtmfMeth(s_dtmfMeth),
m_secure(s_useCrypto), m_secureRequired(s_cryptoMandatory),
m_hangup(false), m_presTimeout(-1), m_transferring(false), m_recvTransferStanza(0),
m_dataFlags(0), m_ftStatus(FTNone), m_ftHostDirection(FTHostNone),
m_connSocksServer(false)
{
m_line = m_session->line();
plugin.lock();
m_audioFormats.setMedia(s_usedCodecs);
plugin.unlock();
// Update local ip in non server mode
if (!s_serverMode && m_line) {
Message* m = plugin.checkAccount(m_line);
if (m) {
m_localip = m->getValue("localip");
TelEngine::destruct(m);
}
}
if (event->jingle()) {
// Check if this call is transferred
XmlElement* trans = XMPPUtils::findFirstChild(*event->jingle(),XmlTag::Transfer);
if (trans)
m_transferFrom = trans->getAttribute("from");
// Get subject
m_subject = XMPPUtils::subject(*event->jingle());
}
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);
if (m_sessVersion == JGSession::Version0)
m_offerRawTransport = false;
// Process incoming content(s)
ObjList ok;
ObjList remove;
bool haveAudioSession = false;
bool haveFTSession = false;
if (processContentAdd(*event,ok,remove)) {
for (ObjList* o = ok.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
switch (c->type()) {
case JGSessionContent::RtpIceUdp:
case JGSessionContent::RtpRawUdp:
case JGSessionContent::RtpP2P:
case JGSessionContent::RtpGoogleRawUdp:
haveAudioSession = haveAudioSession || c->isSession();
addContent(false,c);
break;
case JGSessionContent::FileBSBOffer:
case JGSessionContent::FileBSBRequest:
haveFTSession = haveFTSession || c->isSession();
m_ftContents.append(c);
break;
default:
// processContentAdd() should return only known content types in ok list
// This a safeguard if we add new content type(s) and forget to process them
Debug(this,DebugNote,
"Can't process incoming content '%s' of type %u [%p]",
c->toString().c_str(),c->type(),this);
// Append this content to 'remove' list
// Let the list own it since we'll remove it from event's list
remove.append(c);
}
event->m_contents.remove(c,false);
}
}
// XEP-0166 7.2.8 At least one content should have disposition=session
// Change state to Pending on failure to terminate the session
const char* error = 0;
if (m_audioContents.skipNull()) {
if (!haveAudioSession)
error = "No content with session disposition";
}
else if (m_ftContents.skipNull()) {
m_secure = false;
m_ftStatus = FTIdle;
m_ftHostDirection = FTHostRemote;
m_session->buildSocksDstAddr(m_dstAddrDomain);
if (haveFTSession) {
// TODO: Check data consistency: all file transfer contents should be
// identical (except for transport method, of course)
}
else
error = "No content with session disposition";
}
else
error = "No acceptable session content(s) in initiate event";
if (!error) {
event->confirmElement();
if (remove.skipNull())
m_session->sendContent(JGSession::ActContentRemove,remove);
// We don't support mixed sessions for now
// Remove file transfer contents if we have an audio session request
if (m_audioContents.skipNull() && m_ftContents.skipNull()) {
Debug(this,DebugNote,"Denying file transfer in audio session [%p]",this);
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<JGSessionContent*>(o->get()));
}
}
else {
m_state = Pending;
setReason("failure");
Debug(this,DebugNote,"%s [%p]",error,this);
event->confirmElement(XMPPError::BadRequest,error);
}
// Startup
Message* m = message("chan.startup");
m->setParam("direction",status());
m->setParam("caller",m_remote.bare());
m->setParam("called",m_local.node());
Engine::enqueue(m);
}
// Release data
YJGConnection::~YJGConnection()
{
TelEngine::destruct(m_recvTransferStanza);
hangup();
disconnected(true,m_reason);
Debug(this,DebugCall,"Destroyed [%p]",this);
}
// Route an incoming call
bool YJGConnection::route()
{
Message* m = message("call.preroute",false,true);
m->addParam("username",m_remote.node());
m->addParam("in_line",m_line,false);
m->addParam("called",m_local.node());
m->addParam("calleduri",BUILD_XMPP_URI(m_local));
m->addParam("caller",m_remote.node());
m->addParam("callername",m_remote.bare());
m->addParam("calleruri",BUILD_XMPP_URI(m_remote));
if (m_subject)
m->addParam("subject",m_subject);
m->addParam("jingle_version",JGSession::lookupVersion(m_sessVersion));
String flags;
JGEngine::encodeFlags(flags,m_sessFlags,JGSession::s_flagName);
m->addParam("jingle_flags",flags,false);
m_mutex.lock();
// TODO: add remote ip/port
// Fill file transfer data
JGSessionContent* c = firstFTContent();
if (c) {
m->addParam("format","data");
if (c->type() == JGSessionContent::FileBSBOffer)
m->addParam("operation","receive");
else if (c->type() == JGSessionContent::FileBSBRequest)
m->addParam("operation","send");
m->addParam("file_name",c->m_fileTransfer.getValue("name"));
int sz = c->m_fileTransfer.getIntValue("size",-1);
if (sz >= 0)
m->addParam("file_size",String(sz));
const char* md5 = c->m_fileTransfer.getValue("hash");
if (!null(md5))
m->addParam("file_md5",md5);
String* date = c->m_fileTransfer.getParam("date");
if (!null(date)) {
unsigned int time = XMPPUtils::decodeDateTimeSec(*date);
if (time != (unsigned int)-1)
m->addParam("file_time",String(time));
}
}
else {
JGRtpMediaList* mList = 0;
if (m_audioContent)
mList = &m_audioContent->m_rtpMedia;
else {
ObjList* o = m_audioContents.skipNull();
if (o)
mList = &static_cast<JGSessionContent*>(o->get())->m_rtpMedia;
}
if (!mList)
mList = &m_audioFormats;
String formats;
mList->createList(formats,true);
m->addParam("formats",formats,false);
}
m_mutex.unlock();
return startRouter(m);
}
// Call accepted
// Init RTP. Accept session and transport. Send transport
void YJGConnection::callAccept(Message& msg)
{
Debug(this,DebugCall,"callAccept [%p]",this);
m_secure = msg.getBoolValue("secure",m_secure);
m_secureRequired = msg.getBoolValue("secure_required",m_secureRequired);
m_dtmfMeth = msg.getIntValue("dtmfmethod",s_dictDtmfMeth,m_dtmfMeth);
m_connSocksServer = msg.getBoolValue("isocksserver",true);
overrideJingleFlags(msg,"jingle_flags");
Channel::callAccept(msg);
Lock lock(m_mutex);
if (m_session)
m_session->setFlags(m_sessFlags);
}
void YJGConnection::callRejected(const char* error, const char* reason,
const Message* msg)
{
Debug(this,DebugCall,"callRejected. error=%s reason=%s [%p]",error,reason,this);
if (!reason)
reason = "rejected";
hangup(error,reason);
Channel::callRejected(error,reason,msg);
}
bool YJGConnection::callRouted(Message& msg)
{
DDebug(this,DebugCall,"callRouted [%p]",this);
// Update ringing
m_ringFlags = getRinging(msg,this,m_ringFlags);
// Update formats
const String& formats = msg[YSTRING("formats")];
if (formats) {
m_mutex.lock();
m_audioFormats.filterMedia(formats);
for (ObjList* o = m_audioContents.skipNull(); o; o = o->skipNext())
static_cast<JGSessionContent*>(o->get())->m_rtpMedia.filterMedia(formats);
m_mutex.unlock();
}
return Channel::callRouted(msg);
}
void YJGConnection::disconnected(bool final, const char* reason)
{
Debug(this,DebugCall,"disconnected. final=%u reason=%s [%p]",
final,reason,this);
TelEngine::destruct(m_audioContent);
setReason(reason);
Channel::disconnected(final,m_reason);
}
bool YJGConnection::msgProgress(Message& msg)
{
DDebug(this,DebugInfo,"msgProgress [%p]",this);
Channel::msgProgress(msg);
if (m_ftStatus != FTNone)
return true;
if (ringFlag(RingWithContent) && msg.getBoolValue("earlymedia",true) &&
getPeer() && getPeer()->getSource()) {
m_ringFlags |= RingRinging;
sendRinging(&msg);
}
setEarlyMediaOut(msg);
return true;
}
bool YJGConnection::msgRinging(Message& msg)
{
DDebug(this,DebugInfo,"msgRinging [%p]",this);
Channel::msgRinging(msg);
if (m_ftStatus != FTNone)
return true;
m_ringFlags |= RingRinging;
sendRinging(&msg);
setEarlyMediaOut(msg);
return true;
}
bool YJGConnection::msgAnswered(Message& msg)
{
Debug(this,DebugCall,"msgAnswered [%p]",this);
m_presTimeout = -1;
if (m_ftStatus == FTNone) {
m_mutex.lock();
if (!m_audioContent || ((m_sessVersion != JGSession::Version0) && m_audioContent->isEarlyMedia()))
resetCurrentAudioContent(true,false,true);
ObjList tmp;
if (m_audioContent)
tmp.append(m_audioContent)->setDelete(false);
else
Debug(this,DebugMild,"No session audio content available on answer time!!! [%p]",this);
if (m_session)
m_session->accept(tmp);
m_mutex.unlock();
return Channel::msgAnswered(msg);
}
// File transfer connection
Channel::msgAnswered(msg);
if (m_ftStatus == FTEstablished) {
if (setupSocksFileTransfer(true)) {
ObjList tmp;
JGSessionContent* c = firstFTContent();
if (c)
tmp.append(c)->setDelete(false);
m_session->accept(tmp);
}
else
hangup("failure");
}
return true;
}
bool YJGConnection::msgUpdate(Message& msg)
{
DDebug(this,DebugCall,"msgUpdate [%p]",this);
Channel::msgUpdate(msg);
if (m_ftStatus != FTNone)
return false;
NamedString* oper = msg.getParam("operation");
if (TelEngine::null(oper))
return false;
bool req = (*oper == "request");
bool notify = !req && (*oper == "notify");
bool ok = false;
#define SET_ERROR_BREAK(error,reason) { \
if (error) \
msg.setParam("error",error); \
if (reason) \
msg.setParam("reason",reason); \
break; \
}
Lock lock(m_mutex);
bool hold = msg.getBoolValue("hold");
bool active = msg.getBoolValue("active");
// Use a while to check session and break to method end
while (m_session) {
// Hold
if (hold) {
// TODO: check if remote peer supports JingleRtpInfo
if (notify) {
ok = true;
break;
}
if (!req)
break;
// Already put on hold
if (dataFlags(OnHold)) {
if (dataFlags(OnHoldLocal))
SET_ERROR_BREAK("pending",0);
SET_ERROR_BREAK("failure","Already on hold");
}
// Send XML. Copy any additional params
XmlElement* hold = XMPPUtils::createElement(XmlTag::Hold,
XMPPNamespace::JingleAppsRtpInfo);
unsigned int n = msg.length();
for (unsigned int i = 0; i < n; i++) {
NamedString* ns = msg.getParam(i);
if (!(ns && ns->name().startsWith("hold.") && ns->name().at(5)))
continue;
hold->setAttributeValid(ns->name().substr(5),*ns);
}
m_onHoldOutId << "hold" << Time::secNow();
if (!m_session->sendInfo(hold,&m_onHoldOutId)) {
m_onHoldOutId = "";
SET_ERROR_BREAK("noconn",0);
}
DDebug(this,DebugAll,"Sent hold request [%p]",this);
m_dataFlags |= OnHoldLocal;
removeCurrentAudioContent();
ok = true;
break;
}
// Active
if (active) {
// TODO: check if remote peer supports JingleRtpInfo
if (notify) {
ok = true;
break;
}
if (!req)
break;
// Not on hold
if (!dataFlags(OnHold))
SET_ERROR_BREAK("failure","Already active");
// Put on hold by remote
if (dataFlags(OnHoldRemote))
SET_ERROR_BREAK("failure","Already on hold by the other party");
// Send XML. Copy additional attributes
XmlElement* active = XMPPUtils::createElement(XmlTag::Active,
XMPPNamespace::JingleAppsRtpInfo);
unsigned int n = msg.length();
for (unsigned int i = 0; i < n; i++) {
NamedString* ns = msg.getParam(i);
if (!(ns && ns->name().startsWith("active.") && ns->name().at(5)))
continue;
active->setAttributeValid(ns->name().substr(5),*ns);
}
m_activeOutId << "active" << Time::secNow();
if (!m_session->sendInfo(active,&m_activeOutId)) {
m_activeOutId = "";
SET_ERROR_BREAK("noconn",0);
}
DDebug(this,DebugAll,"Sent active request [%p]",this);
ok = true;
break;
}
break;
}
if (!ok && req && (hold || active))
Debug(this,DebugNote,"Failed to send '%s' request error='%s' reason='%s' [%p]",
hold ? "hold" : "active",msg.getValue("error"),msg.getValue("reason"),this);
#undef SET_ERROR_BREAK
return ok;
}
// Send message to remote peer
bool YJGConnection::msgText(Message& msg, const char* text)
{
DDebug(this,DebugCall,"msgText. '%s' [%p]",text,this);
Lock lock(m_mutex);
if (m_session)
return s_jingle->sendMessage(m_session,text);
return false;
}
// Hangup
bool YJGConnection::msgDrop(Message& msg, const char* reason)
{
DDebug(this,DebugCall,"msgDrop('%s') [%p]",reason,this);
setReason(reason ? reason : "dropped");
if (!Channel::msgDrop(msg,m_reason))
return false;
hangup(m_reason);
return true;
}
// Send tones to remote peer
bool YJGConnection::msgTone(Message& msg, const char* tone)
{
DDebug(this,DebugCall,"msgTone. '%s' [%p]",tone,this);
if (!(tone && *tone))
return true;
int meth = msg.getIntValue("method",s_dictDtmfMeth,m_dtmfMeth);
Lock lock(m_mutex);
// Inband and RFC 2833 require an active local RTP stream
if (meth == YJGDriver::DtmfInband) {
if (m_rtpStarted && dtmfInband(tone))
return true;
}
else if (meth == YJGDriver::DtmfRfc2833) {
if (m_rtpStarted) {
msg.setParam("targetid",m_rtpId);
return false;
}
}
if (!m_session)
return false;
if (s_singleTone) {
char s[2] = {0,0};
while (*tone) {
s[0] = *tone++;
if (meth != YJGDriver::DtmfChat)
m_session->sendDtmf(s);
else
s_jingle->sendMessage(m_session,s);
}
}
else if (meth != YJGDriver::DtmfChat)
m_session->sendDtmf(tone);
else
s_jingle->sendMessage(m_session,tone);
return true;
}
// Send a transfer request
bool YJGConnection::msgTransfer(Message& msg)
{
Lock lock(m_mutex);
if (!canTransfer())
return false;
// Get transfer destination
m_transferTo.set(msg.getValue("to"));
// Check attended transfer request
NamedString* chanId = msg.getParam("channelid");
if (chanId) {
bool ok = false;
plugin.lock();
YJGConnection* conn = static_cast<YJGConnection*>(plugin.find(*chanId));
if (conn) {
ok = conn->getSid(m_transferSid);
if (!m_transferTo)
m_transferTo = conn->remote();
}
plugin.unlock();
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;
}
}
else if (!m_transferTo) {
DDebug(this,DebugNote,"Transfer request with empty target [%p]",this);
return false;
}
// Try to get a resource for transfer target if incomplete
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);
}
// Send the transfer request
XmlElement* trans = m_session->buildTransfer(m_transferTo,
m_transferSid ? m_session->local() : String::empty(),m_transferSid);
const char* subject = msg.getValue("subject");
if (!null(subject))
trans->addChild(XMPPUtils::createSubject(subject));
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, JGSession::Reason send)
{
Lock lock(m_mutex);
if (m_hangup)
return;
m_hangup = true;
m_state = Terminated;
m_ftStatus = FTTerminated;
setReason(reason ? reason : (Engine::exiting() ? "shutdown" : "hangup"));
if (!text && Engine::exiting())
text = "Shutdown";
if (m_transferring)
transferTerminated(false,m_reason);
Message* m = message("chan.hangup",true);
m->setParam("status","hangup");
m->setParam("reason",m_reason);
Engine::enqueue(m);
JGSession* sess = 0;
if (m_session) {
int res = send;
if (res == JGSession::ReasonUnknown)
res = lookup(m_reason,s_errMap,JGSession::ReasonUnknown);
XmlElement* xml = 0;
switch (res) {
case JGSession::CryptoRequired:
case JGSession::InvalidCrypto:
xml = m_session->createReason(JGSession::ReasonSecurity,text,
m_session->createRtpSessionReason(res));
break;
case JGSession::Transferred:
xml = m_session->createReason(JGSession::ReasonOk,text,
m_session->createTransferReason(res));
break;
case JGSession::ReasonUnknown:
break;
default:
xml = m_session->createReason(res,text);
}
m_session->hangup(xml);
sess = m_session;
m_session = 0;
}
Debug(this,DebugCall,"Hangup. reason=%s [%p]",m_reason.c_str(),this);
lock.drop();
if (sess) {
plugin.lock();
sess->userData(0);
plugin.unlock();
TelEngine::destruct(sess);
}
}
// Handle Jingle events
// Return false to terminate
bool YJGConnection::handleEvent(JGEvent* event)
{
if (!event)
return true;
Lock lock(m_mutex);
if (m_hangup) {
Debug(this,DebugInfo,"Ignoring event (%p,%u). Already hung up [%p]",
event,event->type(),this);
return false;
}
if (event->type() == JGEvent::Terminated) {
// Handle redirect
if (isOutgoing() && event->reason() == "redirect" && event->text()) {
bool validCounter = false;
if (m_redirectCount) {
m_redirectCount--;
validCounter = true;
}
// Handle here XMPP targets
// Let the pbx deal with other targets
if (validCounter && event->text().startsWith("xmpp:",false)) {
JabberID callto(event->text().substr(5));
if (callto.bare()) {
if (callto == m_remote) {
Debug(this,DebugNote,"Got redirect to the same remote party! [%p]",this);
callto.clear();
}
}
else {
Debug(this,DebugNote,"Got redirect to incomplete jid=%s [%p]",
event->text().c_str(),this);
callto.clear();
}
String id;
if (callto && getPeerId(id)) {
Message m("chan.masquerade");
m.addParam("message","call.execute");
m.addParam("id",id);
m.addParam("callto",plugin.prefix() + callto);
m.addParam("caller",m_local,false);
copySessionParams(m);
Debug(this,DebugCall,"Redirecting to '%s' [%p]",callto.c_str(),this);
lock.drop();
Engine::dispatch(m);
}
}
else {
URI uri(event->text());
paramMutex().lock();
parameters().clearParams();
parameters().addParam("called",uri.getUser());
parameters().addParam("calledname",uri.getDescription(),false);
parameters().addParam("calleduri",event->text());
parameters().addParam("copyparams","");
copySessionParams(parameters());
paramMutex().unlock();
}
}
const char* reason = event->reason();
Debug(this,DebugInfo,
"Session terminated with reason='%s' text='%s' [%p]",
reason,event->text().c_str(),this);
if (!TelEngine::null(reason)) {
int jingleReason = lookup(reason,JGSession::s_reasons,JGSession::ReasonGeneral);
setReason(lookup(jingleReason,s_errMap,reason));
}
return false;
}
bool response = false;
switch (event->type()) {
case JGEvent::Jingle:
break;
case JGEvent::ResultOk:
case JGEvent::ResultError:
case JGEvent::ResultTimeout:
response = true;
break;
default:
DDebug(this,DebugStub,"Unhandled event (%p,%u) [%p]",
event,event->type(),this);
return true;
}
// Process responses
if (response) {
XDebug(this,DebugAll,"Processing response event=%s id=%s [%p]",
event->name(),event->id().c_str(),this);
bool rspOk = (event->type() == JGEvent::ResultOk);
if (event->action() == JGSession::ActInitiate) {
if (m_ftStatus == FTNone) {
// Non file transfer session
// Notify ringing if initiate was confirmed and the remote party doesn't support it
if (rspOk && !m_session->hasFeature(XMPPNamespace::JingleAppsRtpInfo)) {
status("ringing");
Engine::enqueue(message("call.ringing",false,true));
}
}
else {
// File transfer session
// Send stream host
if (rspOk) {
bool ok = false;
if (m_session) {
m_session->buildSocksDstAddr(m_dstAddrDomain);
ok = setupSocksFileTransfer(false);
if (!ok) {
ok = (m_ftStatus != FTTerminated);
if (ok) {
// Send empty host
m_streamHosts.clear();
m_session->sendStreamHosts(m_streamHosts,&m_ftStanzaId);
}
}
}
if (!ok)
hangup("noconn");
return ok;
}
}
}
if (m_ftStanzaId && m_ftStanzaId == event->id()) {
m_ftStanzaId = "";
String usedHost;
bool ok = rspOk;
if (rspOk && event->element()) {
XmlElement* query = XMPPUtils::findFirstChild(*event->element(),XmlTag::Query);
if (query) {
XmlElement* used = XMPPUtils::findFirstChild(*query,XmlTag::StreamHostUsed);
if (used)
usedHost = used->getAttribute("jid");
}
}
if (!ok) {
dropFT(true);
// Result error: continue if we still can receive hosts
if (event->type() == JGEvent::ResultError && isOutgoing()) {
if (changeFTHostDir()) {
ok = true;
m_ftStatus = FTIdle;
}
}
}
Debug(this,rspOk ? DebugAll : DebugInfo,
"Received result=%s to streamhost used=%s [%p]",
event->name(),usedHost.c_str(),this);
return ok;
}
// Hold/active result
bool hold = (m_onHoldOutId && m_onHoldOutId == event->id());
if (hold || (m_activeOutId && m_activeOutId == event->id())) {
Debug(this,rspOk ? DebugAll : DebugInfo,
"Received result=%s to %s request [%p]",
event->name(),hold ? "hold" : "active",this);
if (!hold)
m_dataFlags &= ~OnHoldLocal;
Message* m = message("call.update");
m->userData(this);
m->addParam("operation","notify");
if (hold)
m->addParam("hold",String::boolText(dataFlags(OnHold)));
else
m->addParam("active",String::boolText(!dataFlags(OnHold)));
Engine::enqueue(m);
if (hold)
m_onHoldOutId = "";
else {
m_activeOutId = "";
resetCurrentAudioContent(true,false);
}
return true;
}
// 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,DebugInfo,"Transfer succeedded [%p]",this);
// TODO: implement
}
else {
Debug(this,DebugNote,"Transfer failed error=%s [%p]",
event->text().c_str(),this);
}
return true;
}
return true;
}
// Process jingle events
switch (event->action()) {
case JGSession::ActDtmf:
event->confirmElement();
Debug(this,DebugAll,"Received dtmf(%s) '%s' [%p]",
event->reason().c_str(),event->text().c_str(),this);
if (event->text()) {
Message* m = message("chan.dtmf");
m->addParam("text",event->text());
m->addParam("detected","jingle");
dtmfEnqueue(m);
}
break;
case JGSession::ActTransportInfo:
if (m_ftStatus == FTNone)
processActionTransportInfo(event);
else
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActTransportAccept:
// TODO: handle it when (if) we'll send transport-replace
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActTransportReject:
// TODO: handle it when (if) we'll send transport-replace
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActTransportReplace:
// TODO: handle it
event->confirmElement();
Debug(this,DebugInfo,"Denying event(%s) [%p]",event->actionName(),this);
if (m_session)
m_session->sendContent(JGSession::ActTransportReject,event->m_contents);
break;
case JGSession::ActContentAccept:
if (m_ftStatus != FTNone) {
event->confirmElement(XMPPError::Request);
break;
}
event->confirmElement();
for (ObjList* o = event->m_contents.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
if (findContent(*c,m_audioContents))
Debug(this,DebugAll,"Event(%s) remote accepted content=%s [%p]",
event->actionName(),c->toString().c_str(),this);
else {
// We don't have such a content
Debug(this,DebugNote,
"Event(%s) remote accepted missing content=%s [%p]",
event->actionName(),c->toString().c_str(),this);
}
}
if (!m_audioContent)
resetCurrentAudioContent(isAnswered(),!isAnswered());
break;
case JGSession::ActContentAdd:
if (m_ftStatus == FTNone)
processActionContentAdd(event);
else
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActContentModify:
// This event should modify the content 'senders' attribute
Debug(this,DebugInfo,"Denying event(%s) [%p]",event->actionName(),this);
event->confirmElement(XMPPError::NotAllowed);
break;
case JGSession::ActContentReject:
if (m_ftStatus != FTNone) {
event->confirmElement(XMPPError::Request);
break;
}
// XEP-0166 Notes - 16: terminate the session if there are no more contents
if (!removeContents(event))
return true;
if (!m_audioContent)
resetCurrentAudioContent(isAnswered(),!isAnswered());
break;
case JGSession::ActContentRemove:
// XEP-0166 Notes - 16: terminate the session if there are no more contents
if (m_ftStatus == FTNone) {
if (!removeContents(event))
return true;
if (!m_audioContent)
resetCurrentAudioContent(isAnswered(),!isAnswered());
}
else {
// Confirm and remove requested content(s)
// Terminate if the first content is removed while negotiating
event->confirmElement();
for (ObjList* o = event->m_contents.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
JGSessionContent* cc = findContent(*c,m_ftContents);
if (cc) {
if (cc == firstFTContent() && m_ftStatus != FTIdle)
return false;
m_ftContents.remove(cc);
}
}
return 0 != m_ftContents.skipNull();
}
break;
case JGSession::ActAccept:
if (!isAnswered()) {
if (m_ftStatus != FTNone)
return setupSocksFileTransfer(true);
processActionAccept(event);
}
break;
case JGSession::ActTransfer:
if (m_ftStatus == FTNone)
processTransferRequest(event);
else
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActRinging:
if (m_ftStatus == FTNone) {
event->confirmElement();
status("ringing");
Engine::enqueue(message("call.ringing",false,true));
}
else
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActHold:
case JGSession::ActActive:
case JGSession::ActMute:
if (m_ftStatus == FTNone)
handleAudioInfoEvent(event);
else
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActTrying:
case JGSession::ActReceived:
if (m_ftStatus == FTNone) {
event->confirmElement();
Debug(this,DebugAll,"Received Jingle event (%p) with action=%s [%p]",
event,event->actionName(),this);
}
else
event->confirmElement(XMPPError::Request);
break;
case JGSession::ActStreamHost:
return processStreamHosts(event);
default:
Debug(this,DebugNote,
"Received unexpected Jingle event (%p) with action=%s [%p]",
event,event->actionName(),this);
}
return true;
}
// Process remote user's presence notifications
// Make the call if outgoing and in Pending (waiting for presence information) state
// Hangup if the remote user is unavailbale
// Return true to disconnect
bool YJGConnection::presenceChanged(bool available, NamedList* params)
{
Lock lock(m_mutex);
if (m_state == Terminated)
return false;
if (m_presTimeout > 0)
maxcall(m_presTimeout + Time::now());
else if (m_presTimeout)
maxcall(0);
m_presTimeout = -1;
// Check if unavailable in any other states
if (!available) {
if (!m_hangup) {
DDebug(this,DebugCall,"Remote user is unavailable [%p]",this);
hangup("offline","Remote user is unavailable");
}
return true;
}
// Check if we are in pending state and remote peer is present
if (!(isOutgoing() && m_state == Pending && available))
return false;
bool ok = true;
if (params) {
// Check for required audio or file transfer
if (m_ftStatus == FTNone)
ok = params->getBoolValue("caps.audio");
else
ok = params->getBoolValue("caps.filetransfer");
}
if (!ok)
return false;
// Check for jingle version override
if (params)
overrideJingleVersion(*params,true);
// Make the call
Debug(this,DebugCall,"Calling. caller=%s called=%s [%p]",
m_local.c_str(),m_remote.c_str(),this);
m_state = Active;
if (m_ftStatus == FTNone) {
XmlElement* transfer = 0;
if (m_transferFrom)
transfer = JGSession::buildTransfer(String::empty(),m_transferFrom);
if (m_offerRawTransport)
addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp));
if (m_offerIceTransport)
addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp));
if (m_offerP2PTransport)
addContent(true,buildAudioContent(JGRtpCandidates::RtpP2P));
if (m_offerGRawTransport)
addContent(true,buildAudioContent(JGRtpCandidates::RtpGoogleRawUdp));
m_session = s_jingle->call(m_sessVersion,m_local,m_remote,m_audioContents,transfer,
m_callerPrompt,m_subject,m_line,&m_sessFlags);
// 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_sessVersion,m_local,m_remote,m_ftContents,0,
m_callerPrompt,m_subject,m_line);
if (!m_session) {
hangup("noconn");
return true;
}
m_session->userData(this);
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::BadRequest;
while (true) {
if (!canTransfer()) {
error = XMPPError::Request;
reason = "Unacceptable in current state";
break;
}
trans = event->jingle() ? XMPPUtils::findFirstChild(*event->jingle(),XmlTag::Transfer) : 0;
if (!trans) {
reason = "Transfer element is misssing";
break;
}
m_transferTo = 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 = trans->getAttribute("from");
break;
}
String subject;
if (!reason && trans)
subject = XMPPUtils::subject(*trans);
if (!reason) {
TelEngine::destruct(m_recvTransferStanza);
m_recvTransferStanza = event->releaseXml();
event->setConfirmed();
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);
bool ok = ((new YJGTransfer(this,subject))->startup());
if (!ok)
transferTerminated(false,"Internal server error");
return ok;
}
// Not acceptable
Debug(this,DebugNote,
"Refusing transfer request reason='%s' (transferring=%u answered=%u) [%p]",
reason,m_transferring,isAnswered(),this);
event->confirmElement(error,reason);
return false;
}
// Transfer terminated notification from transfer thread
void YJGConnection::transferTerminated(bool ok, const char* reason)
{
Lock lock(m_mutex);
if (m_transferring && m_recvTransferStanza) {
if (ok)
Debug(this,DebugCall,"Transfer succeedded [%p]",this);
else
Debug(this,DebugNote,"Transfer failed error='%s' [%p]",reason,this);
}
if (m_session && m_recvTransferStanza) {
if (ok)
m_session->confirmResult(m_recvTransferStanza);
else
m_session->confirmError(m_recvTransferStanza,XMPPError::UndefinedCondition,
reason,XMPPError::TypeCancel);
TelEngine::destruct(m_recvTransferStanza);
}
// Reset transfer data
TelEngine::destruct(m_recvTransferStanza);
m_transferring = false;
m_transferStanzaId = "";
m_transferTo.set("");
m_transferFrom.set("");
m_transferSid = "";
}
int YJGConnection::getRinging(const String& flags, DebugEnabler* enabler, int defVal)
{
if (flags)
defVal = XMPPUtils::decodeFlags(flags,s_ringFlgName);
// Set RingNoEarlySession if RingWithContent
// Reset RingWithContentOnly if RingWithContent is not set
if (0 != (defVal & RingWithContent))
defVal |= RingNoEarlySession;
else
defVal &= ~RingWithContentOnly;
#ifdef DEBUG
String tmp;
XMPPUtils::buildFlags(tmp,defVal,s_ringFlgName);
DDebug(enabler,DebugAll,"Got ring flags 0x%x '%s' from '%s'",defVal,tmp.safe(),flags.safe());
#endif
return defVal;
}
// Process an ActContentAdd event
void YJGConnection::processActionContentAdd(JGEvent* event)
{
if (!event)
return;
ObjList ok;
ObjList remove;
if (!processContentAdd(*event,ok,remove)) {
event->confirmElement(XMPPError::Conflict,"Duplicate content(s)");
return;
}
ObjList* o = 0;
event->confirmElement();
if (m_session && remove.skipNull())
m_session->sendContent(JGSession::ActContentRemove,remove);
if (!ok.skipNull())
return;
for (o = ok.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
event->m_contents.remove(c,false);
addContent(false,c);
}
if (!(m_audioContent || dataFlags(OnHold)))
resetCurrentAudioContent(isAnswered(),!isAnswered());
enqueueCallProgress();
}
// Process an ActTransportInfo event
void YJGConnection::processActionTransportInfo(JGEvent* event)
{
if (!event)
return;
DDebug(this,DebugAll,"processActionTransportInfo() event=%s' [%p]",
event->actionName(),this);
bool ok = m_sessVersion != JGSession::Version0;
bool startAudioContent = false;
JGSessionContent* newContent = 0;
for (ObjList* o = event->m_contents.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
JGSessionContent* cc = findContent(*c,m_audioContents);
if (!cc) {
Debug(this,DebugNote,"Event('%s') content '%s' not found [%p]",
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;
// Check RTCP
changed = updateCandidate(2,*cc,*c) || changed;
if (!changed)
continue;
// Restart current content if the transport belongs to it or
// replace or if the transport belongs to another one
if (m_audioContent == cc) {
startAudioContent = true;
newContent = 0;
}
else
newContent = cc;
}
XDebug(this,DebugAll,
"processActionTransportInfo() event=%s' start=%u crtAudiocontent=%p newContent=%p [%p]",
event->actionName(),startAudioContent,m_audioContent,newContent,this);
if (ok) {
event->confirmElement();
if (newContent) {
if ((m_rtpStarted || m_audioContent || m_rtpId.null()) && !dataFlags(OnHold))
resetCurrentAudioContent(isAnswered(),!isAnswered(),true,newContent);
}
else if ((startAudioContent && !startRtp()) || !(m_audioContent || dataFlags(OnHold)))
resetCurrentAudioContent(isAnswered(),!isAnswered());
else if (!isAnswered())
sendRinging();
}
else
event->confirmElement(XMPPError::NotAcceptable);
enqueueCallProgress();
}
// Handle answer (session accept) event for non file transfer
void YJGConnection::processActionAccept(JGEvent* event)
{
// Update media
Debug(this,DebugCall,"Remote peer answered the call [%p]",this);
m_presTimeout = -1;
m_state = Active;
status("answered");
for (ObjList* o = event->m_contents.skipNull(); o; o = o->skipNext()) {
JGSessionContent* recv = static_cast<JGSessionContent*>(o->get());
// Ignore non session contents
if (!recv->isSession())
continue;
JGSessionContent* c = findContent(*recv,m_audioContents);
if (!c)
continue;
// Update credentials for ICE-UDP
// 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
bool mediaChanged = false;
bool telEvChanged = false;
if (!(checkMedia(*event,*recv) &&
matchMedia(*c,*recv,mediaChanged,telEvChanged))) {
if (c == m_audioContent)
resetEp("audio");
continue;
}
c->m_rtpMedia.m_ready = true;
// First media changed for current audio content
// RTP don't support update: reset audio
if (mediaChanged && c == m_audioContent)
resetEp("audio");
// Update transport(s)
bool changed = updateCandidate(1,*c,*recv);
changed = updateCandidate(2,*c,*recv) || changed;
if (!m_audioContent || (changed && c == m_audioContent))
resetCurrentAudioContent(true,false,true,c);
}
if (!m_audioContent)
resetCurrentAudioContent(true,false,true);
Engine::enqueue(message("call.answered",false,true));
}
// Handle stream hosts events
// Return false if the session was terminated
bool YJGConnection::processStreamHosts(JGEvent* ev)
{
if (!ev)
return true;
if (m_ftStatus == FTNone) {
ev->confirmElement(XMPPError::Request);
return true;
}
// Check if allowed
if (m_ftHostDirection != FTHostRemote) {
// Check if we can change direction
if (!changeFTHostDir(false)) {
ev->confirmElement(XMPPError::Request);
return true;
}
// Drop current FT host
dropFT(true);
// Remove local hosts
dropFTHosts(true,"received remote host(s)");
m_ftStatus = FTIdle;
}
// Check if we already received it
if (m_ftStatus != FTIdle) {
ev->confirmElement(XMPPError::Request);
return true;
}
ev->setConfirmed();
// Remember stanza id
m_ftStanzaId = ev->id();
// Copy hosts from event
ObjList* o = ev->m_streamHosts.skipNull();
while (o) {
m_streamHosts.append(o->get());
o->remove(false);
o = o->skipNull();
}
if (setupSocksFileTransfer(false))
return true;
if (m_ftStanzaId) {
m_session->sendStreamHostUsed("",m_ftStanzaId);
m_ftStanzaId = "";
}
return setupSocksFileTransfer(false);
}
// Update a received candidate. Return true if changed
bool YJGConnection::updateCandidate(unsigned int component, JGSessionContent& local,
JGSessionContent& recv)
{
JGRtpCandidate* rtpRecv = recv.m_rtpRemoteCandidates.findByComponent(component);
if (!rtpRecv)
return false;
JGRtpCandidate* rtp = local.m_rtpRemoteCandidates.findByComponent(component);
#ifdef DEBUG
String s1;
String s2;
dumpCandidate(s1,rtpRecv);
dumpCandidate(s2,rtp);
Debug(this,DebugAll,"updateCandidate() recv: %s found: %s [%p]",
s1.c_str(),s2.c_str(),this);
#endif
// Version0 or p2p transport: check acceptable transport
if (m_sessVersion == JGSession::Version0 ||
local.type() == JGSessionContent::RtpP2P ||
local.type() == JGSessionContent::RtpGoogleRawUdp) {
// We only handle UDP based transports for now
if (rtpRecv->m_protocol != "udp")
return false;
// Only accept a relay as a second transport and only once
if (m_acceptRelay && rtpRecv->m_type == "relay") {
m_acceptRelay = false;
if (rtp) {
Debug(this,DebugNote,"Replacing remote transport type '%s' with a relay [%p]",
rtp->m_type.c_str(),this);
local.m_rtpRemoteCandidates.remove(rtp);
rtp = 0;
if (local.m_rtpMedia.media() == JGRtpMediaList::Audio)
resetEp("audio",false);
}
}
// Any other transport type accepted only initially
else if (rtp)
return false;
}
if (!rtp) {
DDebug(this,DebugAll,"Adding remote transport '%s' in content '%s' [%p]",
rtpRecv->toString().c_str(),local.toString().c_str(),this);
recv.m_rtpRemoteCandidates.remove(rtpRecv,false);
local.m_rtpRemoteCandidates.append(rtpRecv);
return true;
}
// Another candidate: replace
// Same candidate with greater generation: replace
if (rtp->toString() != rtpRecv->toString() ||
rtp->m_generation.toInteger() < rtpRecv->m_generation.toInteger()) {
DDebug(this,DebugAll,
"Replacing remote transport '%s' with '%s' in content '%s' [%p]",
rtp->toString().c_str(),rtpRecv->toString().c_str(),local.toString().c_str(),this);
local.m_rtpRemoteCandidates.remove(rtp);
recv.m_rtpRemoteCandidates.remove(rtpRecv,false);
local.m_rtpRemoteCandidates.append(rtpRecv);
return true;
}
return false;
}
// Add a new content to the list
void YJGConnection::addContent(bool local, JGSessionContent* c)
{
Lock lock(m_mutex);
m_audioContents.append(c);
c->m_rtpMedia.m_ssrc = "";
if (local)
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) {
if (m_sessVersion != JGSession::Version0)
c->m_rtpLocalCandidates.generateIceAuth();
else
c->m_rtpLocalCandidates.generateOldIceAuth();
}
String tmp;
#ifdef DEBUG
JGRtpCandidate* rtp = local ? c->m_rtpLocalCandidates.findByComponent(1) :
c->m_rtpRemoteCandidates.findByComponent(1);
if (rtp) {
tmp << " candidate: ";
dumpCandidate(tmp,rtp);
}
#endif
Debug(this,DebugAll,"Added content='%s' type=%s initiator=%s%s [%p]",
c->toString().c_str(),c->m_rtpLocalCandidates.typeName(),
String::boolText(c->creator() == JGSessionContent::CreatorInitiator),tmp.safe(),this);
}
// Remove a content from list
void YJGConnection::removeContent(JGSessionContent* c)
{
if (!c)
return;
Debug(this,DebugAll,"Removing content='%s' type=%s initiator=%s [%p]",
c->toString().c_str(),c->m_rtpLocalCandidates.typeName(),
String::boolText(c->creator() == JGSessionContent::CreatorInitiator),this);
m_audioContents.remove(c);
}
// Reset the current audio content
// If the content is not re-usable (SRTP with local address),
// add a new identical content and remove the old old one from the session
void YJGConnection::removeCurrentAudioContent(bool removeReq)
{
if (m_rtpStarted || m_audioContent || dataFlags(OnHold)) {
clearEndpoint();
m_rtpId.clear();
m_rtpStarted = false;
}
if (!m_audioContent)
return;
Debug(this,DebugAll,"Removing current audio content (%p,'%s') [%p]",
m_audioContent,m_audioContent->toString().c_str(),this);
// Remove from list if not re-usable
bool check = (m_audioContent->isSession() == isAnswered());
bool removeFromList = removeReq;
if (check && (0 != m_audioContent->m_rtpMedia.m_cryptoLocal.skipNull())) {
JGRtpCandidate* rtpLocal = m_audioContent->m_rtpLocalCandidates.findByComponent(1);
if (rtpLocal && rtpLocal->m_address) {
removeFromList = true;
// Build a new content
JGSessionContent* c = buildAudioContent(m_audioContent->m_rtpLocalCandidates.m_type,
m_audioContent->senders(),false,false);
if (m_audioContent->isEarlyMedia())
c->setEarlyMedia();
// Copy media
c->m_rtpMedia.m_cryptoRequired = m_audioContent->m_rtpMedia.m_cryptoRequired;
c->m_rtpMedia.setMedia(m_audioContent->m_rtpMedia);
// Append
addContent(true,c);
if (m_session)
m_session->sendContent(JGSession::ActContentAdd,c);
}
}
if (removeFromList) {
if (!removeReq && m_session)
m_session->sendContent(JGSession::ActContentRemove,m_audioContent);
removeContent(m_audioContent);
}
TelEngine::destruct(m_audioContent);
}
// This method is used to set the current audio content
// Reset the current content
// Find the first available content and try to use it
// Send a transport info for the new current content
// Return false on error
bool YJGConnection::resetCurrentAudioContent(bool session, bool earlyMedia,
bool sendTransInfo, JGSessionContent* newContent, bool sendRing)
{
DDebug(this,DebugAll,"Resetting current audio content (%s,%s,%s,%p,%s) [%p]",
String::boolText(session),String::boolText(earlyMedia),
String::boolText(sendTransInfo),newContent,String::boolText(sendRing),this);
// Remove the current audio content
removeCurrentAudioContent();
// Set nothing if on hold
if (dataFlags(OnHold))
return false;
if (!newContent) {
// Pick up a new content. Try to find a content with remote candidates
for (ObjList* o = m_audioContents.skipNull(); o; o = o->skipNext()) {
newContent = static_cast<JGSessionContent*>(o->get());
bool ok = newContent->isValidAudio() &&
((session && newContent->isSession()) ||
(earlyMedia && newContent->isEarlyMedia()));
if (ok && newContent->m_rtpRemoteCandidates.findByComponent(1))
break;
newContent = 0;
}
// No content: choose the first suitable one
if (!newContent) {
for (ObjList* o = m_audioContents.skipNull(); o; o = o->skipNext()) {
newContent = static_cast<JGSessionContent*>(o->get());
if (newContent->isValidAudio() &&
((session && newContent->isSession()) ||
(earlyMedia && newContent->isEarlyMedia())))
break;
newContent = 0;
}
}
}
else if (!newContent->isValidAudio())
return false;
if (newContent && newContent->ref()) {
m_audioContent = newContent;
Debug(this,DebugAll,"Using audio content '%s' [%p]",
m_audioContent->toString().c_str(),this);
JGRtpCandidate* rtp = m_audioContent->m_rtpLocalCandidates.findByComponent(1);
if (!(rtp && rtp->m_address))
initLocalCandidates(*m_audioContent,sendTransInfo);
// Reset ring content sent flag
m_ringFlags &= ~RingContentSent;
if (sendRing)
sendRinging();
return startRtp();
}
return false;
}
// Start RTP for the given content
// For raw udp transports, sends a 'trying' session info
bool YJGConnection::startRtp()
{
if (m_hangup)
return false;
if (!m_audioContent) {
DDebug(this,DebugInfo,"Failed to start RTP: no audio content [%p]",this);
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)) {
Debug(this,DebugNote,
"Failed to start RTP for content='%s' candidates local=%s remote=%s [%p]",
m_audioContent->toString().c_str(),String::boolText(0 != rtpLocal),
String::boolText(0 != rtpRemote),this);
return false;
}
Message m("chan.rtp");
m.userData(this);
complete(m);
m.setParam("direction",rtpDir(*m_audioContent));
m.addParam("media","audio");
m.addParam("getsession","true");
ObjList* obj = m_audioContent->m_rtpMedia.skipNull();
if (obj) {
JGRtpMedia* media = static_cast<JGRtpMedia*>(obj->get());
m.addParam("payload",media->m_id);
m.addParam("format",media->m_synonym);
}
m.addParam("evpayload",String(m_audioContent->m_rtpMedia.m_telEvent));
m.addParam("localip",rtpLocal->m_address);
m.addParam("localport",rtpLocal->m_port);
m.addParam("remoteip",rtpRemote->m_address);
m.addParam("remoteport",rtpRemote->m_port);
//m.addParam("autoaddr","false");
bool rtcp = (0 != m_audioContent->m_rtpLocalCandidates.findByComponent(2));
m.addParam("rtcp",String::boolText(rtcp));
// Crypto
if (m_secure) {
ObjList* cr = m_audioContent->m_rtpMedia.m_cryptoRemote.skipNull();
if (cr && m_audioContent->m_rtpMedia.m_cryptoLocal.skipNull())
addSecure(m,static_cast<JGCrypto*>(cr->get()));
else if (m_secureRequired) {
Debug(this,DebugNote,"No required crypto in current content [%p]",this);
dropNoCrypto();
return false;
}
}
String oldPort = rtpLocal->m_port;
if (!Engine::dispatch(m)) {
Debug(this,DebugNote,"Failed to start RTP for content='%s' [%p]",
m_audioContent->toString().c_str(),this);
return false;
}
m_rtpId = m.getValue("rtpid");
rtpLocal->m_port = m.getValue("localport");
String buf;
#ifdef DEBUG
buf << ". Candidates local: ";
dumpCandidate(buf,rtpLocal);
buf << " remote: ";
dumpCandidate(buf,rtpRemote);
#endif
Debug(this,DebugAll,
"RTP started for content='%s' local='%s:%s' remote='%s:%s'%s [%p]",
m_audioContent->toString().c_str(),
rtpLocal->m_address.c_str(),rtpLocal->m_port.c_str(),
rtpRemote->m_address.c_str(),rtpRemote->m_port.c_str(),buf.safe(),this);
if (oldPort != rtpLocal->m_port && m_session) {
rtpLocal->m_generation = rtpLocal->m_generation.toInteger(0) + 1;
m_session->sendContent(JGSession::ActTransportInfo,m_audioContent);
}
if (rtpRemote->m_address &&
(m_audioContent->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpIceUdp ||
m_audioContent->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpP2P)) {
m_rtpStarted = true;
// Start STUN
Message* msg = plugin.message("socket.stun");
msg->userData(m.userData());
String user;
String pwd;
if (m_audioContent->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpIceUdp) {
// FIXME: check if these parameters are correct
user = m_audioContent->m_rtpRemoteCandidates.m_ufrag +
m_audioContent->m_rtpLocalCandidates.m_ufrag;
pwd = m_audioContent->m_rtpLocalCandidates.m_ufrag +
m_audioContent->m_rtpRemoteCandidates.m_ufrag;
}
else {
JGRtpCandidateP2P* local = YOBJECT(JGRtpCandidateP2P,rtpLocal);
JGRtpCandidateP2P* remote = YOBJECT(JGRtpCandidateP2P,rtpRemote);
if (local && remote) {
user = remote->m_username + local->m_username;
pwd = local->m_username + remote->m_username;
}
}
msg->addParam("localusername",user);
msg->addParam("remoteusername",pwd);
msg->addParam("remoteip",rtpRemote->m_address.c_str());
msg->addParam("remoteport",rtpRemote->m_port);
msg->addParam("userid",m_rtpId);
Engine::enqueue(msg);
}
else if (m_audioContent->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpRawUdp) {
m_rtpStarted = true;
// Send trying
if (m_session) {
XmlElement* trying = XMPPUtils::createElement(XmlTag::Trying,
XMPPNamespace::JingleTransportRawUdpInfo);
m_session->sendInfo(trying);
}
}
return true;
}
// Check a received candidate's parameters
// Return false if some parameter's value is incorrect
bool YJGConnection::checkRecvCandidate(JGSessionContent& content, JGRtpCandidate& c)
{
// Check address and port for all
if (!c.m_address || c.m_port.toInteger() <= 0)
return false;
if (content.m_rtpRemoteCandidates.m_type == JGRtpCandidates::RtpRawUdp) {
// XEP-0177 4.2 these attributes are required
return c.toString() && c.m_component && (c.m_generation.toInteger(-1) >= 0);
}
if (content.m_rtpRemoteCandidates.m_type == JGRtpCandidates::RtpIceUdp) {
// XEP-0176 13 XML Schema: these attributes are required
return c.toString() && c.m_component && (c.m_generation.toInteger(-1) >= 0) &&
c.m_network && c.m_priority && (c.m_protocol == "udp") && c.m_type;
}
return false;
}
// Check a received content(s). Fill received lists with accepted/rejected content(s)
// The lists don't own their pointers
// Return false on error
bool YJGConnection::processContentAdd(const JGEvent& event, ObjList& ok, ObjList& remove)
{
for (ObjList* o = event.m_contents.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
bool fileTransfer = false;
// Check content type
switch (c->type()) {
case JGSessionContent::RtpIceUdp:
case JGSessionContent::RtpRawUdp:
case JGSessionContent::RtpP2P:
case JGSessionContent::RtpGoogleRawUdp:
break;
case JGSessionContent::FileBSBOffer:
case JGSessionContent::FileBSBRequest:
// File transfer contents can be added only in session initiate
if (event.action() != JGSession::ActInitiate) {
Debug(this,DebugInfo,
"Event(%s) file transfer content='%s' in non initiate event [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
fileTransfer = true;
break;
case JGSessionContent::Unknown:
case JGSessionContent::UnknownFileTransfer:
Debug(this,DebugNote,
"Event(%s) with unknown (unsupported) content '%s' [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
// Check creator
if ((isOutgoing() && c->creator() == JGSessionContent::CreatorInitiator) ||
(isIncoming() && c->creator() == JGSessionContent::CreatorResponder)) {
Debug(this,DebugNote,
"Event(%s) content='%s' has invalid creator [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
// Done if file transfer
if (fileTransfer) {
ok.append(c)->setDelete(false);
continue;
}
// Check if we already have an audio content with the same name and creator
if (findContent(*c,m_audioContents)) {
Debug(this,DebugNote,
"Event(%s) content='%s' is already added [%p]",
event.actionName(),c->toString().c_str(),this);
return false;
}
// Check transport type
if (c->m_rtpRemoteCandidates.m_type == JGRtpCandidates::Unknown) {
Debug(this,DebugNote,
"Event(%s) content='%s' has unknown transport type [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
// Check candidates
// XEP-0177 Raw UDP: the content must contain valid transport data
JGRtpCandidate* rtp = c->m_rtpRemoteCandidates.findByComponent(1);
if (rtp) {
if (!checkRecvCandidate(*c,*rtp)) {
Debug(this,DebugNote,
"Event(%s) content='%s' has invalid RTP candidate [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
}
else if (c->m_rtpRemoteCandidates.m_type == JGRtpCandidates::RtpRawUdp) {
Debug(this,DebugNote,
"Event(%s) raw udp content='%s' without RTP candidate [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
JGRtpCandidate* rtcp = c->m_rtpRemoteCandidates.findByComponent(2);
if (rtcp && !checkRecvCandidate(*c,*rtcp)) {
Debug(this,DebugNote,
"Event(%s) content='%s' has invalid RTCP candidate [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
// Check media
if (!checkMedia(event,*c)) {
remove.append(c)->setDelete(false);
continue;
}
c->m_rtpMedia.m_ready = true;
// Check crypto
bool error = false;
ObjList* cr = c->m_rtpMedia.m_cryptoRemote.skipNull();
for (; cr; cr = cr->skipNext()) {
JGCrypto* crypto = static_cast<JGCrypto*>(cr->get());
if (!(crypto->m_suite && crypto->m_keyParams)) {
error = true;
break;
}
}
if (error) {
Debug(this,DebugNote,
"Event(%s) content=%s with invalid crypto [%p]",
event.actionName(),c->toString().c_str(),this);
remove.append(c)->setDelete(false);
continue;
}
// Ok
ok.append(c)->setDelete(false);
}
return true;
}
// Remove contents
// Return false if there are no more contents
bool YJGConnection::removeContents(JGEvent* event)
{
if (!event)
return true;
// Confirm and remove requested content(s)
event->confirmElement();
for (ObjList* o = event->m_contents.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
JGSessionContent* cc = findContent(*c,m_audioContents);
if (cc) {
if (m_audioContent == cc)
removeCurrentAudioContent(true);
else
removeContent(cc);
}
}
bool ok = 0 != m_audioContents.skipNull();
if (!ok)
Debug(this,DebugCall,"No more audio contents [%p]",this);
return ok;
}
// Build a RTP audio content. Add used codecs to the list
// Build and init the candidate(s) if the content is a raw udp one
JGSessionContent* YJGConnection::buildAudioContent(JGRtpCandidates::Type type,
JGSessionContent::Senders senders, bool rtcp, bool useFormats)
{
String id;
id << this->id() << "_content_" << (int)Random::random();
JGSessionContent::Type t = JGSessionContent::Unknown;
if (type == JGRtpCandidates::RtpRawUdp)
t = JGSessionContent::RtpRawUdp;
else if (type == JGRtpCandidates::RtpIceUdp)
t = JGSessionContent::RtpIceUdp;
else if (type == JGRtpCandidates::RtpP2P)
t = JGSessionContent::RtpP2P;
else if (type == JGRtpCandidates::RtpGoogleRawUdp)
t = JGSessionContent::RtpGoogleRawUdp;
JGSessionContent* c = new JGSessionContent(t,id,senders,
isOutgoing() ? JGSessionContent::CreatorInitiator : JGSessionContent::CreatorResponder);
// Add codecs
c->m_rtpMedia.m_media = JGRtpMediaList::Audio;
if (m_secure && m_secureRequired)
c->m_rtpMedia.m_cryptoRequired = true;
if (useFormats)
c->m_rtpMedia.setMedia(m_audioFormats);
if (m_sessVersion == JGSession::Version0 || type == JGRtpCandidates::RtpP2P ||
type == JGRtpCandidates::RtpGoogleRawUdp){
// Hack: set second telephone event for implementations expecting it
c->m_rtpMedia.m_telEventName2 = "audio/telephone-event";
}
c->m_rtpLocalCandidates.m_type = c->m_rtpRemoteCandidates.m_type = type;
if (type == JGRtpCandidates::RtpRawUdp || m_secure)
initLocalCandidates(*c,false);
return c;
}
// Build a file transfer content
JGSessionContent* YJGConnection::buildFileTransferContent(bool send, const char* filename,
NamedList& params)
{
// Build the content
String id;
id << this->id() << "_content_" << (int)Random::random();
JGSessionContent::Type t = JGSessionContent::Unknown;
JGSessionContent::Senders s = JGSessionContent::SendUnknown;
if (send) {
t = JGSessionContent::FileBSBOffer;
s = JGSessionContent::SendInitiator;
}
else {
t = JGSessionContent::FileBSBRequest;
s = JGSessionContent::SendResponder;
}
JGSessionContent* c = new JGSessionContent(t,id,s,JGSessionContent::CreatorInitiator);
// Init file
c->m_fileTransfer.addParam("name",filename);
int sz = params.getIntValue("file_size",-1);
if (sz >= 0)
c->m_fileTransfer.addParam("size",String(sz));
const char* hash = params.getValue("file_md5");
if (!null(hash))
c->m_fileTransfer.addParam("hash",hash);
int date = params.getIntValue("file_time",-1);
if (date >= 0) {
String buf;
XMPPUtils::encodeDateTimeSec(buf,date);
c->m_fileTransfer.addParam("date",buf);
}
return c;
}
// Reserve local port for a RTP session content
bool YJGConnection::initLocalCandidates(JGSessionContent& content, bool sendTransInfo)
{
if (m_hangup)
return false;
JGRtpCandidate* rtp = content.m_rtpLocalCandidates.findByComponent(1);
bool incGeneration = (0 != rtp);
if (!rtp) {
bool nonP2P = content.type() != JGSessionContent::RtpP2P &&
content.type() != JGSessionContent::RtpGoogleRawUdp;
rtp = buildCandidate(nonP2P);
content.m_rtpLocalCandidates.append(rtp);
}
// TODO: handle RTCP
Message m("chan.rtp");
m.userData(static_cast<CallEndpoint*>(this));
complete(m);
m.setParam("direction",rtpDir(content));
m.addParam("media","audio");
m.addParam("getsession","true");
m.addParam("anyssrc","true");
if (m_localip)
m.addParam("localip",m_localip);
else if (!plugin.addLocalIp(m)) {
JGRtpCandidate* remote = content.m_rtpRemoteCandidates.findByComponent(1);
if (remote && remote->m_address)
m.addParam("remoteip",remote->m_address);
}
if (m_secure) {
ObjList* cr = content.m_rtpMedia.m_cryptoRemote.skipNull();
if (cr)
addSecure(m,static_cast<JGCrypto*>(cr->get()));
else if (m_secureRequired) {
// TODO: Terminate the call or try to use another content
Debug(this,DebugNote,"No required crypto in current content [%p]",this);
dropNoCrypto();
return false;
}
}
if (!Engine::dispatch(m)) {
Debug(this,DebugNote,"Failed to init RTP for content='%s' [%p]",
content.toString().c_str(),this);
return false;
}
if (m_secure) {
NamedString* cSuite = m.getParam("ocrypto_suite");
if (cSuite) {
JGCrypto* crypto = new JGCrypto("1",*cSuite,m.getValue("ocrypto_key"));
content.m_rtpMedia.m_cryptoLocal.append(crypto);
}
else if (m_secureRequired) {
// Failed to setup encryption
// TODO: Terminate the call or try to use another content
Debug(this,DebugNote,"Failed to setup required crypto [%p]",this);
dropNoCrypto();
return false;
}
}
m_rtpId = m.getValue("rtpid");
plugin.setLocalIp(rtp->m_address,m);
rtp->m_port = m.getValue("localport","-1");
if (incGeneration) {
rtp->m_generation = rtp->m_generation.toInteger(0) + 1;
sendTransInfo = true;
}
// Send transport info
if (sendTransInfo && m_session)
m_session->sendContent(JGSession::ActTransportInfo,&content);
return true;
}
// Match a local content agaist a received one
// Return false if there is no common media
bool YJGConnection::matchMedia(JGSessionContent& local, JGSessionContent& recv,
bool& firstChanged, bool& telEvChanged) const
{
bool first = true;
ListIterator iter(local.m_rtpMedia);
for (GenObject* gen = 0; 0 != (gen = iter.get()); first = false) {
JGRtpMedia* m = static_cast<JGRtpMedia*>(gen);
JGRtpMedia* found = recv.m_rtpMedia.findSynonym(m->m_synonym);
// Check synonym, the content is already checked
if (found) {
if (m->m_id == found->m_id)
continue;
Debug(this,DebugAll,
"Content '%s' remote changed payload id from %s to %s for '%s' [%p]",
local.toString().c_str(),m->m_id.c_str(),found->m_id.c_str(),
m->m_synonym.c_str(),this);
m->m_id = found->m_id;
if (first)
firstChanged = true;
continue;
}
Debug(this,DebugAll,"Content '%s' removing media %s/%s from offer [%p]",
local.toString().c_str(),m->m_id.c_str(),m->m_synonym.c_str(),this);
local.m_rtpMedia.remove(m);
if (first)
firstChanged = true;
}
// Update telephone event payload id
if (local.m_rtpMedia.m_telEvent != recv.m_rtpMedia.m_telEvent) {
Debug(this,DebugAll,"Content '%s' changing tel event from %d to %d [%p]",
local.toString().c_str(),local.m_rtpMedia.m_telEvent,
recv.m_rtpMedia.m_telEvent,this);
local.m_rtpMedia.m_telEvent = recv.m_rtpMedia.m_telEvent;
telEvChanged = true;
}
if (local.m_rtpMedia.skipNull())
return true;
Debug(this,DebugInfo,"No common media for content=%s [%p]",
local.toString().c_str(),this);
return false;
}
// Find a session content in a list
JGSessionContent* YJGConnection::findContent(JGSessionContent& recv,
const ObjList& list) const
{
for (ObjList* o = list.skipNull(); o; o = o->skipNext()) {
JGSessionContent* c = static_cast<JGSessionContent*>(o->get());
if (c->creator() == recv.creator() && c->toString() == recv.toString())
return c;
}
return 0;
}
// Set early media to remote
void YJGConnection::setEarlyMediaOut(Message& msg)
{
if (ringFlag(RingNoEarlySession) || isOutgoing() || isAnswered())
return;
// Don't set it if the peer don't have a source
if (!(getPeer() && getPeer()->getSource() && msg.getBoolValue("earlymedia",true)))
return;
String formats = msg.getParam("formats");
if (!formats)
formats = getPeer()->getSource()->getFormat();
if (!formats)
return;
Lock lock(m_mutex);
if (m_audioContent && m_audioContent->isEarlyMedia())
return;
// Check if we already have an early media content
JGSessionContent* c = 0;
for (ObjList* o = m_audioContents.skipNull(); o; o = o->skipNext()) {
c = static_cast<JGSessionContent*>(o->get());
if (c->isValidAudio() && c->isEarlyMedia())
break;
c = 0;
}
// Build a new content if not found
if (!c) {
c = buildAudioContent(JGRtpCandidates::RtpRawUdp,
JGSessionContent::SendResponder,false,false);
plugin.lock();
c->m_rtpMedia.setMedia(s_usedCodecs,formats);
plugin.unlock();
c->setEarlyMedia();
addContent(true,c);
}
resetCurrentAudioContent(false,true,false,c);
if (m_session)
m_session->sendContent(JGSession::ActContentAdd,c);
}
// Enqueue a call.progress message from the current audio content
// Used for early media
void YJGConnection::enqueueCallProgress()
{
if (!(m_audioContent && m_audioContent->isEarlyMedia()))
return;
status("progressing");
Message* m = message("call.progress");
String formats;
m_audioContent->m_rtpMedia.createList(formats,true);
m->addParam("formats",formats);
Engine::enqueue(m);
}
// Set file transfer stream host
bool YJGConnection::setupSocksFileTransfer(bool start)
{
if (!m_session) {
DDebug(this,DebugNote,"setupSocksFileTransfer: no session [%p]",this);
return false;
}
JGSessionContent* c = firstFTContent();
if (!c) {
DDebug(this,DebugNote,"setupSocksFileTransfer: no contents [%p]",this);
return false;
}
const char* dir = 0;
if (c->type() == JGSessionContent::FileBSBOffer)
dir = isOutgoing() ? "send" : "receive";
else if (c->type() == JGSessionContent::FileBSBRequest)
dir = isIncoming() ? "send" : "receive";
else {
DDebug(this,DebugNote,"setupSocksFileTransfer: no SOCKS contents [%p]",this);
return false;
}
if (start) {
Message m("chan.socks");
m.userData(this);
m.addParam("dst_addr_domain",m_dstAddrDomain);
m.addParam("format","data");
m.addParam("client",String::boolText(m_ftHostDirection != FTHostLocal));
bool ok = Engine::dispatch(m);
if (ok) {
m_ftStatus = FTRunning;
Debug(this,DebugAll,"Started SOCKS file transfer [%p]",this);
}
else {
setReason("notransport");
m_ftStatus = FTTerminated;
Debug(this,DebugNote,"Failed to start SOCKS file transfer [%p]",this);
}
return ok;
}
// Init transport
const char* error = 0;
while (true) {
ObjList* o = m_streamHosts.skipNull();
if (!o) {
dropFT(true);
// We can send hosts: try to get a local socks server
if (m_ftHostDirection == FTHostLocal) {
Message m("chan.socks");
m.userData(this);
m.addParam("dst_addr_domain",m_dstAddrDomain);
m.addParam("direction",dir);
m.addParam("client",String::boolText(false));
if (m_localip && !s_serverMode && m_connSocksServer)
m.addParam("localip",m_localip);
DDebug(this,DebugAll,"Trying to setup local SOCKS server [%p]",this);
if (Engine::dispatch(m)) {
const char* addr = m.getValue("address");
int port = m.getIntValue("port");
m_ftNotifier = m.getValue("notifier");
if (!null(addr) && port > 0) {
m_streamHosts.append(new JGStreamHost(true,m_local,addr,port));
m_ftStatus = FTWaitEstablish;
// Send our stream host
m_session->sendStreamHosts(m_streamHosts,&m_ftStanzaId);
break;
}
dropFT(true);
}
error = "chan.socks failed";
}
else
error = "no hosts";
break;
}
// Remove the first stream host if status is idle: it failed
if (m_ftStatus != FTIdle) {
dropFT(false);
dropFTHost(0,o,"failed");
o = m_streamHosts.skipNull();
}
while (o) {
dropFT(false);
Message m("chan.socks");
m.userData(this);
m.addParam("dst_addr_domain",m_dstAddrDomain);
m.addParam("direction",dir);
m.addParam("client",String::boolText(true));
JGStreamHost* sh = static_cast<JGStreamHost*>(o->get());
m.addParam("remoteip",sh->m_address);
m.addParam("remoteport",String(sh->m_port));
if (Engine::dispatch(m)) {
m_ftNotifier = m.getValue("notifier");
break;
}
dropFTHost(sh,o,"failed");
o = m_streamHosts.skipNull();
}
if (o)
m_ftStatus = FTWaitEstablish;
else
error = "no more hosts";
break;
}
if (!error) {
DDebug(this,DebugAll,"Waiting SOCKS file transfer notifier=%s [%p]",
m_ftNotifier.c_str(),this);
return true;
}
// Check if we can still negotiate hosts
if (changeFTHostDir()) {
m_ftStatus = FTIdle;
return false;
}
m_ftStatus = FTTerminated;
Debug(this,DebugNote,"Failed to initialize SOCKS file transfer '%s' [%p]",
error,this);
hangup("notransport",0,JGSession::ReasonFailTransport);
return false;
}
// Drop file transfer data. Remove the first host in list
void YJGConnection::dropFT(bool removeFirst)
{
if (removeFirst) {
// Remove first entry in hosts
ObjList* o = m_streamHosts.skipNull();
if (o)
dropFTHost(0,o);
}
m_ftNotifier.clear();
clearEndpoint("data");
}
// Drop file transfer hosts
void YJGConnection::dropFTHosts(bool local, const char* reason)
{
for (ObjList* o = m_streamHosts.skipNull(); o;) {
JGStreamHost* sh = static_cast<JGStreamHost*>(o->get());
if (sh->m_local != local)
o = o->skipNext();
else {
dropFTHost(sh,o,reason);
o = o->skipNull();
}
}
}
// Drop file transfer host
void YJGConnection::dropFTHost(JGStreamHost* sh, ObjList* remove, const char* reason)
{
if (!sh && remove)
sh = static_cast<JGStreamHost*>(remove->get());
if (!sh)
return;
Debug(this,DebugAll,"Removing %s streamhost '%s:%d' reason='%s' [%p]",
sh->m_local ? "local" : "remote",sh->m_address.c_str(),sh->m_port,
TelEngine::c_safe(reason),this);
if (remove)
remove->remove();
else
TelEngine::destruct(sh);
}
// Change host sender. Return false on failure
bool YJGConnection::changeFTHostDir(bool resetState)
{
// Outgoing: we've sent hosts, allow remote to sent hosts
// Incoming: remote sent hosts, allow us to send hosts
bool fromLocal = (m_ftHostDirection == FTHostRemote);
if (m_ftHostDirection != FTHostNone && isOutgoing() != fromLocal) {
m_ftHostDirection = fromLocal ? FTHostLocal : FTHostRemote;
Debug(this,DebugAll,"Allowing %s party to send file transfer host(s) [%p]",
fromLocal ? "local" : "remote",this);
return true;
}
if (!resetState)
return false;
if (m_ftHostDirection != FTHostNone)
Debug(this,DebugNote,"No more hosts available [%p]",this);
m_ftHostDirection = FTHostNone;
return false;
}
// Build a RTP candidate
JGRtpCandidate* YJGConnection::buildCandidate(bool nonP2P, bool rtp)
{
if (nonP2P)
return new JGRtpCandidate(id() + "_candidate_" + String((int)Random::random()),
rtp ? "1" : "2");
JGRtpCandidateP2P* p2p = new JGRtpCandidateP2P;
JGRtpCandidates::generateOldIceToken(p2p->m_username);
JGRtpCandidates::generateOldIceToken(p2p->m_password);
return p2p;
}
// Process chan.notify messages
// Handle SOCKS status changes for file transfer
bool YJGConnection::processChanNotify(Message& msg)
{
XDebug(this,DebugAll,"processChanNotify notifier=%s status=%s",
msg.getValue("id"),msg.getValue("status"));
NamedString* notifier = msg.getParam("id");
if (!notifier)
return false;
Lock lock(m_mutex);
if (m_state == Terminated)
return true;
if (*notifier == m_ftNotifier) {
NamedString* status = msg.getParam("status");
if (!status)
return false;
if (*status == "established") {
// Safety check
if (m_state == Terminated || !m_session ||
m_ftHostDirection == FTHostNone || !m_streamHosts.skipNull()) {
hangup("failure");
return true;
}
const String& jid = m_streamHosts.skipNull()->get()->toString();
if (isOutgoing()) {
// Send hosts if the jid is not our's: we did't sent it
if (m_ftHostDirection == FTHostLocal) {
if (m_local != jid)
m_session->sendStreamHosts(m_streamHosts,&m_ftStanzaId);
}
else
m_session->sendStreamHostUsed(jid,m_ftStanzaId);
}
else {
if (m_ftHostDirection == FTHostRemote)
m_session->sendStreamHostUsed(jid,m_ftStanzaId);
// Accept the session
if (isAnswered()) {
if (setupSocksFileTransfer(true)) {
ObjList tmp;
JGSessionContent* c = firstFTContent();
if (c)
tmp.append(c)->setDelete(false);
m_session->accept(tmp);
}
else
hangup("failure");
}
}
if (m_ftStatus != FTRunning && !m_hangup)
m_ftStatus = FTEstablished;
}
else if (*status == "running") {
// Ignore it for now !!!
}
else if (*status == "terminated") {
if (m_ftStatus == FTWaitEstablish) {
// Try to setup another stream host
// Remember: setupSocksFileTransfer changes the host dir
if (setupSocksFileTransfer(false))
return true;
if (m_ftStatus != FTTerminated &&
m_ftHostDirection != FTHostNone && m_session) {
m_streamHosts.clear();
// Current host dir is remote: old one was local: send empty hosts
if (m_ftHostDirection == FTHostRemote) {
m_session->sendStreamHosts(m_streamHosts,&m_ftStanzaId);
return true;
}
// Respond and try to setup our hosts
if (m_ftStanzaId) {
m_session->sendStreamHostUsed("",m_ftStanzaId);
m_ftStanzaId = "";
}
if (setupSocksFileTransfer(false))
return true;
}
}
else if (m_ftStatus != FTIdle)
hangup("failure");
}
return true;
}
return false;
}
// Handle hold/active/mute actions
// Confirm the received element
void YJGConnection::handleAudioInfoEvent(JGEvent* event)
{
Lock lock(m_mutex);
if (!(event && m_session))
return;
XMPPError::Type err = XMPPError::NoError;
const char* text = 0;
// Hold
bool hold = event->action() == JGSession::ActHold;
if (hold || event->action() == JGSession::ActActive) {
if ((hold && !dataFlags(OnHold)) || (!hold && dataFlags(OnHoldRemote))) {
XmlElement* what = 0;
if (event->jingle())
what = XMPPUtils::findFirstChild(*event->jingle(),
hold ? XmlTag::Hold : XmlTag::Active);
if (what) {
if (hold)
m_dataFlags |= OnHoldRemote;
else
m_dataFlags &= ~OnHoldRemote;
Message* m = message("call.update");
m->addParam("operation","notify");
m->userData(this);
// Copy additional attributes
// Reset param 'name': the second param of toList() is the prefix
XMPPUtils::toList(*what,*m,what->tag());
m->setParam(what->tag(),String::boolText(true));
// Clear endpoint before dispatching the message
// Our data source/consumer may be replaced
if (hold) {
clearEndpoint();
m_rtpId.clear();
m_rtpStarted = false;
}
Engine::dispatch(*m);
TelEngine::destruct(m);
// Reset data transport when put on hold
removeCurrentAudioContent();
// Update channel data source/consumer
if (!hold)
resetCurrentAudioContent(true,false);
}
else
err = XMPPError::FeatureNotImpl;
}
// Respond with error if put on hold by the other party
else if (dataFlags(OnHoldLocal)) {
err = XMPPError::Request;
text = "Already on hold by the other party";
}
}
else if (event->action() == JGSession::ActMute) {
// TODO: implement
err = XMPPError::FeatureNotImpl;
}
else
err = XMPPError::FeatureNotImpl;
// Confirm received element
if (err == XMPPError::NoError) {
DDebug(this,DebugAll,"Accepted '%s' request [%p]",event->actionName(),this);
event->confirmElement();
}
else {
Debug(this,DebugInfo,"Denying '%s' request error='%s' reason='%s' [%p]",
event->actionName(),XMPPUtils::s_error[err].c_str(),text,this);
event->confirmElement(err,text);
}
}
// Check jingle version override from call.execute or resource caps
void YJGConnection::overrideJingleVersion(const NamedList& list, bool caps)
{
String* ver = list.getParam(caps ? "caps.jingle_version" : "ojingle_version");
if (!ver)
return;
JGSession::Version v = JGSession::lookupVersion(*ver);
if (v != JGSession::VersionUnknown && v != m_sessVersion) {
Debug(this,DebugAll,"Jingle version set to %s from %s",
ver->c_str(),caps ? "resource caps" : "routing");
m_sessVersion = v;
}
}
// Override session flags
void YJGConnection::overrideJingleFlags(const NamedList& list, const char* param)
{
String* str = list.getParam(param);
if (!str)
return;
m_sessFlags = JGEngine::decodeFlags(*str,JGSession::s_flagName);
Debug(this,DebugAll,"Session flags set to %d from %s=%s [%p]",
m_sessFlags,param,str->c_str(),this);
}
// Copy chan/session params to a destination list
void YJGConnection::copySessionParams(NamedList& list, bool redirect)
{
String* copy = list.getParam("copyparams");
if (redirect) {
list.addParam("redirect",String::boolText(true));
jingleAddParam(list,"redirectcount",String(m_redirectCount),copy);
list.addParam("diverter",m_remote,false);
}
if (m_ftStatus == FTNone) {
String formats;
m_audioFormats.createList(formats,true);
jingleAddParam(list,"formats",formats,copy,false);
}
else
jingleAddParam(list,"format","data",copy);
// Jingle session params
jingleAddParam(list,"line",m_line,copy,false);
jingleAddParam(list,"ojingle_version",
JGSession::lookupVersion(m_sessVersion,""),copy,false);
String flags;
JGEngine::encodeFlags(flags,m_sessFlags,JGSession::s_flagName);
jingleAddParam(list,"ojingle_flags",flags,copy,false);
jingleAddParam(list,"callerprompt",m_callerPrompt,copy,false);
jingleAddParam(list,"subject",m_subject,copy,false);
jingleAddParam(list,"secure",String::boolText(m_secure),copy);
jingleAddParam(list,"secure_required",String::boolText(m_secureRequired),copy);
jingleAddParam(list,"offerrawudp",String::boolText(m_offerRawTransport),copy);
jingleAddParam(list,"offericeudp",String::boolText(m_offerIceTransport),copy);
jingleAddParam(list,"offerp2p",String::boolText(m_offerP2PTransport),copy);
jingleAddParam(list,"offergraw",String::boolText(m_offerGRawTransport),copy);
jingleAddParam(list,"dtmfmethod",lookup(m_dtmfMeth,s_dictDtmfMeth),copy,false);
// File transfer
JGSessionContent* c = firstFTContent();
if (!c)
return;
const char* oper = 0;
if (c->type() == JGSessionContent::FileBSBOffer)
oper = "send";
else if (c->type() == JGSessionContent::FileBSBRequest)
oper = "receive";
else
return;
const String& file = c->m_fileTransfer["name"];
if (!file)
return;
jingleAddParam(list,"operation",oper,copy);
jingleAddParam(list,"file_name",file,copy);
jingleAddParam(list,"file_size",c->m_fileTransfer.getValue("size"),copy,false);
jingleAddParam(list,"file_md5",c->m_fileTransfer.getValue("hash"),copy,false);
unsigned int t = XMPPUtils::decodeDateTimeSec(c->m_fileTransfer["date"]);
if (t != (unsigned int)-1)
jingleAddParam(list,"file_time",String(t),copy);
}
// Check media in a received content
bool YJGConnection::checkMedia(const JGEvent& event, JGSessionContent& c)
{
JGRtpMediaList& codecs = m_audioFormats;
// Fill a string with our capabilities for debug purposes
String remoteCaps;
if (debugAt(DebugInfo))
c.m_rtpMedia.createList(remoteCaps,false);
ListIterator iter(c.m_rtpMedia);
for (GenObject* go = 0; (go = iter.get());) {
JGRtpMedia* recv = static_cast<JGRtpMedia*>(go);
XDebug(this,DebugAll,"Checking received media %s/%s/%s/%s/%s/%s/%s [%p]",
recv->m_id.c_str(),recv->m_name.c_str(),recv->m_clockrate.c_str(),
recv->m_channels.c_str(),recv->m_pTime.c_str(),
recv->m_maxPTime.c_str(),recv->m_bitRate.c_str(),this);
const char* reason = 0;
int level = DebugNote;
// Use a while() to break to the end
while (true) {
// RTP payload id must be [0..127]
int payloadId = recv->m_id.toInteger(-1);
if (payloadId < 0 || payloadId > 127) {
reason = "Invalid id";
break;
}
// XEP 0167: Channels is an unsigned byte, defaults to 1
// We support only 1 channel for now
if (recv->m_channels.toInteger(1) != 1) {
reason = "Invalid number of channels";
break;
}
JGRtpMedia* found = 0;
// 0..95: static payloads: match by id
// > 95: dynamic payloads: match by name
if (payloadId < 96)
found = codecs.findMedia(recv->m_id);
else if (recv->m_name) {
// Remove tel event from offer
if (isTelEvent(recv->m_name)) {
XDebug(this,DebugAll,"Removing tel event payload=%d '%s' [%p]",
payloadId,recv->m_name.c_str(),this);
c.m_rtpMedia.m_telEvent = payloadId;
c.m_rtpMedia.m_telEventName = recv->m_name;
c.m_rtpMedia.remove(recv);
break;
}
for (ObjList* o = codecs.skipNull(); o; o = o->skipNext(), found = 0) {
found = static_cast<JGRtpMedia*>(o->get());
if ((found->m_name |= recv->m_name))
continue;
if (recv->m_clockrate && recv->m_clockrate != found->m_clockrate)
continue;
// Fix ilbc
if (recv->m_name &= "ilbc") {
// RFC 3952 specifies
// 30ms ptime = 13.33 kbit/s: check 13000
// 20ms ptime = 15.2 kbit/s: check 15000
if (!recv->m_pTime && recv->m_bitRate) {
int val = recv->m_bitRate.toInteger() / 1000;
if (val == 13)
recv->m_pTime = "30";
else if (val == 15)
recv->m_pTime = "20";
}
if (!recv->m_pTime)
recv->m_pTime = (s_ilbcDefault30 ? "30" : "20");
if (recv->m_pTime != found->m_pTime)
continue;
}
break;
}
}
else {
// XEP 0167: name is mandatory for dynamic payloads
reason = "Missing name for dynamic payload";
break;
}
if (found) {
XDebug(this,DebugAll,"Setting synonym=%s to received %s from %s/%s [%p]",
found->m_synonym.c_str(),recv->m_name.c_str(),
found->m_id.c_str(),found->m_name.c_str(),this);
recv->m_synonym = found->m_synonym;
}
else {
reason = "Codec disabled/unknown";
level = DebugAll;
}
break;
}
if (!reason)
continue;
Debug(this,level,
"Event(%s) removing payload id=%s %s/%s/%s/%s from content='%s': %s [%p]",
event.actionName(),recv->m_id.c_str(),recv->m_name.c_str(),
recv->m_clockrate.c_str(),recv->m_channels.c_str(),recv->m_pTime.c_str(),
c.toString().c_str(),reason,this);
c.m_rtpMedia.remove(recv);
}
// Check if both parties have common media
if (c.m_rtpMedia.skipNull()) {
#ifdef DEBUG
String formats;
c.m_rtpMedia.createList(formats,true);
Debug(this,DebugAll,"Set formats '%s' in content '%s' [%p]",
formats.c_str(),c.toString().c_str(),this);
#endif
return true;
}
if (debugAt(DebugInfo)) {
String localCaps;
codecs.createList(localCaps,false);
Debug(this,DebugNote,
"Event(%s) no common media for content='%s' local='%s' remote='%s' [%p]",
event.actionName(),c.toString().c_str(),localCaps.c_str(),
remoteCaps.c_str(),this);
}
return false;
}
// Clear and reset audio related data
void YJGConnection::resetEp(const String& what, bool releaseContent)
{
Debug(this,DebugAll,"Resetting endpoint '%s' [%p]",what.c_str(),this);
clearEndpoint(what);
Lock lock(m_mutex);
if (!what || what == "audio") {
m_rtpId.clear();
m_rtpStarted = false;
if (releaseContent)
TelEngine::destruct(m_audioContent);
}
}
// Hangup and drop the call if failed to setup encryption
void YJGConnection::dropNoCrypto()
{
const char* reason = "crypto-required";
hangup(reason,"Failed to setup encryption");
Message* m = new Message("call.drop");
m->addParam("id",id());
m->addParam("reason",reason);
Engine::enqueue(m);
}
// Send ringing
void YJGConnection::sendRinging(NamedList* params)
{
if (ringFlag(RingNone))
return;
bool sendContent = ringFlag(RingWithContent) && getPeer() && getPeer()->getSource();
DDebug(this,DebugNote,"sendRinging flags=0x%x params=%p sendContent=%u [%p]",
m_ringFlags,params,sendContent,this);
if (params) {
if (params->getBoolValue(YSTRING("earlymedia"),true))
m_ringFlags |= RingGotEarlyMedia;
sendContent = sendContent && ringFlag(RingGotEarlyMedia);
}
else {
// Added new content or changed one
// Return if no ringing or content already sent
if (!ringFlag(RingRinging) || ringFlag(RingContentSent))
return;
sendContent = sendContent && ringFlag(RingGotEarlyMedia);
// No need to send content: return
if (!sendContent)
return;
}
Lock mylock(m_mutex);
if (!m_session)
return;
XmlElement* rInfo = m_session->createRtpInfoXml(JGSession::RtpRinging);
if (!rInfo)
return;
XmlElement* cXml = 0;
if (sendContent) {
if (!m_audioContent || m_audioContent->isEarlyMedia())
resetCurrentAudioContent(true,false,true,0,false);
JGRtpCandidate* rtp = m_audioContent ? m_audioContent->m_rtpLocalCandidates.findByComponent(1) : 0;
if (rtp && rtp->m_address) {
cXml = m_audioContent->toXml(false,true,true,true,false);
m_ringFlags |= RingContentSent;
}
else if (ringFlag(RingWithContentOnly)) {
TelEngine::destruct(rInfo);
return;
}
}
m_session->sendInfo(rInfo,0,cXml);
}
/*
* Transfer thread (route and execute)
*/
YJGTransfer::YJGTransfer(YJGConnection* conn, const char* subject)
: 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));
if (!null(subject))
m_msg.addParam("subject",subject);
m_msg.addParam("reason",lookup(JGSession::Transferred,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.find(m_transferorID));
if (conn)
conn->transferTerminated(!error,error);
#ifdef DEBUG
else
Debug(&plugin,DebugNote,
"%s thread transfer terminated trans=%s error=%s (transferor not found) [%p]",
name(),m_transferredID.c_str(),error.c_str(),this);
#endif
plugin.unlock();
}
/*
* JBMessageHandler
*/
YJGMessageHandler::YJGMessageHandler(int handler, int prio)
: MessageHandler(lookup(handler,s_msgHandler),prio,plugin.name()),
m_handler(handler)
{
}
bool YJGMessageHandler::received(Message& msg)
{
switch (m_handler) {
case JabberIq:
return !plugin.isModule(msg) && plugin.handleJabberIq(msg);
case ResNotify:
return !plugin.isModule(msg) && plugin.handleResNotify(msg);
case ResSubscribe:
return !plugin.isModule(msg) && plugin.handleResSubscribe(msg);
case ChanNotify:
return !plugin.isModule(msg) && plugin.handleChanNotify(msg);
case EngineStart:
plugin.handleEngineStart(msg);
return false;
case UserNotify:
return !plugin.isModule(msg) && plugin.handleUserNotify(msg);
default:
DDebug(&plugin,DebugStub,"YJGMessageHandler(%s) not handled!",msg.c_str());
}
return false;
}
/*
* YJGDriver
*/
YJGDriver::YJGDriver()
: Driver("jingle","varchans"), m_init(false), m_ftProxy(0), m_handleAllRes(false),
m_entityCaps(0)
{
Output("Loaded module YJingle");
s_serverMode = !Engine::clientMode();
if (s_serverMode)
Engine::extraPath("jabber");
}
YJGDriver::~YJGDriver()
{
Output("Unloading module YJingle");
delete s_jingle;
s_jingle = 0;
TelEngine::destruct(m_entityCaps);
}
void YJGDriver::initialize()
{
Output("Initializing module YJingle");
lock();
s_cfg = Engine::configFile("yjinglechan");
s_cfg.load();
NamedList dummy("");
NamedList* sect = s_cfg.getSection("general");
if (!sect)
sect = &dummy;
// Update now the server mode flag
s_serverMode = sect->getBoolValue("servermode",!Engine::clientMode());
if (!m_init) {
m_init = true;
// Init all known codecs
s_knownCodecs.add("0", "PCMU", "8000", "mulaw");
s_knownCodecs.add("2", "G726-32", "8000", "g726");
s_knownCodecs.add("3", "GSM", "8000", "gsm");
s_knownCodecs.add("4", "G723", "8000", "g723");
s_knownCodecs.add("7", "LPC", "8000", "lpc10");
s_knownCodecs.add("8", "PCMA", "8000", "alaw");
s_knownCodecs.add("9", "G722", "8000", "g722/16000");
s_knownCodecs.add("11", "L16", "8000", "slin");
s_knownCodecs.add("15", "G728", "8000", "g728");
s_knownCodecs.add("18", "G729", "8000", "g729");
s_knownCodecs.add("31", "H261", "90000", "h261");
s_knownCodecs.add("32", "MPV", "90000", "mpv");
s_knownCodecs.add("34", "H263", "90000", "h263");
s_knownCodecs.add("98", "iLBC", "8000", "ilbc");
s_knownCodecs.add("98", "iLBC", "8000", "ilbc20", 0, "20", 0, "15200");
s_knownCodecs.add("98", "iLBC", "8000", "ilbc30", 0, "30", 0, "13300");
s_knownCodecs.add("102","speex", "8000", "speex");
s_knownCodecs.add("103","speex", "16000", "speex/16000");
s_knownCodecs.add("104","speex", "32000", "speex/32000");
s_knownCodecs.add("105","ISAC", "16000", "isac/16000");
s_knownCodecs.add("106","ISAC", "32000", "isac/32000");
s_jingle = new YJGEngine;
s_jingle->debugChain(this);
// Driver setup
setup();
installRelay(Halt);
installRelay(Route);
installRelay(Update);
installRelay(Transfer);
installRelay(MsgExecute);
installRelay(Progress);
// Install handlers
for (const TokenDict* d = s_msgHandler; d->token; d++) {
if (!Engine::clientMode() && d->value == YJGMessageHandler::UserNotify)
continue;
int prio = d->value < 0 ? 100 : d->value;
if (d->value == YJGMessageHandler::ResNotify)
prio = sect->getIntValue(d->token,prio);
YJGMessageHandler* h = new YJGMessageHandler(d->value,prio);
Engine::install(h);
m_handlers.append(h);
}
// Set features
m_features.add(XMPPNamespace::Jingle);
m_features.add(XMPPNamespace::JingleError);
m_features.add(XMPPNamespace::JingleAppsRtpAudio);
m_features.add(XMPPNamespace::JingleAppsRtp);
m_features.add(XMPPNamespace::JingleAppsRtpInfo);
m_features.add(XMPPNamespace::JingleAppsRtpError);
m_features.add(XMPPNamespace::JingleTransportIceUdp);
m_features.add(XMPPNamespace::JingleTransportRawUdp);
m_features.add(XMPPNamespace::JingleTransfer);
m_features.add(XMPPNamespace::JingleDtmf);
m_features.add(XMPPNamespace::JingleAppsFileTransfer);
m_features.add(XMPPNamespace::JingleSession);
m_features.add(XMPPNamespace::JingleAudio);
m_features.add(XMPPNamespace::JingleTransport);
m_features.add(XMPPNamespace::DtmfOld);
m_features.add(XMPPNamespace::DiscoInfo);
m_features.add(XMPPNamespace::DiscoItems);
m_features.add(XMPPNamespace::EntityCaps);
if (s_serverMode)
m_features.m_identities.append(new JIDIdentity("gateway","telephony","Jingle Telephony Gateway"));
else
m_features.m_identities.append(new JIDIdentity("client","pc"));
m_features.updateEntityCaps();
m_entityCaps = XMPPUtils::createEntityCaps(m_features.m_entityCapsHash,s_capsNode);
(new YJGEngineWorker)->startup();
}
else {
setDomains(sect->getValue("domains"));
loadLimits();
}
s_jingle->initialize(*sect);
if (s_serverMode) {
s_requestSubscribe = sect->getBoolValue("request_subscribe",true);
s_autoSubscribe = sect->getBoolValue("auto_subscribe",false);
m_resources.clear();
m_handleAllRes = false;
const char* resources = sect->getValue("resources");
if (resources) {
String resList(resources);
ObjList* list = resList.split(',',false);
for (ObjList* o = list->skipNull(); o; o = o->skipNext()) {
String* tmp = static_cast<String*>(o->get());
if (!m_resources.find(*tmp))
m_resources.append(new String(*tmp));
}
TelEngine::destruct(list);
}
else {
m_handleAllRes = true;
m_resources.append(new String("yate"));
}
}
else {
s_requestSubscribe = false;
s_autoSubscribe = false;
}
s_singleTone = sect->getBoolValue("singletone",true);
s_pendingTimeout = sect->getIntValue("pending_timeout",10000);
s_imToChanText = sect->getBoolValue("imtochantext",false);
s_useCrypto = sect->getBoolValue("secure",false);
s_cryptoMandatory = sect->getBoolValue("secure_required",false);
s_acceptRelay = sect->getBoolValue("accept_relay",!s_serverMode);
s_sessVersion = JGSession::lookupVersion(sect->getValue("jingle_version"),JGSession::Version1);
s_ringFlags = YJGConnection::getRinging(*sect,this);
m_anonymousCaller = sect->getValue("anonymous_caller","unk_caller");
m_localAddress = sect->getValue("localip");
s_offerRawTransport = sect->getBoolValue("offerrawudp",true);
s_offerIceTransport = sect->getBoolValue("offericeudp",true);
s_offerP2PTransport = sect->getBoolValue("offerp2p",false);
s_offerGRawTransport = sect->getBoolValue("offergraw",false);
int redir = sect->getIntValue("redirectcount");
s_redirectCount = (redir >= 0) ? redir : 0;
s_dtmfMeth = sect->getIntValue("dtmfmethod",s_dictDtmfMeth,DtmfJingle);
s_clearFilePath = sect->getBoolValue("clear_file_path");
// set max chans
maxChans(sect->getIntValue("maxchans",maxChans()));
int prio = sect->getIntValue("resource_priority");
if (prio < -128)
s_priority = -128;
else if (prio > 127)
s_priority = 127;
else
s_priority = prio;
// Init codecs in use. Check each codec in known codecs list against the configuration
s_usedCodecs.clear();
bool defcodecs = s_cfg.getBoolValue("codecs","default",true);
for (ObjList* o = s_knownCodecs.skipNull(); o; o = o->skipNext()) {
JGRtpMedia* crt = static_cast<JGRtpMedia*>(o->get());
if (crt->m_name &= "ilbc")
continue;
bool enable = defcodecs && DataTranslator::canConvert(crt->m_synonym);
if (s_cfg.getBoolValue("codecs",crt->m_synonym,enable))
s_usedCodecs.append(new JGRtpMedia(*crt));
}
// Special care for ilbc
bool ilbc = s_cfg.getBoolValue("codecs","ilbc",defcodecs);
if (ilbc) {
String tmp = s_cfg.getValue("hacks","ilbc_forced","ilbc30");
if (tmp != "ilbc20" && tmp != "ilbc30")
tmp = "ilbc30";
JGRtpMedia* s = s_knownCodecs.findSynonym(tmp);
if (s && DataTranslator::canConvert(s->m_synonym))
s_usedCodecs.append(new JGRtpMedia(*s));
tmp = s_cfg.getValue("hacks","ilbc_default","ilbc30");
s_ilbcDefault30 = (tmp != "ilbc20");
}
TelEngine::destruct(m_ftProxy);
const char* ftJid = sect->getValue("socks_proxy_jid");
if (!null(ftJid)) {
const char* ftAddr = sect->getValue("socks_proxy_ip");
int ftPort = sect->getIntValue("socks_proxy_port",-1);
if (!(null(ftAddr) || ftPort < 1))
m_ftProxy = new JGStreamHost(true,ftJid,ftAddr,ftPort);
else
Debug(this,DebugNote,
"Invalid addr/port (%s:%s) for default file transfer proxy",
sect->getValue("socks_proxy_ip"),sect->getValue("socks_proxy_port"));
}
int dbg = DebugInfo;
if (!m_localAddress && s_serverMode)
dbg = DebugNote;
if (!s_usedCodecs.count())
dbg = DebugWarn;
if (debugAt(dbg)) {
String s;
s << " localip=" << (m_localAddress ? m_localAddress.c_str() : "MISSING");
s << " jingle_version=" << JGSession::lookupVersion(s_sessVersion);
s << " singletone=" << String::boolText(s_singleTone);
s << " pending_timeout=" << s_pendingTimeout;
s << " anonymous_caller=" << m_anonymousCaller;
String media;
if (!s_usedCodecs.createList(media,true))
media = "MISSING";
s << " codecs=" << media;
if (m_ftProxy)
s << " socks_proxy=" << m_ftProxy->c_str() << ":" <<
m_ftProxy->m_address.c_str() << ":" << m_ftProxy->m_port;
Debug(this,dbg,"Module initialized:%s",s.c_str());
}
unlock();
}
// Check if we have an existing stream (account)
bool YJGDriver::hasLine(const String& line) const
{
if (!line)
return false;
Message* m = checkAccount(line);
if (!m)
return false;
TelEngine::destruct(m);
return true;
}
// Make outgoing calls
// Build peers' JIDs and check if the destination is available
bool YJGDriver::msgExecute(Message& msg, String& dest)
{
if (!msg.userData()) {
Debug(this,DebugNote,"Jingle call failed. No data channel");
msg.setParam("error","failure");
return false;
}
JabberID caller;
JabberID called;
NamedList caps("");
called.set(dest);
if (!called.node()) {
Debug(this,DebugNote,"Jingle call failed. Incomplete called '%s'",called.c_str());
msg.setParam("error","failure");
return false;
}
bool checkCalled = msg.getBoolValue("checkcalled",true);
const char* line = msg.getValue("line");
String localip;
// Set caller
if (s_serverMode) {
const char* cr = msg.getValue("caller");
caller.set(cr);
if (!caller.node()) {
lock();
// has domain: 'cr' is the username: pick the default domain
// no domain: set default username and pick the default domain
if (!caller.domain())
cr = m_anonymousCaller;
// The first component domain is the default one
ObjList* o = m_domains.skipNull();
if (o)
caller.set(cr,o->get()->toString());
else
caller.set("");
unlock();
if (!caller) {
Debug(this,DebugNote,"Jingle call failed. No default server");
msg.setParam("error","failure");
return false;
}
}
// Check domain
if (!handleDomain(caller.domain())) {
Debug(this,DebugNote,"Jingle call failed. Caller '%s' not in our domain(s)",
caller.c_str());
msg.setParam("error","failure");
return false;
}
// Check/set the resource
if (caller.bare() && !caller.resource()) {
caller.resource(msg.getValue("caller_instance"));
if (!caller.resource()) {
String tmp;
defaultResource(tmp);
caller.resource(tmp);
}
}
if (caller.resource() && !handleResource(caller.resource())) {
Debug(this,DebugNote,"Jingle call failed. Invalid resource '%s'",
caller.resource().c_str());
msg.setParam("error","failure");
return false;
}
}
else {
// Get line data
if (!TelEngine::null(line)) {
Message* m = plugin.checkAccount(line,true,checkCalled ? &called : 0);
if (m) {
caller.set(m->getValue("jid"));
if (caller.isFull()) {
if (checkCalled && called && !called.resource())
called.resource(m->getValue("instance"));
// Copy resource caps
unsigned int n = m->length();
for (unsigned int i = 0; i < n; i++) {
NamedString* ns = m->getParam(i);
if (ns && ns->name().startsWith("caps."))
caps.addParam(ns->name(),*ns);
}
}
else
caller.set("");
localip = m->getValue("localip");
TelEngine::destruct(m);
}
if (!caller)
DDebug(this,DebugInfo,"No stream for line=%s",line);
}
if (!caller)
caller.set(msg.getValue("caller"));
}
if (!caller.isFull()) {
Debug(this,DebugNote,"Jingle call failed. Incomplete caller '%s'",
caller.c_str());
msg.setParam("error","failure");
return false;
}
// Called party must always be full in client mode
if (checkCalled && !(s_serverMode || called.isFull())) {
Debug(this,DebugNote,"Jingle call failed. Incomplete called '%s'",
called.c_str());
msg.setParam("error","failure");
return false;
}
// Check if this is a file transfer
String file;
String* format = msg.getParam("format");
if (format && *format == "data") {
// Check file. Remove path if present
file = msg.getValue("file_name");
if (file && msg.getBoolValue(YSTRING("clear_file_path"),s_clearFilePath)) {
int pos = file.rfind('/');
if (pos == -1)
pos = file.rfind('\\');
if (pos != -1)
file = file.substr(pos + 1);
}
if (file.null()) {
Debug(this,DebugNote,"Jingle call failed. File transfer request with no file");
msg.setParam("error","failure");
return false;
}
}
bool online = !(checkCalled && called.resource().null());
bool local = (caller.domain() == called.domain());
if (!online) {
bool reqSub = false;
// Get a resource
// Synchronous probe targets (try to get resource and caps from stored data)
Message* m = plugin.message("resource.notify");
m->addParam("operation","probe");
m->addParam("from",caller.bare());
m->addParam("to",called.bare());
m->addParam("to_local",String::boolText(local));
m->addParam("sync",String::boolText(true));
bool ok = Engine::dispatch(m);
if (ok) {
int n = m->getIntValue("instance.count");
DDebug(this,DebugAll,"Checking %d instances for call from %s to %s",
n,caller.c_str(),called.c_str());
String prefix("instance.");
for (int i = 1; i <= n; i++) {
// TODO: avoid our own resources
String pref(prefix + String(i));
String* inst = m->getParam(pref);
if (TelEngine::null(inst))
continue;
pref << ".";
bool cap = false;
if (!file)
cap = m->getBoolValue(pref + "caps.audio");
else
cap = m->getBoolValue(pref + "caps.filetransfer");
if (!cap)
continue;
called.resource(*inst);
// Copy caps
unsigned int count = m->count();
String p(pref + "caps.");
for (unsigned int j = 0; j < count; j++) {
NamedString* ns = m->getParam(j);
if (ns && ns->name().startsWith(p))
caps.addParam(ns->name().substr(pref.length()),*ns);
}
}
if (!called.resource())
reqSub = s_requestSubscribe;
}
else
reqSub = s_serverMode && s_requestSubscribe;
TelEngine::destruct(m);
if (called.resource()) {
online = true;
Debug(this,DebugAll,"Found resource '%s' for called '%s'",
called.resource().c_str(),called.bare().c_str());
}
else if (reqSub) {
Message* m = plugin.message("resource.subscribe");
m->addParam("operation","subscribe");
m->addParam("subscriber",caller.bare());
m->addParam("notifier",called.bare());
Engine::enqueue(m);
}
else {
Debug(this,DebugNote,"Jingle call failed. No resource available for called party");
msg.setParam("error","offline");
return false;
}
}
// Lock driver to prevent probe response to be processed before the channel
// is fully built
Lock lock(this);
Debug(this,DebugAll,
"msgExecute. caller='%s' called='%s' online=%s filetransfer=%s",
caller.c_str(),called.c_str(),String::boolText(online),
String::boolText(!file.null()));
YJGConnection* conn = new YJGConnection(msg,caller,called,online,caps,file,localip);
conn->initChan();
bool ok = conn->state() != YJGConnection::Terminated;
lock.drop();
if (ok) {
CallEndpoint* ch = YOBJECT(CallEndpoint,msg.userData());
if (ch && conn->connect(ch,msg.getValue("reason"))) {
conn->callConnect(msg);
msg.setParam("peerid",conn->id());
msg.setParam("targetid",conn->id());
}
}
else {
Debug(this,DebugNote,"Jingle call failed to initialize error=%s",
conn->reason().c_str());
msg.setParam("error","failure");
}
TelEngine::destruct(conn);
return ok;
}
// Message handler: Disconnect channels, destroy streams, clear rosters
bool YJGDriver::received(Message& msg, int id)
{
if (id == MsgExecute)
return !isModule(msg) && handleImExecute(msg);
if (id == Execute) {
// Client only: handle call.execute with target starting jabber/
if (s_serverMode)
return Driver::received(msg,id);
String callto(msg.getValue("callto"));
if (!callto.startSkip("jabber/",false))
return Driver::received(msg,id);
return msgExecute(msg,callto);
}
if (id == Halt) {
// Uninstall message handlers
for (ObjList* o = m_handlers.skipNull(); o; o = o->skipNext()) {
YJGMessageHandler* h = static_cast<YJGMessageHandler*>(o->get());
Engine::uninstall(h);
}
dropAll(msg);
}
return Driver::received(msg,id);
}
// Handle jabber.iq messages
bool YJGDriver::handleJabberIq(Message& msg)
{
JabberID to(msg.getValue("to"));
if (s_serverMode && !(to.domain() && handleDomain(to.domain())))
return false;
if (to && !to.resource())
to.resource(msg.getValue("to_instance"));
const char* xmlns = msg.getValue("xmlns");
bool session = false;
bool discoInfo = false;
bool discoItems = false;
XMPPUtils::IqType t = XMPPUtils::iqType(msg.getValue("type"));
// Let the jingle sessions match responses
// Check handled namespaces if the iq is not an error or result
if (t != XMPPUtils::IqResult && t != XMPPUtils::IqError &&
!TelEngine::null(xmlns)) {
int t = XMPPUtils::s_ns[xmlns];
session = (t == XMPPNamespace::Jingle || t == XMPPNamespace::JingleSession ||
t == XMPPNamespace::ByteStreams);
discoInfo = !session && (t == XMPPNamespace::DiscoInfo);
discoItems = !(session || discoInfo) && (t == XMPPNamespace::DiscoItems);
if (!(session || discoInfo || discoItems))
return false;
}
// No disco: check 'to' resource
if (!(discoInfo || discoItems || (to.resource() && handleResource(to.resource()))))
return false;
XmlElement* xml = XMPPUtils::getXml(msg,"xml",0);
if (!xml) {
DDebug(this,DebugAll,"handleJabberIq() no xml element");
return false;
}
JabberID from(msg.getValue("from"));
if (!from.resource())
from.resource(msg.getValue("from_instance"));
DDebug(this,DebugAll,"handleJabberIq() from=%s to=%s xmlns=%s",
from.c_str(),to.c_str(),xmlns);
if (discoInfo || discoItems) {
XmlElement* rsp = 0;
const char* id = msg.getValue("id");
XmlElement* query = XMPPUtils::findFirstChild(*xml,XmlTag::Query);
String node = query ? query->attribute("node") : 0;
if (TelEngine::null(node)) {
if (discoInfo)
rsp = m_features.buildDiscoInfo(0,0,id);
else
rsp = XMPPUtils::createIqDisco(false,false,0,0,id);
}
else {
// Disco info to our node#hash
if (discoInfo) {
if (node == s_capsNode)
rsp = m_features.buildDiscoInfo(0,0,id,node);
else {
int pos = node.find("#");
if (pos > 0 && node.substr(0,pos) == s_capsNode &&
node.substr(pos + 1) == m_features.m_entityCapsHash)
rsp = m_features.buildDiscoInfo(0,0,id,node);
}
}
if (!rsp)
rsp = XMPPUtils::createIqDisco(discoInfo,false,0,0,id,node);
}
TelEngine::destruct(xml);
msg.setParam(new NamedPointer("response",rsp));
return true;
}
XMPPError::Type error = XMPPError::NoError;
String text;
const String* id = msg.getParam("id");
bool ok = s_jingle->acceptIq(t,from,to,id ? *id : String::empty(),xml,
msg.getValue("line"),error,text);
if (ok || error != XMPPError::NoError) {
msg.setParam("respond",String::boolText(!ok));
if (!ok) {
xml = XMPPUtils::createIqError(0,0,xml,XMPPError::TypeModify,error,text);
msg.setParam(new NamedPointer("response",xml));
}
return true;
}
// Put back the xml into the message
msg.setParam(new NamedPointer("xml",xml));
return false;
}
// Handle resource.notify messages
bool YJGDriver::handleResNotify(Message& msg)
{
String* oper = msg.getParam("operation");
if (TelEngine::null(oper))
return false;
// online/offline
bool online = (*oper == "update" || *oper == "online");
if (online || *oper == "delete" || *oper == "offline") {
JabberID remote(msg.getValue("contact"));
// Add jingle caps for serviced domains if requested
if (msg.getBoolValue("addjinglecaps") && handleDomain(remote.domain())) {
XmlElement* xml = YOBJECT(XmlElement,msg.getParam("xml"));
String* data = !xml ? msg.getParam("data") : 0;
XmlElement* dataXml = data ? XMPPUtils::getXml(*data) : 0;
if (xml || dataXml) {
XmlElement* target = xml ? xml : dataXml;
// Add entity caps if not already there
if (!XMPPUtils::findFirstChild(*target,XmlTag::EntityCapsTag,
XMPPNamespace::EntityCaps)) {
target->addChild(new XmlElement(*m_entityCaps));
target->addChild(XMPPUtils::createEntityCapsGTalkV1(s_capsNode));
// Restore the data parameter
if (dataXml) {
data->clear();
dataXml->toString(*data);
}
msg.clearParam("addjinglecaps");
}
TelEngine::destruct(dataXml);
}
}
JabberID local;
if (remote)
remote.resource(msg.getValue("instance"));
else {
local.set(msg.getValue("to"));
Lock lock(this);
if (!handleDomain(local.domain()))
return false;
lock.drop();
if (!local.resource())
local.resource(msg.getValue("to_instance"));
remote.set(msg.getValue("from"));
if (!remote.resource())
remote.resource(msg.getValue("from_instance"));
}
DDebug(this,DebugAll,"handleResNotify(%u) from=%s to=%s",
online,remote.c_str(),local.c_str());
if (!remote)
return false;
if (online) {
if (!remote.resource())
return false;
Lock lock(this);
for (ObjList* o = channels().skipNull(); o; o = o->skipNext()) {
YJGConnection* conn = static_cast<YJGConnection*>(o->get());
if (conn->state() != YJGConnection::Pending)
continue;
if (remote.bare() != conn->remote().bare())
continue;
if (!local || conn->local().match(local)) {
conn->updateResource(remote.resource());
if (conn->presenceChanged(true,&msg))
conn->disconnect(0);
}
}
return false;
}
// Offline
// Remote user is unavailable: notify all connections
// Remote has no resource: match connections by bare jid
Lock lock(this);
for (ObjList* o = channels().skipNull(); o; o = o->skipNext()) {
YJGConnection* conn = static_cast<YJGConnection*>(o->get());
if (conn->remote().match(remote) && (!local ||
local.bare() != conn->local().bare())) {
if (conn->presenceChanged(false))
conn->disconnect(0);
}
}
return false;
}
String* src = msg.getParam("from");
String* dest = msg.getParam("to");
if (TelEngine::null(src) || TelEngine::null(dest))
return false;
// (un)subscribed
bool sub = (*oper == "subscribed");
if (sub || *oper == "unsubscribed") {
// We are not interested in 'unsubscribed'
if (!sub)
return false;
return false;
}
// probe
if (*oper == "probe") {
if (!s_autoSubscribe)
return false;
JabberID to(msg.getValue("to"));
if (!to || !s_serverMode || !handleDomain(to.domain()))
return false;
DDebug(this,DebugAll,"handleResNotify(probe) from=%s to=%s",
msg.getValue("from"),to.c_str());
notifyPresence(to,msg.getValue("from"),true);
return false;
}
return false;
}
// Handle resource.subscribe messages
bool YJGDriver::handleResSubscribe(Message& msg)
{
if (!s_autoSubscribe)
return false;
String* oper = msg.getParam("operation");
if (TelEngine::null(oper))
return false;
bool sub = (*oper == "subscribe");
if (!sub && *oper != "unsubscribe")
return false;
JabberID notifier(msg.getValue("notifier"));
if (!notifier || !s_serverMode || !handleDomain(notifier.domain()))
return false;
JabberID subscriber(msg.getValue("subscriber"));
if (!subscriber)
return false;
subscriber.resource();
DDebug(this,DebugAll,"handleResSubscribe(%s) from %s to %s",oper->c_str(),
subscriber.c_str(),notifier.c_str());
Message* m = message("resource.notify");
m->addParam("from",notifier.bare());
m->addParam("to",subscriber.bare());
m->addParam("operation",sub ? "subscribed" : "unsubscribed");
bool ok = Engine::enqueue(m);
if (ok)
notifyPresence(notifier,subscriber,sub);
return ok;
}
// Handle user.notify messages
bool YJGDriver::handleUserNotify(Message& msg)
{
if (!Engine::clientMode() || msg.getBoolValue("registered"))
return false;
// Local account is offline: disconnect it
JabberID jid(msg.getValue("jid"));
DDebug(this,DebugAll,"handleUserNotify(offline) jid=%s",jid.c_str());
Lock lock(this);
for (ObjList* o = channels().skipNull(); o; o = o->skipNext()) {
YJGConnection* conn = static_cast<YJGConnection*>(o->get());
if (jid == conn->local())
conn->disconnect("unregistered");
}
return false;
}
// Handle chan.notify messages
bool YJGDriver::handleChanNotify(Message& msg)
{
String* chan = msg.getParam("notify");
YJGConnection* ch = chan ? findChan(*chan) : 0;
if (!ch)
return false;
ch->processChanNotify(msg);
if (ch->state() == YJGConnection::Terminated)
ch->disconnect(0);
TelEngine::destruct(ch);
return true;
}
// Handle msg.execute message
// Send chan.text message if enabled
bool YJGDriver::handleImExecute(Message& msg)
{
if (!s_imToChanText)
return false;
// Set local (target) from callto/called parameter
JabberID local;
String* callto = msg.getParam("callto");
if (TelEngine::null(callto))
local.set(msg.getValue("called"));
else if (callto->startsWith(prefix()))
local.set(callto->substr(prefix().length()));
else
return false;
if (!local)
return false;
if (!local.resource())
local.resource(msg.getValue("called_instance"));
Message* m = 0;
Lock lock(this);
// Check if target is in our domain(s)
if (!(local.node() && handleDomain(local.domain())))
return false;
JabberID remote(msg.getValue("caller"));
if (!remote.resource())
remote.resource(msg.getValue("caller_resource"));
if (!remote)
return false;
// NOTE: broadcast chat to all channels matching the bare jid if local resource is empty ?
YJGConnection* conn = findByJid(local,remote);
if (conn) {
DDebug(this,DebugInfo,"Found conn=(%p,%s) for message from=%s to=%s",
conn,conn->debugName(),remote.c_str(),local.c_str());
m = conn->message("chan.text");
}
lock.drop();
if (m) {
m->addParam("text",msg.getValue("body"));
Engine::enqueue(m);
}
return m != 0;
}
// Handle engine.start message
void YJGDriver::handleEngineStart(Message& msg)
{
setDomains(s_cfg.getValue("general","domains"));
}
// Find a connection by local and remote jid, optionally ignore local
// resource (always ignore if local has no resource)
YJGConnection* YJGDriver::findByJid(const JabberID& local, const JabberID& remote,
bool anyResource)
{
if (local.bare() == local)
anyResource = true;
ObjList* obj = channels().skipNull();
for (; obj; obj = obj->skipNext()) {
YJGConnection* conn = static_cast<YJGConnection*>(obj->get());
if (!conn->remote().match(remote))
continue;
if (anyResource) {
if (local.bare() == conn->local().bare())
return conn;
}
else if (conn->local().match(local))
return conn;
}
return 0;
}
// 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;
}
// Notify presence
void YJGDriver::notifyPresence(const JabberID& from, const char* to, bool online)
{
if (!from)
return;
Lock lock(this);
if (!handleDomain(from.domain()))
return;
for (ObjList* o = m_resources.skipNull(); o; o = o->skipNext()) {
String* res = static_cast<String*>(o->get());
if (!from.resource() || *res == from.resource()) {
Message* m = message("resource.notify");
m->addParam("from",from.bare());
m->addParam("to",to);
m->addParam("from_instance",*res);
m->addParam("operation",online ? "online" : "offline");
if (online) {
XmlElement* xml = XMPPUtils::createPresence(0,0);
XMPPUtils::setPriority(*xml,String(s_priority));
xml->addChild(XMPPUtils::createEntityCapsGTalkV1(s_capsNode));
xml->addChild(new XmlElement(*m_entityCaps));
m->addParam(new NamedPointer("xml",xml));
}
Engine::enqueue(m);
if (from.resource())
break;
}
}
}
// Build and dispatch a 'jabber.account' message. Returns it on success
Message* YJGDriver::checkAccount(const String& line, bool query,
const JabberID* contact) const
{
if (!line)
return 0;
Message* m = message("jabber.account");
m->addParam("line",line);
if (query)
m->addParam("query",String::boolText(true));
if (contact) {
m->addParam("contact",contact->bare());
if (contact->resource())
m->addParam("instance",contact->resource());
}
if (!Engine::dispatch(m))
TelEngine::destruct(m);
return m;
}
// Update the list of domains
void YJGDriver::setDomains(const String& list)
{
Lock lock(this);
ObjList* l = list.split(',',false);
// Notify the domains not serviced anymore
ObjList* o = m_domains.skipNull();
while (o) {
String* old = static_cast<String*>(o->get());
if (!l->find(*old)) {
for (ObjList* ores = m_resources.skipNull(); ores; ores = ores->skipNext()) {
Message* m = message("jabber.item");
m->addParam("jid",*old + "/" + *static_cast<String*>(ores->get()));
m->addParam("remove",String::boolText(true));
Engine::enqueue(m);
}
o->remove();
o = o->skipNull();
}
else
o = o->skipNext();
}
// Notify the new domains
for (o = l->skipNull(); o; o = o->skipNext()) {
String* d = static_cast<String*>(o->get());
if (m_domains.find(*d))
continue;
m_domains.append(new String(*d));
for (ObjList* ores = m_resources.skipNull(); ores; ores = ores->skipNext()) {
Message* m = message("jabber.item");
m->addParam("jid",*d + "/" + *static_cast<String*>(ores->get()));
Engine::enqueue(m);
}
}
TelEngine::destruct(l);
}
}; // anonymous namespace
/* vi: set ts=8 sw=4 sts=4 noet: */