diff --git a/Makefile.in b/Makefile.in index 20b75d4a..e60175e5 100644 --- a/Makefile.in +++ b/Makefile.in @@ -22,7 +22,7 @@ YLIB := libyate.so.@PACKAGE_VERSION@ SLIBS:= $(YLIB) libyate.so \ libyatesig.so.@PACKAGE_VERSION@ libyatesig.so \ libyatemgcp.so.@PACKAGE_VERSION@ libyatemgcp.so \ - libyatejingle.so.@PACKAGE_VERSION@ libyatejingle.so + libyatejabber.so.@PACKAGE_VERSION@ libyatejabber.so INCS := yateclass.h yatemime.h yatengine.h yatephone.h yatecbase.h GENS := yateversn.h LIBS := diff --git a/conf.d/jabberclient.conf.sample b/conf.d/jabberclient.conf.sample new file mode 100644 index 00000000..23080265 --- /dev/null +++ b/conf.d/jabberclient.conf.sample @@ -0,0 +1,54 @@ +[general] + +; stream_readbuffer: integer: The length of the stream read buffer +; Defaults to 8192 if missing or invalid. Minimum allowed value is 1024 +;stream_readbuffer=8192 + +; stream_parsermaxbuffer: integer: The maximum length of an incomplete xml allowed +; in a stream parser's buffer +; Defaults to 8192 if missing or invalid. Minimum allowed value is 1024 +;stream_parsermaxbuffer=8192 + +; stream_restartcount: integer: The maximum value for stream restart counter +; Defaults to 2 if missing or invalid +; Minimum allowed value is 1, maximum allowed value is 10 +;stream_restartcount=2 + +; stream_restartupdateinterval: integer: The interval, in milliseconds, to increase a +; stream's current restart counter (not exceeding the stream_restartcount value) +; Defaults to 15000 if missing or invalid +; Minimum allowed value is 5000, maximum allowed value is 300000 +;stream_restartupdateinterval=15000 + +; stream_starttimeout: integer: The interval, in milliseconds, allowed for a remote +; party to send the stream start tag +; Defaults to 5000 if missing or invalid +; Minimum allowed value is 1000, maximum allowed value is 10000 +;stream_starttimeout=5000 + +; stream_setuptimeout: integer: Overall stream setup interval in milliseconds. The timer +; will stop when the stream is authenticated +; Defaults to 60000 if missing or invalid +; Minimum allowed value is 5000, maximum allowed value is 120000 +;stream_setuptimeout=60000 + +; stream_connecttimeout: integer: The interval, in milliseconds, allowed for an +; outgoing stream to make a TCP connection to a remote host, including SRV request +; and resolving domain(s) +; Defaults to 5000 if missing or invalid +; Minimum allowed value is 1000, maximum allowed value is 10000 +;stream_connecttimeout=5000 + +; entitycaps: boolean: Enable entity capabilities cache. +; If enabled entity capabilities will be requested and cached each time a presence +; stanza is received +; Defaults to enable +;entitycaps=enable + +; printxml: boolean/string: Print sent/received XML data to output if debug +; level is at least 9 +; Allowed values are boolean values or 'verbose' string +; If verbose is specified, XML elements' children, attributes or text will be +; shown on separate lines +; Defaults to no +;printxml=no diff --git a/conf.d/jabberserver.conf.sample b/conf.d/jabberserver.conf.sample new file mode 100644 index 00000000..c4a7ff82 --- /dev/null +++ b/conf.d/jabberserver.conf.sample @@ -0,0 +1,106 @@ +[general] + +; domains: string: Comma separated list of domains serviced by the server +; This parameter is required +;domains= + +; dialback_secret: string: Dialback key to be used when authenticating with foreign domains +; A random one will be generated if missing +;dialback_secret= + +; restricted_resources: string: Comma separated list of restricted resource names +; Users won't be allowed to use these resources or any other resource name starting +; with it +;restricted_resources= + +; s2s_tlsrequired: boolean: Stream encryption is required on all server to server streams +; Defaults to no +;s2s_tlsrequired= + +; c2s_tlsrequired: boolean: Stream encryption is required on all client to server streams +; Defaults to no +;c2s_tlsrequired= + +; stream_readbuffer: integer: The length of the stream read buffer +; Defaults to 8192 if missing or invalid. Minimum allowed value is 1024 +;stream_readbuffer=8192 + +; stream_parsermaxbuffer: integer: The maximum length of an incomplete xml allowed +; in a stream parser's buffer +; Defaults to 8192 if missing or invalid. Minimum allowed value is 1024 +;stream_parsermaxbuffer=8192 + +; stream_restartcount: integer: The maximum value for stream restart counter +; Defaults to 2 if missing or invalid +; Minimum allowed value is 1, maximum allowed value is 10 +;stream_restartcount=2 + +; stream_restartupdateinterval: integer: The interval, in milliseconds, to increase a +; stream's current restart counter (not exceeding the stream_restartcount value) +; Defaults to 15000 if missing or invalid +; Minimum allowed value is 5000, maximum allowed value is 300000 +;stream_restartupdateinterval=15000 + +; stream_starttimeout: integer: The interval, in milliseconds, allowed for a remote +; party to send the stream start tag +; Defaults to 5000 if missing or invalid +; Minimum allowed value is 1000, maximum allowed value is 10000 +;stream_starttimeout=5000 + +; stream_setuptimeout: integer: Overall stream setup interval in milliseconds. The timer +; will stop when the stream is authenticated +; Defaults to 60000 if missing or invalid +; Minimum allowed value is 5000, maximum allowed value is 120000 +;stream_setuptimeout=60000 + +; stream_connecttimeout: integer: The interval, in milliseconds, allowed for an +; outgoing stream to make a TCP connection to a remote host, including SRV request +; and resolving domain(s) +; Defaults to 5000 if missing or invalid +; Minimum allowed value is 1000, maximum allowed value is 10000 +;stream_connecttimeout=5000 + +; entitycaps: boolean: Enable entity capabilities cache. +; If enabled entity capabilities will be requested and cached each time a presence +; stanza is received +; Defaults to enable +;entitycaps=enable + +; printxml: boolean/string: Print sent/received XML data to output if debug +; level is at least 9 +; Allowed values are boolean values or 'verbose' string +; If verbose is specified, XML elements' children, attributes or text will be +; shown on separate lines +; Defaults to no +;printxml=no + + +;[listener name] +; This section configures a connection listener +; This section may repeat to configure more listeners + +; enable: boolean: Enable or disable this listener +; Defaults to false if missing or invalid +;enable= + +; type: string: The type of the expected incoming connection +; This parameter is required +; Allowed values: +; c2s Client to server connection +; s2s Server to server connection +;type= + +; address: string: IP address to listen +; Listen on all available interfaces if empty or invalid +;address= + +; port: integer: The port to listen +; These are the default values for some known types (only if this parameter is missing) +; c2s 5222 +; s2s 5269 +;port= + +; backlog: integer: Maximum length of the queue of pending connections +; Set it to 0 for system maximum +; Defaults to 5 if missing or invalid +;backlog=5 diff --git a/conf.d/jbfeatures.conf.sample b/conf.d/jbfeatures.conf.sample new file mode 100644 index 00000000..0707ad3d --- /dev/null +++ b/conf.d/jbfeatures.conf.sample @@ -0,0 +1,96 @@ +[general] + +; account: string: Default database account +; This parameter can be overridden in section using the database +;account= + + +[register] +; This section configures in stream user management (add, change, delete) + +; allow_management: boolean: Enable user add/remove +; Defaults to yes +;allow_management=yes + +; allow_change: boolean: Enable existing authenticated user to change its password +; Defaults to yes +;allow_change=yes + +; allow_unsecure: boolean: Enable user management on unsecure connections +; Defaults to no +;allow_unsecure=no + +; url: string: URL to be sent to the user when requesting registration data +; if in stream registration is not enabled +;url= + +; intructions: string: Instructions to be sent with the URL +;intructions= + + +[vcard] +; User vcard management + +; get: string: Query executed on user vcard get request +;get=SELECT vcard FROM users WHERE username='${username}' + +; set: string: Query executed on user vcard set request +; The query must update the user's vcard +; Care must be taken if the user vcard is kept in the same table with the user: avoid +; inserting items when the user is not found +;set=UPDATE users SET vcard VALUES ('${vcard}') WHERE username='${username}' + +; clear_user: string: Query used to delete an user's vcard +; This query must be set if an user's vcard is not stored in the users table +;clear_user= + + +[private_data] +; User private data management +; User private data chunk key is given by xml tag and namespace +; Data should be kept in a separate table with a one-to-many relationship from the users +; table + +; get: string: Query executed on user private data get request +;get=SELECT xml FROM privatedata WHERE username='${username}' AND tag='${tag}' AND xmlns='${xmlns}' + +; set: string: Query executed on user private data set request +; The query must update existing data or insert it if not found +;set=SELECT * FROM privatedata_set('${username}', '${tag}', '${xmlns}', '${xml}') + +; clear_user: string: Query used to delete all private data belonging to an user +;clear_user=DELETE FROM privatedata WHERE username='${username}' + + +[offline_chat] +; Offline messages management + +; maxcount: integer: Maximum number of chat messages saved for an user +; This parameter can be used to limit the number of chat messages stored on +; behalf of an user +; This parameter is applied on reload +; Defaults to 0 (no limit) if missing or invalid +;maxcount= + +; expires: integer: The time interval (in minutes) a saved chat message +; will be kept in the database +; This parameter can be used to clear old messages +; This parameter is applied on reload +; Defaults to 0 (no limit) if missing or invalid +;expires= + +; expire_query: string: Query used to expire offline messages +;expire_query=DELETE FROM offlinechat WHERE time<'${time}' + +; get: string: Query used to retrieve an user's offline messages +;get=SELECT * FROM offlinechat WHERE username='${username}' ORDER BY time + +; set: string: Query used to add an offline message addressed to an user +; The query should check if the user exists +; It should return a non 0 integer value indicating success or 0 on failure +; If 'maxcount' is greater then 0 the query should check for maximum +; allowed number of chat messages +;add=SELECT * FROM offlinechat_add('${username}', '${xml}', ${time}, ${maxcount}) + +; clear_user: string: Query used to delete all offline messages belonging to an user +;clear_user=DELETE FROM offlinechat WHERE username='${username}' diff --git a/conf.d/openssl.conf.sample b/conf.d/openssl.conf.sample new file mode 100644 index 00000000..4759fe51 --- /dev/null +++ b/conf.d/openssl.conf.sample @@ -0,0 +1,24 @@ +; This file keeps the configuration of the openssl module +; Each section, except for 'general' configures a server context + +[general] + +[server_context] +; This section configures a SSL server context + +; enable: boolean: Enable or disable the context +; Defaults to yes +;enable=yes + +; domains: string: Comma separated list of domains the context will be used for +; A subdomain wildcard can be specified for a given domain, e.g. +; *.null.ro will match any null.ro subdomains (including the 'null.ro' domain) +;domains= + +; certificate: string: The name of the file containing the certificate for the context +; This parameter is required +;certificate= + +; key: string: Optional certificate key file name +;key= + diff --git a/configure.in b/configure.in index cc18f240..b6144ba8 100644 --- a/configure.in +++ b/configure.in @@ -1104,7 +1104,7 @@ AC_CONFIG_FILES([packing/rpm/yate.spec libs/ysdp/Makefile libs/yiax/Makefile libs/yxml/Makefile - libs/yjingle/Makefile + libs/yjabber/Makefile libs/ymgcp/Makefile libs/ysig/Makefile libs/ypbx/Makefile diff --git a/libs/yjingle/Makefile.in b/libs/yjabber/Makefile.in similarity index 78% rename from libs/yjingle/Makefile.in rename to libs/yjabber/Makefile.in index ede1502b..ee00c18b 100644 --- a/libs/yjingle/Makefile.in +++ b/libs/yjabber/Makefile.in @@ -1,5 +1,5 @@ # Makefile -# This file holds the make rules for the libyatejingle +# This file holds the make rules for the libyatejabber DEBUG := @@ -9,12 +9,12 @@ DEFS := INCLUDES := -I@top_srcdir@ -I../.. -I@srcdir@/../yxml -I@srcdir@ CFLAGS := @CFLAGS@ @MODULE_CPPFLAGS@ @INLINE_FLAGS@ LDFLAGS:= @LDFLAGS@ -L../.. -lyate -INCFILES := @top_srcdir@/yateclass.h @srcdir@/../yxml/tinystr.h @srcdir@/../yxml/tinyxml.h @srcdir@/xmlparser.h @srcdir@/xmpputils.h @srcdir@/yatejabber.h @srcdir@/yatejingle.h +INCFILES := @top_srcdir@/yateclass.h @srcdir@/../yxml/yatexml.h @srcdir@/xmpputils.h @srcdir@/yatejabber.h @srcdir@/yatejingle.h PROGS= -LIBS = libyatejingle.a -OBJS = xmlparser.o xmpputils.o jbstream.o jbengine.o session.o jgengine.o -LIBD_DEV:= libyatejingle.so +LIBS = libyatejabber.a +OBJS = xmpputils.o jbstream.o jbengine.o session.o jgengine.o +LIBD_DEV:= libyatejabber.so LIBD_VER:= $(LIBD_DEV).@PACKAGE_VERSION@ LIBD:= ../../$(LIBD_VER) ../../$(LIBD_DEV) YXML:= ../yxml/libyatexml.a diff --git a/libs/yjabber/jbengine.cpp b/libs/yjabber/jbengine.cpp new file mode 100644 index 00000000..f43b9603 --- /dev/null +++ b/libs/yjabber/jbengine.cpp @@ -0,0 +1,2271 @@ +/** + * jbengine.cpp + * Yet Another Jabber Component Protocol Stack + * This file is part of the YATE Project http://YATE.null.ro + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +using namespace TelEngine; + + +static unsigned int fixValue(const NamedList& p, const char* param, + unsigned int defVal, unsigned int min, unsigned int max, bool zero = false) +{ + unsigned int val = p.getIntValue(param); + if (!val) { + if (!zero) + val = defVal; + } + else if (val < min) + val = min; + else if (val > max) + val = max; + return val; +} + +const TokenDict JBEvent::s_type[] = { + {"Message", Message}, + {"Presence", Presence}, + {"Iq", Iq}, + {"Terminated", Terminated}, + {"Destroy", Destroy}, + {"Start", Start}, + {"Auth", Auth}, + {"Bind", Bind}, + {"Running", Running}, + {"DbResult", DbResult}, + {"DbVerify", DbVerify}, + {"RegisterOk", RegisterOk}, + {"RegisterFailed", RegisterFailed}, + {"Unknown", Unknown}, + {0,0} +}; + +// Entity caps item tag in document +static const String s_entityCapsItem = "item"; +// Node values used by entity caps +static const String s_googleTalkNode = "http://www.google.com/xmpp/client/caps"; +static const String s_googleMailNode = "http://mail.google.com/xmpp/client/caps"; + +// Stream read buffer +#define JB_STREAMBUF 8192 +#define JB_STREAMBUF_MIN 1024 +// Stream restart counter +#define JB_RESTART_COUNT 2 +#define JB_RESTART_COUNT_MIN 1 +#define JB_RESTART_COUNT_MAX 10 +#define JB_RESTART_UPDATE 15000 +#define JB_RESTART_UPDATE_MIN 5000 +#define JB_RESTART_UPDATE_MAX 300000 +// Stream setup timer +#define JB_SETUP_INTERVAL 60000 +#define JB_SETUP_INTERVAL_MIN 5000 +#define JB_SETUP_INTERVAL_MAX 120000 +// Wait stream start timer +#define JB_START_INTERVAL 5000 +#define JB_START_INTERVAL_MIN 1000 +#define JB_START_INTERVAL_MAX 10000 +// Stream connect timer +#define JB_CONNECT_INTERVAL 5000 +#define JB_CONNECT_INTERVAL_MIN 1000 +#define JB_CONNECT_INTERVAL_MAX 10000 +// Ping +#define JB_PING_INTERVAL 120000 +#define JB_PING_INTERVAL_MIN 60000 +#define JB_PING_INTERVAL_MAX 600000 +#define JB_PING_TIMEOUT 30000 +#define JB_PING_TIMEOUT_MIN 10000 +#define JB_PING_TIMEOUT_MAX JB_PING_INTERVAL_MIN +// Idle +#define JB_IDLE_INTERVAL 43200000 // 12h +#define JB_IDLE_INTERVAL_MIN 3600000 // 1h +#define JB_IDLE_INTERVAL_MAX 86400000 // 24h + + +/* + * SASL + */ +static inline unsigned int findZero(const char* buf, unsigned int max) +{ + if (!buf) + return max + 1; + unsigned int pos = 0; + while (pos < max && buf[pos]) + pos++; + return pos; +} + +// Parse and decode a buffer containing SASL plain authentication data +// See RFC 4616 Section 2 +// Format: [authzid] UTF8NUL username UTF8NUL passwd +// Each token must be up to 255 bytes length +static NamedList* splitPlainSasl(const DataBlock& buf) +{ + const char* d = (const char*)buf.data(); + unsigned int len = buf.length(); + if (!len) + return 0; + String user, pwd, authzid; + // Use a while to break to the end + bool ok = false; + while (true) { + // authzid + unsigned int ll = findZero(d,len); + if (ll && (ll > 255 || ll > len)) + break; + authzid.assign(d,ll); + if (-1 == authzid.lenUtf8()) + break; + d += ll; + len -= ll; + // Username + if (d[0] || len < 2) + break; + ll = findZero(++d,--len); + if (!(ll && ll < len && ll < 256)) + break; + user.assign(d,ll); + if (-1 == user.lenUtf8()) + break; + d += ll; + len -= ll; + // Password + if (d[0] || len < 2) + break; + ll = findZero(++d,--len); + if (ll != len || ll > 255) + break; + pwd.assign(d,ll); + ok = (-1 != pwd.lenUtf8()); + break; + } + if (!ok) + return 0; + NamedList* result = new NamedList(""); + result->addParam("username",user); + result->addParam("response",pwd); + if (authzid) + result->addParam("authzid",authzid); + return result; +} + +static NamedList* splitDigestSasl(const String& buf) +{ + const char* d = buf.c_str(); + unsigned int len = buf.length(); + NamedList* result = 0; + while (len) { + // Find '=' + unsigned int i = 0; + while (i < len && d[i] != '=') + i++; + if (!i || i >= len) { + Debug(DebugNote,"splitDigestSasl() unexpected end of buffer '%s'",d); + break; + } + // Get param name and skip over '=' + String name(d,i); + i++; + d += i; + len -= i; + XDebug(DebugAll,"splitDigestSasl() found directive='%s' rest='%s' len=%u", + name.c_str(),d,len); + String value; + if (len) { + // Find ',', handle quoted parameters + if (*d == '\"') { + if (len < 2) { + Debug(DebugNote, + "splitDigestSasl() unexpected end of buffer '%s'",d); + break; + } + // Find an unescaped " + for (i = 1; i < len; i++) { + if (d[i] == '"' && d[i-1] != '\\') + break; + } + if (i == len) { + Debug(DebugNote,"splitDigestSasl() unclosed '\"' found at %u", + buf.length() - len); + break; + } + // Unescape the content + value.assign(d + 1,i - 1); + int pos = -1; + unsigned int start = 0; + bool ok = true; + while (-1 != (value.find('\\',start))) { + if (pos == 0) { + // No character to escape: error + if (value.length() == 1) { + Debug(DebugNote,"splitDigestSasl() 2"); + ok = false; + break; + } + value = value.substr(1); + } + else if ((unsigned int)pos < value.length() - 1) { + if (value[pos - 1] != '"') { + // Escaped char + value = value.substr(0,pos) + value.substr(0,pos + 1); + start = pos + 1; + } + else if (value[pos + 1] == '"') { + // Escaped backslash + value = value.substr(0,pos - 1) + "\\" + value.substr(0,pos + 2); + start = pos + 1; + } + else { + // Error + Debug(DebugNote,"splitDigestSasl() 3"); + ok = false; + break; + } + } + else { + // No character to escape: error + Debug(DebugNote,"splitDigestSasl() 4"); + ok = false; + break; + } + } + if (!ok) + break; + // Adjust buffer and length + if (i < len) { + if (i == len - 1) + i++; + else if (d[i + 1] == ',') + i += 2; + else { + Debug(DebugNote,"splitDigestSasl() ',' not found at %u rest=%s", + buf.length() - len + i + 1,d); + break; + } + } + } + else { + // Skip until , + for (i = 0; i < len && d[i] != ','; i++) + ; + if (i) + value.assign(d,i); + if (i < len) + i++; + } + d += i; + len -= i; + } + if (!result) + result = new NamedList(""); + XDebug(DebugAll,"splitDigestSasl() found '%s'='%s' rest='%s' len=%u", + name.c_str(),value.c_str(),d,len); + result->addParam(name,value); + } + if (len) + TelEngine::destruct(result); + return result; +} + +// Apend a quoted directive to a string +// Escape the value +static inline void appendQDirective(String& buf, const String& name, + const String& value) +{ + if (-1 == value.find('\"') && -1 == value.find('\\')) { + buf.append(name + "=\"" + value + "\"",","); + return; + } + // Replace \ with "\" and " with \" + // See RFC2831 7.2 + String tmp; + char c = 0; + char* s = (char*)value.c_str(); + while ((c = *s++)) { + if (c == '\"') + tmp << '\\' << c; + else if (c == '\\') + tmp << "\"\\\""; + else + tmp += c; + } + buf.append(name + "=\"" + tmp + "\"",","); +} + +// Build a SASL nonce count string +static inline void buildNc(String& buf, int nc) +{ + char tmp[9]; + ::sprintf(tmp,"%08x",nc); + buf = tmp; +} + +// Constructor +SASL::SASL(bool plain, const char* realm) + : m_plain(plain), m_params(0), m_realm(realm), m_nonceCount(0) +{ +} + +// Set auth params +void SASL::setAuthParams(const char* user, const char* pwd) +{ + if (TelEngine::null(user) && TelEngine::null(pwd)) + return; + if (!m_params) + m_params = new NamedList(""); + if (!TelEngine::null(user)) + m_params->setParam("username",user); + if (!TelEngine::null(pwd)) + m_params->setParam("password",pwd); +} + +// Build an auth response +bool SASL::buildAuthRsp(String& buf, const char* digestUri) +{ + if (!m_params) + return false; + + // Plain. See RFC 4616 Section 2 + // Format: [authzid] UTF8NUL username UTF8NUL passwd + // Each token must be up to 255 bytes length + if (m_plain) { + if (!m_params) + return false; + String* user = m_params->getParam("username"); + String* pwd = m_params->getParam("password"); + if (!user || user->length() > 255 || !pwd || pwd->length() > 255) + return false; + DataBlock data; + unsigned char nul = 0; + data.append(&nul,1); + data += *user; + data.append(&nul,1); + data += *pwd; + Base64 base64((void*)data.data(),data.length()); + base64.encode(buf); + return true; + } + + // Digest MD5. See RFC 2831 2.1.2.1 + String* pwd = m_params->getParam("password"); + if (!pwd) + return false; + +#define SASL_ADD_QDIR(n) { \ + NamedString* tmp = m_params->getParam(n); \ + if (tmp) \ + appendQDirective(buf,tmp->name(),*tmp); \ +} + SASL_ADD_QDIR("username") + SASL_ADD_QDIR("realm") + SASL_ADD_QDIR("nonce") + MD5 md5(String((unsigned int)::random())); + m_cnonce = md5.hexDigest(); + m_params->setParam("cnonce",m_cnonce); + SASL_ADD_QDIR("cnonce") + m_nonceCount++; + char tmp[9]; + ::sprintf(tmp,"%08x",m_nonceCount); + m_params->setParam("nc",tmp); + SASL_ADD_QDIR("nc") + m_params->setParam("qop","auth"); + SASL_ADD_QDIR("qop") + m_params->setParam("digest-uri",digestUri); + SASL_ADD_QDIR("digest-uri") + String rsp; + buildMD5Digest(rsp,*pwd); + buf << ",response=" << rsp; + SASL_ADD_QDIR("charset") + SASL_ADD_QDIR("md5-sess") + XDebug(DebugAll,"SASL built MD5 response %s [%p]",buf.c_str(),this); +#undef SASL_ADD_QDIR + Base64 base64((void*)buf.c_str(),buf.length()); + buf.clear(); + base64.encode(buf); + return true; +} + +// Build an MD5 challenge from this object +// See RFC 2831 Section 2.1.1 +bool SASL::buildMD5Challenge(String& buf) +{ + String tmp; + if (m_realm) { + if (-1 == m_realm.lenUtf8()) + return false; + appendQDirective(tmp,"realm",m_realm); + } + // Re-build nonce. Increase nonce count + m_nonce.clear(); + m_nonce << (int)Time::msecNow() << (int)::random(); + MD5 md5(m_nonce); + m_nonce = md5.hexDigest(); + m_nonceCount++; + tmp.append("nonce=\"" + m_nonce + "\"",","); + tmp << ",qop=\"auth\""; + tmp << ",charset=\"utf-8\""; + tmp << ",algorithm=\"md5-sess\""; + // RFC 2831 2.1.1: The size of a digest-challenge MUST be less than 2048 bytes + if (tmp.length() < 2048) { + buf = tmp; + return true; + } + m_nonceCount--; + return false; +} + +bool SASL::parsePlain(const DataBlock& buf) +{ +#ifdef XDEBUG + String tmp; + tmp.hexify((void*)buf.data(),buf.length(),' '); + Debug(DebugAll,"SASL::parsePlain() %s [%p]",tmp.c_str(),this); +#endif + TelEngine::destruct(m_params); + m_params = splitPlainSasl(buf); + return m_params != 0; +} + +// Parse and decode a buffer containing a SASL Digest MD5 challenge +bool SASL::parseMD5Challenge(const String& buf) +{ + XDebug(DebugAll,"SASL::parseMD5Challenge() %s [%p]",buf.c_str(),this); + TelEngine::destruct(m_params); + // RFC 2831 2.1.1: The size of a digest-response MUST be less than 2048 bytes + if (buf.length() >= 2048) { + Debug(DebugNote,"SASL::parseMD5Challenge() invalid length=%u (max=2048) [%p]", + buf.length(),this); + return false; + } + m_params = splitDigestSasl(buf); + if (!m_params) { + Debug(DebugNote,"SASL::parseMD5Challenge() failed to split params [%p]", + this); + return false; + } + return true; +} + +// Parse and decode a buffer containing a SASL Digest MD5 response +// See RFC 2831 +bool SASL::parseMD5ChallengeRsp(const String& buf) +{ + XDebug(DebugAll,"SASL::parseMD5ChallengeRsp() %s [%p]",buf.c_str(),this); + TelEngine::destruct(m_params); + // RFC 2831 2.1.2: The size of a digest-response MUST be less than 4096 bytes + if (buf.length() >= 4096) { + Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid length=%u (max=4096) [%p]", + buf.length(),this); + return false; + } + m_params = splitDigestSasl(buf); + if (!m_params) { + Debug(DebugNote,"SASL::parseMD5ChallengeRsp() failed to split params [%p]", + this); + return false; + } + bool ok = false; + // Check realm, nonce, nonce count + // Use a while to break to the end + while (true) { + String* tmp = m_params->getParam("realm"); + if (!tmp || *tmp != m_realm) { + Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid realm='%s' [%p]", + TelEngine::c_safe(tmp),this); + break; + } + tmp = m_params->getParam("nonce"); + if (!tmp || *tmp != m_nonce) { + Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid nonce='%s' [%p]", + TelEngine::c_safe(tmp),this); + break; + } + tmp = m_params->getParam("nc"); + if (!tmp || (unsigned int)tmp->toInteger(0,16) != m_nonceCount) { + Debug(DebugNote,"SASL::parseMD5ChallengeRsp() invalid nonce count='%s' [%p]", + TelEngine::c_safe(tmp),this); + break; + } + ok = true; + break; + } + if (ok) + return true; + TelEngine::destruct(m_params); + return false; +} + +// Build a Digest MD5 SASL to be sent with authentication responses +// See RFC 2831 2.1.2.1 +void SASL::buildMD5Digest(String& dest, const NamedList& params, + const char* password, bool challengeRsp) +{ + const char* nonce = params.getValue("nonce"); + const char* cnonce = params.getValue("cnonce"); + String qop = params.getValue("qop","auth"); + MD5 md5; + md5 << params.getValue("username") << ":" << params.getValue("realm"); + md5 << ":" << password; + MD5 md5A1(md5.rawDigest(),16); + md5A1 << ":" << nonce << ":" << cnonce; + const char* authzid = params.getValue("authzid"); + if (authzid) + md5A1 << ":" << authzid; + MD5 md5A2; + if (challengeRsp) + md5A2 << "AUTHENTICATE"; + md5A2 << ":" << params.getValue("digest-uri"); + if (qop != "auth") + md5A2 << ":" << String('0',32); + MD5 md5Rsp; + md5Rsp << md5A1.hexDigest(); + md5Rsp << ":" << nonce << ":" << params.getValue("nc"); + md5Rsp << ":" << cnonce << ":" << qop << ":" << md5A2.hexDigest(); + dest = md5Rsp.hexDigest(); +} + + +/* + * JBConnect + */ +// Constructor. Add itself to the stream's engine +JBConnect::JBConnect(const JBStream& stream) + : m_domain(stream.remote().domain()), m_port(0), + m_engine(stream.engine()), m_stream(stream.toString()), + m_streamType((JBStream::Type)stream.type()) +{ + stream.connectAddr(m_address,m_port); + if (m_engine) + m_engine->connectStatus(this,true); +} + +// Remove itself from engine +JBConnect::~JBConnect() +{ + terminated(0,true); +} + +// Stop the thread +void JBConnect::stopConnect() +{ + Debug(m_engine,DebugStub,"JBConnect::stopConnect() not implemented!"); +} + +// Retrieve the stream name +const String& JBConnect::toString() const +{ + return m_stream; +} + +// Connect the socket. +void JBConnect::connect() +{ + if (!m_engine) + return; + Debug(m_engine,DebugAll,"JBConnect(%s) starting [%p]",m_stream.c_str(),this); + Socket* sock = new Socket(PF_INET,SOCK_STREAM); + // Try to use ip/port + int port = m_port; + if (!port) + port = (m_streamType == JBStream::c2s) ? XMPP_C2S_PORT : XMPP_S2S_PORT; + if (!TelEngine::null(m_address) && connect(sock,m_address,port)) { + terminated(sock,false); + return; + } + if (exiting(sock)) + return; + // Try to use the domain + if (!m_domain) { + delete sock; + terminated(0,false); + return; + } + // Get SRV records from remote party + String query; + if (m_streamType == JBStream::c2s) + query = "_xmpp-client._tcp."; + else + query = "_xmpp-server._tcp."; + query << m_domain; + ObjList srv; + int code = Resolver::srvQuery(query,srv); + if (exiting(sock)) + return; + if (!code) { + DDebug(m_engine,DebugAll,"JBConnect(%s) SRV query for '%s' got %u records [%p]", + m_stream.c_str(),query.c_str(),srv.count(),this); + for (ObjList* o = srv.skipNull(); o; o = o->skipNext()) { + SrvRecord* rec = static_cast(o->get()); + if (connect(sock,*rec,rec->m_port)) { + terminated(sock,false); + return; + } + if (exiting(sock)) + return; + } + } + else { + String s; + Thread::errorString(s,code); + Debug(m_engine,DebugNote,"JBConnect(%s) SRV query for '%s' failed: %d '%s' [%p]", + m_stream.c_str(),query.c_str(),code,s.c_str(),this); + } + // Try to resolve the domain + if (connect(sock,m_domain,port)) { + terminated(sock,false); + return; + } + delete sock; + terminated(0,false); +} + +// Connect a socket +bool JBConnect::connect(Socket*& sock, const char* addr, int port) +{ + SocketAddr a(PF_INET); + a.host(addr); + a.port(port); + // Check exiting: it may take some time to resolve the domain + if (exiting(sock)) + return false; + if (!a.host()) { + Debug(m_engine,DebugNote,"JBConnect(%s) failed to resolve '%s' [%p]", + m_stream.c_str(),addr,this); + return false; + } + DDebug(m_engine,DebugAll,"JBConnect(%s) attempt to connect to '%s:%d' (%s) [%p]", + m_stream.c_str(),a.host().c_str(),a.port(),addr,this); + bool ok = (0 != sock->connect(a)); + if (ok) + DDebug(m_engine,DebugAll,"JBConnect(%s) connected to '%s:%d' (%s) [%p]", + m_stream.c_str(),a.host().c_str(),a.port(),addr,this); + else { + String tmp; + Thread::errorString(tmp,sock->error()); + Debug(m_engine,DebugNote, + "JBConnect(%s) failed to connect to '%s:%d' (%s). %d '%s' [%p]", + m_stream.c_str(),a.host().c_str(),a.port(),addr,sock->error(),tmp.c_str(),this); + } + return ok; +} + +// Check if exiting. Release socket +bool JBConnect::exiting(Socket*& sock) +{ + bool done = Thread::check(false) || !m_engine || m_engine->exiting(); + if (done && sock) { + delete sock; + sock = 0; + } + return done; +} + +// Notify termination, remove from engine +void JBConnect::terminated(Socket* sock, bool final) +{ + bool done = exiting(sock); + JBEngine* engine = m_engine; + m_engine = 0; + // Remove from engine + if (engine) + engine->connectStatus(this,false); + if (done) + return; + if (!final) + Debug(engine,DebugAll,"JBConnect stream='%s' terminated [%p]", + m_stream.c_str(),this); + else + Debug(engine,DebugWarn,"JBConnect stream='%s' abnormally terminated! [%p]", + m_stream.c_str(),this); + // Find the stream and notify it + JBStream* stream = engine->findStream(m_stream,m_streamType); + if (stream) { + stream->connectTerminated(sock); + TelEngine::destruct(stream); + } + else { + if (sock) + delete sock; + DDebug(engine,DebugInfo,"JBConnect stream='%s' vanished while connecting [%p]", + m_stream.c_str(),this); + } +} + + +/* + * JBEngine + */ +JBEngine::JBEngine(const char* name) + : Mutex(true,"JBEngine"), + m_exiting(false), + m_restartMax(JB_RESTART_COUNT), m_restartUpdInterval(JB_RESTART_UPDATE), + m_setupTimeout(JB_SETUP_INTERVAL), m_startTimeout(JB_START_INTERVAL), + m_connectTimeout(JB_CONNECT_INTERVAL), + m_pingInterval(JB_PING_INTERVAL), m_pingTimeout(JB_PING_TIMEOUT), + m_idleTimeout(0), + m_streamReadBuffer(JB_STREAMBUF), m_maxIncompleteXml(XMPP_MAX_INCOMPLETEXML), + m_printXml(0), m_initialized(false) +{ + debugName(name); + XDebug(this,DebugAll,"JBEngine [%p]",this); +} + +JBEngine::~JBEngine() +{ + XDebug(this,DebugAll,"~JBEngine [%p]",this); +} + +// Cleanup streams. Stop all threads owned by this engine. Release memory +void JBEngine::destruct() +{ + cleanup(true,false); + GenObject::destruct(); +} + +// Initialize the engine's parameters +void JBEngine::initialize(const NamedList& params) +{ + int lvl = params.getIntValue("debug_level",-1); + if (lvl != -1) + debugLevel(lvl); + String tmp = params.getValue("printxml"); + m_printXml = tmp.toBoolean() ? -1: ((tmp == "verbose") ? 1 : 0); + + m_streamReadBuffer = fixValue(params,"stream_readbuffer", + JB_STREAMBUF,JB_STREAMBUF_MIN,(unsigned int)-1); + m_maxIncompleteXml = fixValue(params,"stream_parsermaxbuffer", + XMPP_MAX_INCOMPLETEXML,1024,(unsigned int)-1); + m_restartMax = fixValue(params,"stream_restartcount", + JB_RESTART_COUNT,JB_RESTART_COUNT_MIN,JB_RESTART_COUNT_MAX); + m_restartUpdInterval = fixValue(params,"stream_restartupdateinterval", + JB_RESTART_UPDATE,JB_RESTART_UPDATE_MIN,JB_RESTART_UPDATE_MAX); + m_setupTimeout = fixValue(params,"stream_setuptimeout", + JB_SETUP_INTERVAL,JB_SETUP_INTERVAL_MIN,JB_SETUP_INTERVAL_MAX); + m_startTimeout = fixValue(params,"stream_starttimeout", + JB_START_INTERVAL,JB_START_INTERVAL_MIN,JB_START_INTERVAL_MAX); + m_connectTimeout = fixValue(params,"stream_connecttimeout", + JB_CONNECT_INTERVAL,JB_CONNECT_INTERVAL_MIN,JB_CONNECT_INTERVAL_MAX); + m_pingInterval = fixValue(params,"stream_pinginterval", + JB_PING_INTERVAL,JB_PING_INTERVAL_MIN,JB_PING_INTERVAL_MAX); + m_pingTimeout = fixValue(params,"stream_pingtimeout", + JB_PING_TIMEOUT,JB_PING_TIMEOUT_MIN,JB_PING_TIMEOUT_MAX); + m_idleTimeout = fixValue(params,"stream_idletimeout", + JB_IDLE_INTERVAL,JB_IDLE_INTERVAL_MIN,JB_IDLE_INTERVAL_MAX,true); + m_initialized = true; +} + +// Terminate all streams +void JBEngine::cleanup(bool final, bool waitTerminate) +{ + DDebug(this,DebugAll,"JBEngine::cleanup() final=%s wait=%s", + String::boolText(final),String::boolText(waitTerminate)); + dropAll(JBStream::TypeCount,JabberID::empty(),JabberID::empty(), + XMPPError::Shutdown); + lock(); + ObjList* found = m_connect.skipNull(); + if (found) { + Debug(this,DebugAll,"Terminating %u stream connect threads",m_connect.count()); + for (ObjList* o = found; o; o = o->skipNext()) { + JBConnect* conn = static_cast(o->get()); + XDebug(this,DebugAll,"Terminating connect thread (%p)",conn); + conn->stopConnect(); + } + } + unlock(); + if (found) { + XDebug(this,DebugAll,"Waiting for stream connect threads to terminate"); + while (found) { + Thread::yield(false); + Lock lock(this); + found = m_connect.skipNull(); + } + Debug(this,DebugAll,"Stream connect threads terminated"); + } + stopStreamSets(waitTerminate); +} + +// Accept an incoming stream connection. Build a stream +bool JBEngine::acceptConn(Socket* sock, SocketAddr& remote, JBStream::Type t) +{ + if (!sock) + return false; + if (exiting()) { + Debug(this,DebugNote, + "Can't accept connection from '%s:%d' type='%s': engine is exiting", + remote.host().c_str(),remote.port(),lookup(t,JBStream::s_typeName)); + return false; + } + JBStream* s = 0; + if (t == JBStream::c2s) + s = new JBClientStream(this,sock); + else if (t == JBStream::s2s) + s = new JBServerStream(this,sock); + if (s) + addStream(s); + else + Debug(this,DebugNote,"Can't accept connection from '%s:%d' type='%s'", + remote.host().c_str(),remote.port(),lookup(t,JBStream::s_typeName)); + return s != 0; +} + +// Find a stream by its name +JBStream* JBEngine::findStream(const String& id, JBStream::Type hint) +{ + if (!id) + return 0; + RefPointer list[JBStream::TypeCount]; + getStreamLists(list,hint); + for (unsigned int i = 0; i < JBStream::TypeCount; i++) { + if (!list[i]) + continue; + JBStream* stream = JBEngine::findStream(id,list[i]); + if (stream) { + for (; i < JBStream::TypeCount; i++) + list[i] = 0; + return stream; + } + list[i] = 0; + } + return 0; +} + +// Find all c2s streams whose local or remote bare jid matches a given one +ObjList* JBEngine::findClientStreams(bool in, const JabberID& jid, int flags) +{ + if (!jid.node()) + return 0; + RefPointer list; + getStreamList(list,JBStream::c2s); + if (!list) + return 0; + ObjList* result = 0; + list->lock(); + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + JBClientStream* stream = static_cast(s->get()); + // Ignore destroying streams + if (stream->incoming() != in || stream->state() == JBStream::Destroy) + continue; + Lock lock(stream); + const JabberID& s = in ? stream->remote() : stream->local(); + if (s.bare() == jid.bare() && stream->flag(flags) && stream->ref()) { + if (!result) + result = new ObjList; + result->append(stream); + } + } + } + list->unlock(); + list = 0; + return result; +} + +// Find all c2s streams whose local or remote bare jid matches a given one and +// their resource is found in the given list +ObjList* JBEngine::findClientStreams(bool in, const JabberID& jid, const ObjList& resources, + int flags) +{ + if (!jid.node()) + return 0; + RefPointer list; + getStreamList(list,JBStream::c2s); + if (!list) + return 0; + ObjList* result = 0; + list->lock(); + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + JBClientStream* stream = static_cast(s->get()); + // Ignore destroying streams + if (stream->incoming() != in || stream->state() == JBStream::Destroy) + continue; + Lock lock(stream); + const JabberID& s = in ? stream->remote() : stream->local(); + if (s.bare() == jid.bare() && resources.find(s.resource()) && + stream->flag(flags) && stream->ref()) { + if (!result) + result = new ObjList; + result->append(stream); + } + } + } + list->unlock(); + list = 0; + return result; +} + +// Find a c2s stream by its local or remote jid +JBClientStream* JBEngine::findClientStream(bool in, const JabberID& jid) +{ + if (!jid.node()) + return 0; + RefPointer list; + getStreamList(list,JBStream::c2s); + if (!list) + return 0; + JBClientStream* found = 0; + list->lock(); + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + found = static_cast(s->get()); + // Ignore destroying streams + if (found->incoming() != in || found->state() == JBStream::Destroy) + continue; + Lock lock(found); + const JabberID& s = in ? found->remote() : found->local(); + if (s == jid && found->ref()) + break; + found = 0; + } + if (found) + break; + } + list->unlock(); + list = 0; + return found; +} + +// Terminate all streams matching type and/or local/remote jid +unsigned int JBEngine::dropAll(JBStream::Type type, const JabberID& local, + const JabberID& remote, XMPPError::Type error, const char* reason) +{ + XDebug(this,DebugInfo,"dropAll(%s,%s,%s,%s,%s)",lookup(type,JBStream::s_typeName), + local.c_str(),remote.c_str(),XMPPUtils::s_error[error].c_str(),reason); + RefPointer list[JBStream::TypeCount]; + getStreamLists(list,type); + unsigned int n = 0; + for (unsigned int i = 0; i < JBStream::TypeCount; i++) { + if (!list[i]) + continue; + list[i]->lock(); + for (ObjList* o = list[i]->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + n += set->dropAll(local,remote,error,reason); + } + list[i]->unlock(); + list[i] = 0; + } + DDebug(this,DebugInfo, + "Dropped %u streams type=%s local=%s remote=%s error=%s reason=%s", + n,lookup(type,JBStream::s_typeName),local.c_str(),remote.c_str(), + XMPPUtils::s_error[error].c_str(),reason); + return n; +} + +// Process an event +void JBEngine::processEvent(JBEvent* ev) +{ + Debug(this,DebugStub,"JBEngine::processEvent() not implemented!"); + returnEvent(ev); +} + +// Return an event to this engine +void JBEngine::returnEvent(JBEvent* ev, XMPPError::Type error, const char* reason) +{ + if (!ev) + return; + // Send error when supported + if (error != XMPPError::NoError) + ev->sendStanzaError(error,reason); + XDebug(this,DebugAll,"Deleting returned event (%p,%s)",ev,ev->name()); + TelEngine::destruct(ev); +} + +// Start stream TLS +void JBEngine::encryptStream(JBStream* stream) +{ + Debug(this,DebugStub,"JBEngine::encryptStream() not implemented!"); +} + +// Connect an outgoing stream +void JBEngine::connectStream(JBStream* stream) +{ + Debug(this,DebugStub,"JBEngine::connectStream() not implemented!"); +} + +// Build a dialback key +void JBEngine::buildDialbackKey(const String& id, String& key) +{ + Debug(this,DebugStub,"JBEngine::buildDialbackKey() not implemented!"); +} + +// Check for duplicate stream id at a remote server +bool JBEngine::checkDupId(const JBStream* stream) +{ + if (!stream || stream->incoming()) + return false; + RefPointer list; + getStreamList(list,stream->type()); + if (!list) + return false; + list->lock(); + JBStream* found = 0; + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + found = static_cast(s->get()); + if (found != stream && found->outgoing()) { + // Lock the stream: its data might change + Lock lock(found); + // Ignore destroying streams + if (found->remote() == stream->remote() && + found->id() == stream->id() && found->state() != JBStream::Destroy) + break; + } + found = 0; + } + if (found) + break; + } + list->unlock(); + list = 0; + return found != 0; +} + +// Print XML to output +void JBEngine::printXml(const JBStream* stream, bool send, XmlChild& xml) const +{ + if (!(m_printXml && debugAt(DebugInfo))) + return; + String s; + XMPPUtils::print(s,xml,m_printXml > 0); + const char* dir = send ? "Sending to" : "Receiving from"; + if (m_printXml < 0) + Debug(stream,DebugInfo,"%s '%s' %s [%p]",dir,stream->remote().c_str(),s.c_str(),stream); + else + Debug(stream,DebugInfo,"%s '%s' [%p]%s",dir,stream->remote().c_str(),stream,s.c_str()); +} + +// Print an XML fragment to output +void JBEngine::printXml(const JBStream* stream, bool send, XmlFragment& frag) const +{ + if (!(m_printXml && debugAt(DebugInfo))) + return; + String s; + for (ObjList* o = frag.getChildren().skipNull(); o; o = o->skipNext()) + XMPPUtils::print(s,*static_cast(o->get()),m_printXml > 0); + const char* dir = send ? "Sending to" : "Receiving from"; + if (m_printXml < 0) + Debug(stream,DebugInfo,"%s '%s' %s [%p]",dir,stream->remote().c_str(),s.c_str(),stream); + else + Debug(stream,DebugInfo,"%s '%s' [%p]%s",dir,stream->remote().c_str(),stream,s.c_str()); +} + +// Add a stream to one of the stream lists +void JBEngine::addStream(JBStream* stream) +{ + Debug(this,DebugStub,"JBEngine::addStream() not implemented!"); +} + +// Remove a stream +void JBEngine::removeStream(JBStream* stream, bool delObj) +{ + if (!stream) + return; + lock(); + ObjList* o = m_connect.find(stream->toString()); + if (o) { + JBConnect* conn = static_cast(o->get()); + Debug(this,DebugAll,"Stopping stream connect thread (%p,%s)", + conn,conn->toString().c_str()); + conn->stopConnect(); + } + unlock(); +} + +// Find a stream by its name in a given set list +JBStream* JBEngine::findStream(const String& id, JBStreamSetList* list) +{ + if (!list) + return 0; + Lock lock(list); + ObjList* found = 0; + for (ObjList* o = list->sets().skipNull(); !found && o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + found = set->clients().find(id); + } + JBStream* stream = found ? static_cast(found->get()) : 0; + if (stream && !stream->ref()) + stream = 0; + return stream; +} + +// Add/remove a connect stream thread when started/stopped +void JBEngine::connectStatus(JBConnect* conn, bool started) +{ + if (!conn) + return; + Lock lock(this); + if (started) { + m_connect.append(conn)->setDelete(false); + DDebug(this,DebugAll,"Added stream connect thread (%p)",conn); + } + else { + GenObject* o = m_connect.remove(conn,false); + if (o) + DDebug(this,DebugAll,"Removed stream connect thread (%p)",conn); + } +} + + +/* + * JBServerEngine + */ +JBServerEngine::JBServerEngine(const char* name) + : JBEngine(name), + m_streamIndex(0), + m_c2sReceive(0), m_c2sProcess(0), m_s2sReceive(0), m_s2sProcess(0) +{ +} + +JBServerEngine::~JBServerEngine() +{ +} + +// Terminate all streams +void JBServerEngine::cleanup(bool final, bool waitTerminate) +{ + JBEngine::cleanup(final,waitTerminate); + DDebug(this,DebugAll,"JBServerEngine::cleanup() final=%s wait=%s", + String::boolText(final),String::boolText(waitTerminate)); + if (!final) + return; + Lock lock(this); + TelEngine::destruct(m_c2sReceive); + TelEngine::destruct(m_c2sProcess); + TelEngine::destruct(m_s2sReceive); + TelEngine::destruct(m_s2sProcess); +} + +// Stop all stream sets +void JBServerEngine::stopStreamSets(bool waitTerminate) +{ + XDebug(this,DebugAll,"JBServerEngine::stopStreamSets() wait=%s", + String::boolText(waitTerminate)); + lock(); + RefPointer c2sReceive = m_c2sReceive; + RefPointer c2sProcess = m_c2sProcess; + RefPointer s2sReceive = m_s2sReceive; + RefPointer s2sProcess = m_s2sProcess; + unlock(); + if (c2sReceive) + c2sReceive->stop(0,waitTerminate); + if (c2sProcess) + c2sProcess->stop(0,waitTerminate); + if (s2sReceive) + s2sReceive->stop(0,waitTerminate); + if (s2sProcess) + s2sProcess->stop(0,waitTerminate); + c2sReceive = 0; + c2sProcess = 0; + s2sReceive = 0; + s2sProcess = 0; +} + +// Retrieve the list of streams of a given type +void JBServerEngine::getStreamList(RefPointer& list, int type) +{ + Lock lock(this); + if (type == JBStream::c2s) + list = m_c2sReceive; + else if (type == JBStream::s2s) + list = m_s2sReceive; +} + +// Find a server to server stream by local/remote domain. +// Skip over outgoing dialback streams +JBServerStream* JBServerEngine::findServerStream(const String& local, const String& remote, + bool out) +{ + if (!(local && remote)) + return 0; + lock(); + RefPointer list = m_s2sReceive; + unlock(); + if (!list) + return 0; + JBServerStream* stream = 0; + list->lock(); + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + stream = static_cast(s->get()); + if (out == stream->outgoing() && !stream->dialback()) { + // Lock the stream: remote jid might change + Lock lock(stream); + if (local == stream->local() && remote == stream->remote()) { + stream->ref(); + break; + } + } + stream = 0; + } + } + list->unlock(); + list = 0; + return stream; +} + +// Create an outgoing s2s stream. +JBServerStream* JBServerEngine::createServerStream(const String& local, + const String& remote, const char* dbId, const char* dbKey, bool dbOnly) +{ + if (exiting()) { + Debug(this,DebugAll,"Can't create s2s local=%s remote=%s: engine is exiting", + local.c_str(),remote.c_str()); + return 0; + } + JBServerStream* stream = 0; + if (!dbOnly) + stream = findServerStream(local,remote,true); + if (!stream) { + stream = new JBServerStream(this,local,remote,dbId,dbKey,dbOnly); + stream->ref(); + addStream(stream); + } + else + TelEngine::destruct(stream); + return stream; +} + +// Terminate all incoming c2s streams matching a given JID +unsigned int JBServerEngine::terminateClientStreams(const JabberID& jid, + XMPPError::Type error, const char* reason) +{ + unsigned int n = 0; + ObjList* list = findClientStreams(true,jid); + if (!list) + return 0; + n = list->count(); + DDebug(this,DebugInfo,"Terminating %u incoming c2s streams jid=%s error=%s reason=%s", + n,jid.bare().c_str(),XMPPUtils::s_tag[error].c_str(),reason); + for (ObjList* o = list->skipNull(); o; o = o->skipNext()) { + JBClientStream* stream = static_cast(o->get()); + stream->terminate(-1,true,0,error,reason); + } + TelEngine::destruct(list); + return n; +} + +// Add a stream to one of the stream lists +void JBServerEngine::addStream(JBStream* stream) +{ + if (!stream) + return; + lock(); + RefPointer recv; + RefPointer process; + if (stream->type() == JBStream::c2s) { + recv = m_c2sReceive; + process = m_c2sProcess; + } + else if (stream->type() == JBStream::s2s) { + recv = m_s2sReceive; + process = m_s2sProcess; + } + unlock(); + if (recv && process) { + recv->add(stream); + process->add(stream); + } + else + Debug(this,DebugStub,"JBServerEngine::addStream() type='%s' not handled!", + stream->typeName()); + recv = 0; + process = 0; + TelEngine::destruct(stream); +} + +// Remove a stream +void JBServerEngine::removeStream(JBStream* stream, bool delObj) +{ + if (!stream) + return; + JBEngine::removeStream(stream,delObj); + lock(); + DDebug(this,DebugAll,"JBServerEngine::removeStream(%p,%u) id=%s", + stream,delObj,stream->toString().c_str()); + RefPointer recv; + RefPointer process; + if (stream->type() == JBStream::c2s) { + recv = m_c2sReceive; + process = m_c2sProcess; + } + else if (stream->type() == JBStream::s2s) { + recv = m_s2sReceive; + process = m_s2sProcess; + } + unlock(); + if (recv) + recv->remove(stream,delObj); + if (process) + process->remove(stream,delObj); + recv = 0; + process = 0; +} + + +/* + * JBClientEngine + */ +JBClientEngine::JBClientEngine(const char* name) + : JBEngine(name), + m_receive(0), m_process(0) +{ +} + +JBClientEngine::~JBClientEngine() +{ +} + +// Terminate all streams +void JBClientEngine::cleanup(bool final, bool waitTerminate) +{ + JBEngine::cleanup(final,waitTerminate); + DDebug(this,DebugAll,"JBClientEngine::cleanup() final=%s wait=%s", + String::boolText(final),String::boolText(waitTerminate)); + if (!final) + return; + Lock lock(this); + TelEngine::destruct(m_receive); + TelEngine::destruct(m_process); +} + +// Build an outgoing client stream +JBClientStream* JBClientEngine::create(const String& account, const NamedList& params) +{ + if (!account) + return 0; + const char* domain = params.getValue("domain"); + if (TelEngine::null(domain)) + domain = params.getValue("server",params.getValue("address")); + JabberID jid(params.getValue("username"),domain,params.getValue("resource")); + if (!jid.bare()) { + Debug(this,DebugNote,"Can't create client stream: invalid jid=%s",jid.bare().c_str()); + return 0; + } + Lock lock(this); + JBClientStream* stream = static_cast(findStream(account)); + if (!stream) { + stream = new JBClientStream(this,jid,account,params); + stream->ref(); + addStream(stream); + } + else + TelEngine::destruct(stream); + return stream; +} + +// Add a stream to one of the stream lists +void JBClientEngine::addStream(JBStream* stream) +{ + if (!stream) + return; + lock(); + RefPointer recv = 0; + RefPointer process = 0; + if (stream->type() == JBStream::c2s) { + recv = m_receive; + process = m_process; + } + unlock(); + if (recv && process) { + recv->add(stream); + process->add(stream); + } + else + Debug(this,DebugStub,"JBClientEngine::addStream() type='%s' not handled!", + stream->typeName()); + recv = 0; + process = 0; + TelEngine::destruct(stream); +} + +// Remove a stream +void JBClientEngine::removeStream(JBStream* stream, bool delObj) +{ + if (!stream) + return; + JBEngine::removeStream(stream,delObj); + lock(); + DDebug(this,DebugAll,"JBClientEngine::removeStream(%p,%u) id=%s", + stream,delObj,stream->toString().c_str()); + RefPointer recv; + RefPointer process; + if (stream->type() == JBStream::c2s) { + recv = m_receive; + process = m_process; + } + unlock(); + if (recv) + recv->remove(stream,delObj); + if (process) + process->remove(stream,delObj); + recv = 0; + process = 0; +} + +// Stop all stream sets +void JBClientEngine::stopStreamSets(bool waitTerminate) +{ + XDebug(this,DebugAll,"JBClientEngine::stopStreamSets() wait=%s", + String::boolText(waitTerminate)); + lock(); + RefPointer receive = m_receive; + RefPointer process = m_process; + unlock(); + if (receive) + receive->stop(0,waitTerminate); + if (process) + process->stop(0,waitTerminate); + receive = 0; + process = 0; +} + +// Retrieve the list of streams of a given type +void JBClientEngine::getStreamList(RefPointer& list, int type) +{ + if (type != JBStream::c2s) + return; + Lock lock(this); + list = m_receive; +} + + +/* + * JBEvent + */ +JBEvent::~JBEvent() +{ + releaseStream(true); + releaseXml(true); + XDebug(DebugAll,"JBEvent::~JBEvent [%p]",this); +} + +// Get a client stream from the event's stream +JBClientStream* JBEvent::clientStream() +{ + return m_stream ? m_stream->clientStream() : 0; +} + +// Get a server stream from the event's stream +JBServerStream* JBEvent::serverStream() +{ + return m_stream ? m_stream->serverStream() : 0; +} + +// Delete the underlying XmlElement(s). Release the ownership. +XmlElement* JBEvent::releaseXml(bool del) +{ + m_child = 0; + if (del) { + TelEngine::destruct(m_element); + return 0; + } + XmlElement* tmp = m_element; + m_element = 0; + return tmp; +} + +void JBEvent::releaseStream(bool release) +{ + if (m_link && m_stream) { + m_stream->eventTerminated(this); + m_link = false; + } + if (release) + TelEngine::destruct(m_stream); +} + +// Build an 'iq' result stanza from event data +XmlElement* JBEvent::buildIqResult(bool addTags, XmlElement* child) +{ + XmlElement* xml = 0; + if (addTags) + xml = XMPPUtils::createIqResult(m_to,m_from,m_id,child); + else + xml = XMPPUtils::createIqResult(0,0,m_id,child); + return xml; +} + +// Build and send a stanza 'result' from enclosed 'iq' element +bool JBEvent::sendIqResult(XmlElement* child) +{ + if (!(m_element && m_stream && !XMPPUtils::isUnprefTag(*m_element,XmlTag::Iq))) { + TelEngine::destruct(child); + return false; + } + if (m_stanzaType == "error" || m_stanzaType == "result") { + TelEngine::destruct(child); + return false; + } + XmlElement* xml = buildIqResult(true,child); + bool ok = m_stream->state() == JBStream::Running ? + m_stream->sendStanza(xml) : m_stream->sendStreamXml(m_stream->state(),xml); + if (ok) { + releaseXml(true); + return true; + } + return false; +} + +// Build an 'iq' error stanza from event data +XmlElement* JBEvent::buildIqError(bool addTags, XMPPError::Type error, const char* reason, + XMPPError::ErrorType type) +{ + XmlElement* xml = 0; + if (addTags) + xml = XMPPUtils::createIq(XMPPUtils::IqError,m_to,m_from,m_id); + else + xml = XMPPUtils::createIq(XMPPUtils::IqError,0,0,m_id); + if (!m_id) + xml->addChild(releaseXml()); + xml->addChild(XMPPUtils::createError(type,error,reason)); + return xml; +} + +// Build and send a stanza error from enclosed element +bool JBEvent::sendStanzaError(XMPPError::Type error, const char* reason, + XMPPError::ErrorType type) +{ + if (!(m_element && m_stream && XMPPUtils::isStanza(*m_element))) + return false; + if (m_stanzaType == "error" || m_stanzaType == "result") + return false; + XmlElement* xml = new XmlElement(m_element->toString()); + xml->setAttributeValid("from",m_to); + xml->setAttributeValid("to",m_from); + xml->setAttributeValid("id",m_id); + xml->setAttribute("type","error"); + xml->addChild(XMPPUtils::createError(type,error,reason)); + bool ok = m_stream->state() == JBStream::Running ? + m_stream->sendStanza(xml) : m_stream->sendStreamXml(m_stream->state(),xml); + if (ok) { + releaseXml(true); + return true; + } + return false; +} + +bool JBEvent::init(JBStream* stream, XmlElement* element, + const JabberID* from, const JabberID* to) +{ + bool bRet = true; + if (stream && stream->ref()) + m_stream = stream; + else + bRet = false; + m_element = element; + if (from) + m_from = *from; + if (to) + m_to = *to; + XDebug(DebugAll,"JBEvent::init type=%s stream=(%p) xml=(%p) [%p]", + name(),m_stream,m_element,this); + if (!m_element) + return bRet; + + // Most elements have these parameters: + m_stanzaType = m_element->getAttribute("type"); + if (!from) + m_from = m_element->getAttribute("from"); + if (!to) + m_to = m_element->getAttribute("to"); + m_id = m_element->getAttribute("id"); + + // Decode some data + int t = XMPPUtils::tag(*m_element); + switch (t) { + case XmlTag::Message: + if (m_stanzaType != "error") + m_text = XMPPUtils::body(*m_element); + else + XMPPUtils::decodeError(m_element,m_text,m_text); + break; + case XmlTag::Iq: + case XmlTag::Presence: + if (m_stanzaType != "error") + break; + default: + XMPPUtils::decodeError(m_element,m_text,m_text); + } + return bRet; +} + + +/* + * JBStreamSet + */ +// Constructor +JBStreamSet::JBStreamSet(JBStreamSetList* owner) + : Mutex(true,"JBStreamSet"), + m_changed(false), m_exiting(false), m_owner(owner) +{ + XDebug(m_owner->engine(),DebugAll,"JBStreamSet::JBStreamSet(%s) [%p]", + m_owner->toString().c_str(),this); +} + +// Remove from owner +JBStreamSet::~JBStreamSet() +{ + if (m_clients.skipNull()) + Debug(m_owner->engine(),DebugGoOn, + "JBStreamSet(%s) destroyed while owning %u streams [%p]", + m_owner->toString().c_str(),m_clients.count(),this); + m_owner->remove(this); + XDebug(m_owner->engine(),DebugAll,"JBStreamSet::~JBStreamSet(%s) [%p]", + m_owner->toString().c_str(),this); +} + +// Add a stream to the set. The stream's reference counter will be increased +bool JBStreamSet::add(JBStream* client) +{ + if (!client) + return false; + Lock lock(this); + if (m_exiting || (m_owner->max() && m_clients.count() >= m_owner->max()) || + !client->ref()) + return false; + m_clients.append(client); + m_changed = true; + DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) added (%p,'%s') type=%s [%p]", + m_owner->toString().c_str(),client,client->name(),client->typeName(),this); + return true; +} + +// Remove a stream from set +bool JBStreamSet::remove(JBStream* client, bool delObj) +{ + if (!client) + return false; + Lock lock(this); + ObjList* o = m_clients.find(client); + if (!o) + return false; + DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) removing (%p,'%s') delObj=%u [%p]", + m_owner->toString().c_str(),client,client->name(),delObj,this); + o->remove(delObj); + m_changed = true; + return true; +} + +// Terminate all streams matching and/or local/remote jid +unsigned int JBStreamSet::dropAll(const JabberID& local, const JabberID& remote, + XMPPError::Type error, const char* reason) +{ + DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) dropAll(%s,%s,%s,%s) [%p]", + m_owner->toString().c_str(),local.c_str(),remote.c_str(), + XMPPUtils::s_error[error].c_str(),reason,this); + unsigned int n = 0; + lock(); + for (ObjList* s = m_clients.skipNull(); s; s = s->skipNext()) { + JBStream* stream = static_cast(s->get()); + Lock lck(stream); + bool terminate = false; + if (!(local || remote)) + terminate = true; + else if (local && remote) + terminate = stream->local().match(local) && stream->remote().match(remote); + else if (remote) + terminate = stream->remote().match(remote); + else + terminate = stream->local().match(local); + if (terminate) { + if (stream->state() != JBStream::Destroy) + n++; + stream->terminate(-1,true,0,error,reason); + } + } + unlock(); + return n; +} + +// Process the list +void JBStreamSet::run() +{ + DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) start running [%p]", + m_owner->toString().c_str(),this); + ObjList* o = 0; + while (true) { + if (Thread::check(false)) { + m_exiting = true; + break; + } + lock(); + if (m_changed) { + o = 0; + m_changed = false; + } + else if (o) + o = o->skipNext(); + if (!o) + o = m_clients.skipNull(); + bool eof = o && !o->skipNext(); + RefPointer stream = o ? static_cast(o->get()) : 0; + unlock(); + if (stream) { + process(*stream); + stream = 0; + } + else { + // Lock the owner to prevent adding a new client + // Don't exit if a new client was already added + Lock lock(m_owner); + if (!m_changed) { + m_exiting = true; + break; + } + } + if (eof) { + if (m_owner->m_sleepMs) + Thread::msleep(m_owner->m_sleepMs,false); + else + Thread::idle(false); + } + } + DDebug(m_owner->engine(),DebugAll,"JBStreamSet(%s) stop running [%p]", + m_owner->toString().c_str(),this); +} + +// Start running +bool JBStreamSet::start() +{ + Debug(m_owner->engine(),DebugStub,"JBStreamSet(%s)::start() [%p]", + m_owner->toString().c_str(),this); + return false; +} + +// Stop running +void JBStreamSet::stop() +{ + Debug(m_owner->engine(),DebugStub,"JBStreamSet(%s)::stop() [%p]", + m_owner->toString().c_str(),this); +} + + +/* + * JBStreamSetProcessor + */ +// Calls stream's getEvent(). Pass a generated event to the engine +bool JBStreamSetProcessor::process(JBStream& stream) +{ + JBEvent* ev = stream.getEvent(); + if (!ev) + return false; + bool remove = (ev->type() == JBEvent::Destroy); + m_owner->engine()->processEvent(ev); + if (remove) { + DDebug(m_owner->engine(),DebugAll, + "JBStreamSetProcessor(%s) requesting stream (%p,%s) ref %u removal [%p]", + m_owner->toString().c_str(),&stream,stream.toString().c_str(), + stream.refcount(),this); + m_owner->engine()->removeStream(&stream,true); + } + return true; +} + + +/* + * JBStreamSetReceive + */ +JBStreamSetReceive::JBStreamSetReceive(JBStreamSetList* owner) + : JBStreamSet(owner) +{ + if (owner && owner->engine()) + m_buffer.assign(0,owner->engine()->streamReadBuffer()); +} + +// Calls stream's readSocket() +bool JBStreamSetReceive::process(JBStream& stream) +{ + return stream.readSocket((char*)m_buffer.data(),m_buffer.length()); +} + + +/* + * JBStreamSetList + */ +// Constructor +JBStreamSetList::JBStreamSetList(JBEngine* engine, unsigned int max, + unsigned int sleepMs, const char* name) + : Mutex(true,"JBStreamSetList"), + m_engine(engine), m_name(name), + m_max(max), m_sleepMs(sleepMs), m_streamCount(0) +{ + XDebug(m_engine,DebugAll,"JBStreamSetList::JBStreamSetList(%s) [%p]", + m_name.c_str(),this); +} + +// Destructor +JBStreamSetList::~JBStreamSetList() +{ + XDebug(m_engine,DebugAll,"JBStreamSetList::~JBStreamSetList(%s) [%p]", + m_name.c_str(),this); +} + +// Add a stream to the list. Build a new set if there is no room in existing sets +bool JBStreamSetList::add(JBStream* client) +{ + if (!client || m_engine->exiting()) + return false; + Lock lock(this); + for (ObjList* o = m_sets.skipNull(); o; o = o->skipNull()) { + if ((static_cast(o->get()))->add(client)) { + m_streamCount++; + return true; + } + } + // Build a new set + JBStreamSet* set = build(); + if (!set) + return false; + if (!set->add(client)) { + lock.drop(); + TelEngine::destruct(set); + return false; + } + m_streamCount++; + m_sets.append(set); + Debug(m_engine,DebugAll,"JBStreamSetList(%s) added set (%p) count=%u [%p]", + m_name.c_str(),set,m_sets.count(),this); + lock.drop(); + if (!set->start()) + TelEngine::destruct(set); + return true; +} + +// Remove a stream from list +void JBStreamSetList::remove(JBStream* client, bool delObj) +{ + if (!client) + return; + DDebug(m_engine,DebugAll,"JBStreamSetList(%s) removing (%p,'%s') delObj=%u [%p]", + m_name.c_str(),client,client->name(),delObj,this); + Lock lock(this); + for (ObjList* o = m_sets.skipNull(); o; o = o->skipNext()) { + if ((static_cast(o->get()))->remove(client,delObj)) { + if (m_streamCount) + m_streamCount--; + return; + } + } +} + +// Stop one set or all sets +void JBStreamSetList::stop(JBStreamSet* set, bool waitTerminate) +{ + // A set will stop when all its streams will terminate + // Stop it now if wait is not requested + Lock lck(this); + if (set) { + if (set->m_owner != this) + return; + DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopping set (%p) [%p]", + m_name.c_str(),set,this); + set->dropAll(); + if (!waitTerminate) + set->stop(); + lck.drop(); + while (true) { + lock(); + bool ok = (0 == m_sets.find(set)); + unlock(); + if (ok) + break; + Thread::yield(!waitTerminate); + } + DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopped set (%p) [%p]", + m_name.c_str(),set,this); + return; + } + ObjList* o = m_sets.skipNull(); + if (!o) + return; + DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopping %u sets [%p]", + m_name.c_str(),m_sets.count(),this); + for (; o; o = o->skipNext()) { + set = static_cast(o->get()); + set->dropAll(); + if (!waitTerminate) + set->stop(); + } + lck.drop(); + while (true) { + lock(); + bool ok = (0 == m_sets.skipNull()); + unlock(); + if (ok) + break; + Thread::yield(!waitTerminate); + } + DDebug(m_engine,DebugAll,"JBStreamSetList(%s) stopped all sets [%p]", + m_name.c_str(),this); +} + +// Get the string representation of this list +const String& JBStreamSetList::toString() const +{ + return m_name; +} + +// Stop all sets. Release memory +void JBStreamSetList::destroyed() +{ + stop(0,true); + RefObject::destroyed(); +} + +//Remove a set from list without deleting it +void JBStreamSetList::remove(JBStreamSet* set) +{ + if (!set) + return; + Lock lock(this); + ObjList* o = m_sets.find(set); + if (!o) + return; + o->remove(false); + Debug(m_engine,DebugAll,"JBStreamSetList(%s) removed set (%p) count=%u [%p]", + m_name.c_str(),set,m_sets.count(),this); +} + +// Build a specialized stream set. Descendants must override this method +JBStreamSet* JBStreamSetList::build() +{ + Debug(m_engine,DebugStub,"JBStreamSetList(%s) build() not implemented! [%p]", + m_name.c_str(),this); + return 0; +} + + +/* + * JBEntityCapsList + */ + +class EntityCapsRequest : public String +{ +public: + inline EntityCapsRequest(const String& id, JBEntityCaps* caps) + : String(id), m_caps(caps), m_expire(Time::msecNow() + 30000) + {} + inline ~EntityCapsRequest() + { TelEngine::destruct(m_caps); } + JBEntityCaps* m_caps; + u_int64_t m_expire; +private: + EntityCapsRequest() {} +}; + +// Expire pending requests +void JBEntityCapsList::expire(u_int64_t msecNow) +{ + if (!m_enable) + return; + Lock lock(this); + // Stop at the first not expired item: the other items are added after it + for (ObjList* o = m_requests.skipNull(); o; o = o->skipNull()) { + EntityCapsRequest* r = static_cast(o->get()); + if (r->m_caps && msecNow < r->m_expire) + break; + DDebug(DebugInfo,"JBEntityCapsList request id=%s timed out [%p]", + r->toString().c_str(),this); + o->remove(); + } +} + +// Process a response. This method is thread safe +bool JBEntityCapsList::processRsp(XmlElement* rsp, const String& id, bool ok) +{ + if (!(rsp && id && id.startsWith(m_reqPrefix))) + return false; + if (!m_enable) + return true; + Lock lock(this); + GenObject* o = m_requests.remove(id,false); + if (!o) { + DDebug(DebugInfo,"JBEntityCapsList::processRsp(%p,%s,%u) id not found [%p]", + &rsp,id.c_str(),ok,this); + return true; + } + while (ok) { + XmlElement* query = XMPPUtils::findFirstChild(*rsp,XmlTag::Query); + if (!(query && XMPPUtils::hasXmlns(*query,XMPPNamespace::DiscoInfo))) + break; + EntityCapsRequest* r = static_cast(o); + JBEntityCaps* caps = r->m_caps; + if (!caps) + break; + // Check node (only for XEP 0115 ver >= 1.4) + if (caps->m_version == JBEntityCaps::Ver1_4) { + String* node = query->getAttribute("node"); + if (node && *node != (caps->m_node + "#" + caps->m_data)) { + DDebug(DebugAll,"JBEntityCapsList response with invalid node=%s [%p]", + node->c_str(),this); + break; + } + } + caps->m_features.fromDiscoInfo(*query); + // Check hash + if (caps->m_version == JBEntityCaps::Ver1_4) { + caps->m_features.updateEntityCaps(); + if (caps->m_data != caps->m_features.m_entityCapsHash) { + DDebug(DebugAll,"JBEntityCapsList response with invalid hash=%s (expected=%s) [%p]", + caps->m_features.m_entityCapsHash.c_str(),caps->m_data.c_str(),this); + break; + } + } + r->m_caps = 0; + // OK + append(caps); + capsAdded(caps); + break; + } + TelEngine::destruct(o); + return true; +} + +// Request entity capabilities. +void JBEntityCapsList::requestCaps(JBStream* stream, const char* from, const char* to, + const String& id, char version, const char* node, const char* data) +{ + if (!stream) + return; + Lock lock(this); + // Make sure we don't send another disco info for the same id + for (ObjList* o = m_requests.skipNull(); o; o = o->skipNext()) { + EntityCapsRequest* r = static_cast(o->get()); + if (r->m_caps && id == r->m_caps) + return; + } + String reqId; + reqId << m_reqPrefix << ++m_reqIndex; + m_requests.append(new EntityCapsRequest(reqId,new JBEntityCaps(id,version,node,data))); + lock.drop(); + XmlElement* d = 0; + if (version == JBEntityCaps::Ver1_4) + d = XMPPUtils::createIqDisco(true,true,from,to,reqId,node,data); + else + d = XMPPUtils::createIqDisco(true,true,from,to,reqId); + DDebug(DebugAll,"JBEntityCapsList sending request to=%s node=%s id=%s [%p]", + to,node,reqId.c_str(),this); + stream->sendStanza(d); +} + +// Build a document from this list +XmlDocument* JBEntityCapsList::toDocument(const char* rootName) +{ + Lock lock(this); + XmlDocument* doc = new XmlDocument; + XmlDeclaration* decl = new XmlDeclaration; + XmlSaxParser::Error err = doc->addChild(decl); + if (err != XmlSaxParser::NoError) + TelEngine::destruct(decl); + XmlComment* info = new XmlComment("Generated jabber entity capabilities cache"); + err = doc->addChild(info); + if (err != XmlSaxParser::NoError) + TelEngine::destruct(info); + XmlElement* root = new XmlElement(rootName); + err = doc->addChild(root); + if (err != XmlSaxParser::NoError) { + TelEngine::destruct(root); + return doc; + } + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + JBEntityCaps* caps = static_cast(o->get()); + XmlElement* item = new XmlElement(s_entityCapsItem); + item->setAttribute("id",caps->c_str()); + item->setAttribute("version",String((int)caps->m_version)); + item->setAttribute("node",caps->m_node); + item->setAttribute("data",caps->m_data); + caps->m_features.add(*item); + doc->addChild(item); + } + return doc; +} + +// Build this list from an XML document +void JBEntityCapsList::fromDocument(XmlDocument& doc, const char* rootName) +{ + Lock lock(this); + clear(); + m_requests.clear(); + XmlElement* root = doc.root(); + if (!root || (!TelEngine::null(rootName) && root->toString() != rootName)) { + DDebug(DebugAll,"JBEntityCapsList invalid document root %p '%s' (expected=%s) [%p]", + root,root ? root->tag() : "",rootName,this); + return; + } + XmlElement* item = root->findFirstChild(&s_entityCapsItem); + for (; item; item = root->findNextChild(item,&s_entityCapsItem)) { + String* id = item->getAttribute("id"); + if (TelEngine::null(id)) + continue; + String* tmp = item->getAttribute("version"); + JBEntityCaps* cap = new JBEntityCaps(*id,tmp ? tmp->toInteger(-1) : -1, + item->attribute("node"),item->attribute("data")); + cap->m_features.fromDiscoInfo(*item); + append(cap); + } + capsAdded(0); +} + +// Process an element containing an entity capabily child. +// Request capabilities if not found in the list +void JBEntityCapsList::processCaps(String& capsId, XmlElement* xml, JBStream* stream, + const char* from, const char* to) +{ + if (!(m_enable && xml)) + return; + char version = 0; + String* node = 0; + String* ver = 0; + String* ext = 0; + if (!decodeCaps(*xml,version,node,ver,ext)) + return; + JBEntityCaps::buildId(capsId,version,*node,*ver,ext); + Lock lock(this); + JBEntityCaps* caps = findCaps(capsId); + if (caps) + return; + // Hack for google (doesn't support disco info, supports only disco info with node) + if (version == JBEntityCaps::Ver1_3 && + (*node == s_googleTalkNode || *node == s_googleMailNode)) { + caps = new JBEntityCaps(capsId,version,*node,*ver); + if (ext) { + ObjList* list = ext->split(' ',false); + if (list->find("voice-v1")) { + caps->m_features.add(XMPPNamespace::JingleSession); + caps->m_features.add(XMPPNamespace::JingleAudio); + } + TelEngine::destruct(list); + } + append(caps); + capsAdded(caps); + return; + } + if (stream) + requestCaps(stream,from,to,capsId,version,*node,*ver); +} + +// Add capabilities to a list. +void JBEntityCapsList::addCaps(NamedList& list, JBEntityCaps& caps) +{ +#define CHECK_NS(ns,param) \ + if (caps.m_features.get(ns)) { \ + params->append(param,","); \ + list.addParam(param,String::boolText(true)); \ + } + int jingleVersion = -1; + if (caps.m_features.get(XMPPNamespace::Jingle)) + jingleVersion = 1; + else if (caps.m_features.get(XMPPNamespace::JingleSession)) + jingleVersion = 0; + if (jingleVersion != -1) { + list.addParam("caps.id",caps.toString()); + NamedString* params = new NamedString("caps.params"); + list.addParam(params); + params->append("caps.jingle_version"); + list.addParam("caps.jingle_version",String(jingleVersion)); + switch (jingleVersion) { + case 1: + CHECK_NS(XMPPNamespace::JingleAppsRtpAudio,"caps.audio"); + CHECK_NS(XMPPNamespace::JingleTransfer,"caps.calltransfer"); + CHECK_NS(XMPPNamespace::JingleAppsFileTransfer,"caps.filetransfer"); + break; + case 0: + CHECK_NS(XMPPNamespace::JingleAudio,"caps.audio"); + break; + } + } +#undef CHECK_NS +} + +// Load (reset) this list from an XML document file. +bool JBEntityCapsList::loadXmlDoc(const char* file, DebugEnabler* enabler) +{ + if (!m_enable) + return false; + XmlDocument d; + int io = 0; + DDebug(enabler,DebugAll,"Loading entity caps from '%s'",file); + XmlSaxParser::Error err = d.loadFile(file,&io); + if (err == XmlSaxParser::NoError) { + fromDocument(d); + return true; + } + String error; + if (err == XmlSaxParser::IOError) { + String tmp; + Thread::errorString(tmp,io); + error << " " << io << " '" << tmp << "'"; + } + Debug(enabler,DebugNote,"Failed to load entity caps from '%s': %s%s", + file,XmlSaxParser::getError(err),error.safe()); + return false; +} + +// Save this list to an XML document file. +bool JBEntityCapsList::saveXmlDoc(const char* file, DebugEnabler* enabler) +{ + DDebug(enabler,DebugAll,"Saving entity caps to '%s'",file); + if (TelEngine::null(file)) + return false; + XmlDocument* doc = toDocument(); + int res = doc->saveFile(file,true," "); + if (res) + Debug(enabler,DebugNote,"Failed to save entity caps to '%s'",file); + delete doc; + return res == 0; +} + +// Check if an XML element has a 'c' entity capability child and process it +bool JBEntityCapsList::decodeCaps(const XmlElement& xml, char& version, String*& node, + String*& ver, String*& ext) +{ + XmlElement* c = XMPPUtils::findFirstChild(xml,XmlTag::EntityCapsTag, + XMPPNamespace::EntityCaps); + if (!c) + return false; + node = c->getAttribute("node"); + ver = c->getAttribute("ver"); + if (TelEngine::null(node) || TelEngine::null(ver)) + return false; + String* hash = c->getAttribute("hash"); + if (hash) { + // Version 1.4 or greater + if (*hash != "sha-1") + return false; + version = JBEntityCaps::Ver1_4; + ext = 0; + } + else { + version = JBEntityCaps::Ver1_3; + ext = c->getAttribute("ext"); + } + return true; +} + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjabber/jbstream.cpp b/libs/yjabber/jbstream.cpp new file mode 100644 index 00000000..e7e0849c --- /dev/null +++ b/libs/yjabber/jbstream.cpp @@ -0,0 +1,2509 @@ +/** + * jbstream.cpp + * Yet Another Jabber Component Protocol Stack + * This file is part of the YATE Project http://YATE.null.ro + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +using namespace TelEngine; + +static const String s_dbVerify = "verify"; +static const String s_dbResult = "result"; + +static inline bool isDbVerify(XmlElement& xml) +{ + const String* tag = 0; + const String* ns = 0; + return xml.getTag(tag,ns) && *tag == s_dbVerify && + ns && *ns == XMPPUtils::s_ns[XMPPNamespace::Dialback]; +} + +static inline bool isDbResult(XmlElement& xml) +{ + const String* tag = 0; + const String* ns = 0; + return xml.getTag(tag,ns) && *tag == s_dbResult && + ns && *ns == XMPPUtils::s_ns[XMPPNamespace::Dialback]; +} + +// Decode a Base64 string to a block +static inline bool decodeBase64(DataBlock& buf, const String& str) +{ + Base64 b((void*)str.c_str(),str.length(),false); + bool ok = b.decode(buf,false); + b.clear(false); + return ok; +} + +// Decode a Base64 string to another string +// Check if decoded data has valid UTF8 characters +static bool decodeBase64(String& buf, const String& str, JBStream* stream) +{ + DataBlock d; + if (!decodeBase64(d,str)) + return false; + buf.assign((const char*)d.data(),d.length()); + if (-1 != buf.lenUtf8()) + return true; + Debug(stream,DebugNote,"Received Base64 encoded invalid UTF8 characters [%p]",stream); + return false; +} + + +static const TokenDict s_location[] = { + {"internal", 0}, + {"remote", 1}, + {"local", -1}, + {0,0}, +}; + +const TokenDict JBStream::s_stateName[] = { + {"Running", Running}, + {"Idle", Idle}, + {"Connecting", Connecting}, + {"WaitStart", WaitStart}, + {"Starting", Starting}, + {"Features", Features}, + {"WaitTlsRsp", WaitTlsRsp}, + {"Auth", Auth}, + {"Challenge", Challenge}, + {"Securing", Securing}, + {"Register", Register}, + {"Destroy", Destroy}, + {0,0}, +}; + +const TokenDict JBStream::s_flagName[] = { + {"noautorestart", NoAutoRestart}, + {"tlsrequired", TlsRequired}, + {"dialback", DialbackOnly}, + {"allowplainauth", AllowPlainAuth}, + {"register", RegisterUser}, + // Internal flags + {"roster_requested", RosterRequested}, + {"online", AvailableResource}, + {"secured", StreamTls | StreamSecured}, + {"authenticated", StreamAuthenticated}, + {"waitbindrsp", StreamWaitBindRsp}, + {"waitsessrsp", StreamWaitSessRsp}, + {"waitchallenge", StreamWaitChallenge}, + {"waitchallengersp", StreamWaitChgRsp}, + {0,0} +}; + +const TokenDict JBStream::s_typeName[] = { + {"c2s", c2s}, + {"s2s", s2s}, + {0,0} +}; + +// Retrieve the multiplier for non client stream timers +static inline unsigned int timerMultiplier(JBStream* stream) +{ + return stream->type() == JBStream::c2s ? 1 : 1; +} + + +/* + * JBStream + */ +// Incoming +JBStream::JBStream(JBEngine* engine, Socket* socket, Type t) + : Mutex(true,"JBStream"), + m_sasl(0), + m_state(Idle), m_flags(0), m_xmlns(XMPPNamespace::Count), m_lastEvent(0), + m_setupTimeout(0), m_startTimeout(0), + m_pingTimeout(0), m_nextPing(0), + m_idleTimeout(0), m_connectTimeout(0), + m_restart(0), m_timeToFillRestart(0), + m_engine(engine), m_type(t), + m_incoming(true), m_terminateEvent(0), + m_xmlDom(0), m_socket(0), m_socketFlags(0), m_connectPort(0) +{ + m_engine->buildStreamName(m_name); + debugName(m_name); + debugChain(m_engine); + Debug(this,DebugAll,"JBStream::JBStream(%p,%p,%s) incoming [%p]", + engine,socket,typeName(),this); + setXmlns(); + // Don't restart incoming streams + m_flags |= NoAutoRestart; + resetConnection(socket); + changeState(WaitStart); +} + +// Outgoing +JBStream::JBStream(JBEngine* engine, Type t, const JabberID& local, const JabberID& remote, + const char* name, const NamedList* params) + : Mutex(true,"JBStream"), + m_sasl(0), + m_state(Idle), m_local(local), m_remote(remote), + m_flags(0), m_xmlns(XMPPNamespace::Count), m_lastEvent(0), + m_setupTimeout(0), m_startTimeout(0), + m_pingTimeout(0), m_nextPing(0), + m_idleTimeout(0), m_connectTimeout(0), + m_restart(1), m_timeToFillRestart(0), + m_engine(engine), m_type(t), + m_incoming(false), m_name(name), + m_terminateEvent(0), + m_xmlDom(0), m_socket(0), m_socketFlags(0), m_connectPort(0) +{ + if (!m_name) + m_engine->buildStreamName(m_name); + debugName(m_name); + debugChain(m_engine); + if (params) { + int flgs = XMPPUtils::decodeFlags(params->getValue("options"),s_flagName); + m_flags = flgs & StreamFlags; + m_connectAddr = params->getValue("server",params->getValue("address")); + m_connectPort = params->getIntValue("port"); + } + else + updateFromRemoteDef(); + Debug(this,DebugAll,"JBStream::JBStream(%p,%s,%s,%s) outgoing [%p]", + engine,typeName(),local.c_str(),remote.c_str(),this); + setXmlns(); + changeState(Idle); +} + +// Destructor +JBStream::~JBStream() +{ + DDebug(this,DebugAll,"JBStream::~JBStream() id=%s [%p]",m_name.c_str(),this); + TelEngine::destruct(m_sasl); +} + +// Outgoing stream connect terminated notification. +void JBStream::connectTerminated(Socket*& sock) +{ + Lock lock(this); + if (m_state == Connecting) { + if (sock) { + resetConnection(sock); + sock = 0; + changeState(Starting); + start(); + } + else { + DDebug(this,DebugNote,"Connect failed [%p]",this); + terminate(0,false,0,XMPPError::NoRemote); + } + return; + } + DDebug(this,DebugInfo,"Connect terminated notification in non %s state [%p]", + lookup(Connecting,s_stateName),this); + if (sock) { + delete sock; + sock = 0; + } +} + +// Get an object from this stream +void* JBStream::getObject(const String& name) const +{ + if (name == "Socket*") + return state() == Securing ? (void*)&m_socket : 0; + if (name == "JBStream") + return (void*)this; + return RefObject::getObject(name); +} + +// Get the string representation of this stream +const String& JBStream::toString() const +{ + return m_name; +} + +// Set/reset RosterRequested flag +void JBStream::setRosterRequested(bool ok) +{ + Lock lock(this); + if (ok == flag(RosterRequested)) + return; + if (ok) + m_flags |= RosterRequested; + else + m_flags &= ~RosterRequested; + XDebug(this,DebugAll,"%s roster requested flag [%p]",ok ? "Set" : "Reset",this); +} + +// Set/reset AvailableResource/PositivePriority flags +bool JBStream::setAvailableResource(bool ok, bool positive) +{ + Lock lock(this); + if (ok && positive) + m_flags |= PositivePriority; + else + m_flags &= ~PositivePriority; + if (ok == flag(AvailableResource)) + return false; + if (ok) + m_flags |= AvailableResource; + else + m_flags &= ~AvailableResource; + XDebug(this,DebugAll,"%s available resource flag [%p]",ok ? "Set" : "Reset",this); + return true; +} + +// Read data from socket. Send it to the parser +bool JBStream::readSocket(char* buf, unsigned int len) +{ + if (!(buf && len > 1)) + return false; + if (!socketCanRead()) + return false; + Lock lock(this); + if (!socketCanRead() || state() == Destroy || state() == Idle || state() == Connecting) + return false; + socketSetReading(true); + if (state() != WaitTlsRsp) + len--; + else + len = 1; + lock.drop(); + XMPPError::Type error = XMPPError::NoError; + int read = m_socket->readData(buf,len); + Lock lck(this); + // Check if something changed + if (!(m_socket && socketReading())) { + Debug(this,DebugAll,"Socket deleted while reading [%p]",this); + return false; + } + if (read && read != Socket::socketError()) { + buf[read] = 0; + XDebug(this,DebugInfo,"Received %s [%p]",buf,this); + if (!m_xmlDom->parse(buf)) { + if (m_xmlDom->error() != XmlSaxParser::Incomplete) + error = XMPPError::Xml; + else if (m_xmlDom->buffer().length() > m_engine->m_maxIncompleteXml) + error = XMPPError::Policy; + } + } + socketSetReading(false); + if (read) { + if (read == Socket::socketError()) { + if (m_socket->canRetry()) { + read = 0; +#ifdef XDEBUG + String tmp; + Thread::errorString(tmp,m_socket->error()); + Debug(this,DebugAll,"Socket temporary unavailable for read. %d: '%s' [%p]", + m_socket->error(),tmp.c_str(),this); +#endif + } + else + error = XMPPError::SocketError; + } + } + else + error = XMPPError::SocketError; + if (error == XMPPError::NoError) { + // Stop reading if waiting for TLS start and received a complete element + // We'll wait for the stream processor to handle the received element + if (read && state() == WaitTlsRsp && !m_xmlDom->buffer().length() && + m_xmlDom->unparsed() == XmlSaxParser::None) { + XmlDocument* doc = m_xmlDom->document(); + // If received a complete element, the parser's current element is + // the document's root + if (doc && m_xmlDom->isCurrent(doc->root())) { + DDebug(this,DebugAll,"Received complete element in state=%s. Stop reading [%p]", + stateName(),this); + socketSetCanRead(false); + } + } + return read > 0; + } + // Error + int location = 0; + const char* reason = 0; + if (error != XMPPError::SocketError) { + String tmp; + if (error == XMPPError::Xml) { + reason = m_xmlDom->getError(); + tmp = m_xmlDom->buffer(); + } + else { + tmp << "overflow len=" << m_xmlDom->buffer().length() << " max=" << + m_engine->m_maxIncompleteXml; + reason = "XML element too long"; + } + Debug(this,DebugNote,"Parser error='%s' buffer='%s' [%p]", + reason,tmp.c_str(),this); + } + else if (read) { + String tmp; + Thread::errorString(tmp,m_socket->error()); + Debug(this,DebugWarn,"Socket read error: %d: '%s' [%p]",m_socket->error(), + tmp.c_str(),this); + } + else { + Debug(this,DebugInfo,"Stream EOF [%p]",this); + location = 1; + } + lck.drop(); + terminate(location,m_incoming,0,error,reason); + return read > 0; +} + +// Stream state processor +JBEvent* JBStream::getEvent(u_int64_t time) +{ + if (m_lastEvent) + return 0; + Lock lock(this); + if (m_lastEvent) + return 0; + XDebug(this,DebugAll,"JBStream::getEvent() [%p]",this); + checkPendingEvent(); + if (!m_lastEvent) { + if (canProcess(time)) { + process(time); + checkPendingEvent(); + if (!m_lastEvent) + checkTimeouts(time); + } + else + checkPendingEvent(); + } +#ifdef XDEBUG + if (m_lastEvent) + Debug(this,DebugAll,"Generating event (%p,%s) in state '%s' [%p]", + m_lastEvent,m_lastEvent->name(),stateName(),this); +#endif + return m_lastEvent; +} + +// Send a stanza ('iq', 'message' or 'presence') in Running state. +bool JBStream::sendStanza(XmlElement*& xml) +{ + if (!xml) + return false; + DDebug(this,DebugAll,"sendStanza(%p) '%s' [%p]",xml,xml->tag(),this); + if (!XMPPUtils::isStanza(*xml)) { + Debug(this,DebugNote,"Request to send non stanza xml='%s' [%p]",xml->tag(),this); + TelEngine::destruct(xml); + return false; + } + Lock lock(this); + m_pending.append(new XmlElementOut(xml)); + xml = 0; + sendPending(); + return true; +} + +// Send stream related XML when negotiating the stream +// or some other stanza in non Running state +bool JBStream::sendStreamXml(State newState, XmlElement* first, XmlElement* second, + XmlElement* third) +{ + DDebug(this,DebugAll,"sendStreamXml(%s,%p,%p,%p) [%p]", + stateName(),first,second,third,this); + Lock lock(this); + bool ok = false; + XmlFragment frag; + // Use a while() to break to the end: safe cleanup + while (true) { + if (m_state == Idle || m_state == Destroy) + break; + // Check if we have unsent stream xml + if (m_outStreamXml) + sendPending(true); + if (m_outStreamXml) + break; + if (!first) + break; + // Add stream declaration before stream start + if (first->getTag() == XMPPUtils::s_tag[XmlTag::Stream] && + first->tag()[0] != '/') { + XmlDeclaration* decl = new XmlDeclaration; + decl->toString(m_outStreamXml,true); + frag.addChild(decl); + } + first->toString(m_outStreamXml,true,String::empty(),String::empty(),false); + frag.addChild(first); + if (second) { + second->toString(m_outStreamXml,true,String::empty(),String::empty(),false); + frag.addChild(second); + if (third) { + third->toString(m_outStreamXml,true,String::empty(),String::empty(),false); + frag.addChild(third); + } + } + first = second = third = 0; + m_engine->printXml(this,true,frag); + ok = sendPending(true); + break; + } + TelEngine::destruct(first); + TelEngine::destruct(second); + TelEngine::destruct(third); + if (ok) + changeState(newState); + return ok; +} + +// Start the stream. This method should be called by the upper layer +// when processing an incoming stream Start event. For outgoing streams +// this method is called internally on succesfully connect. +void JBStream::start(XMPPFeatureList* features, XmlElement* caps) +{ + Lock lock(this); + if (m_state != Starting) + return; + if (outgoing()) { + TelEngine::destruct(caps); + XmlElement* s = buildStreamStart(); + sendStreamXml(WaitStart,s); + return; + } + m_features.clear(); + if (features) + m_features.add(*features); + // Set secured flag if we don't advertise TLS + if (!(flag(StreamSecured) || m_features.get(XMPPNamespace::Tls))) + setSecured(); + // Set authenticated flag if we don't advertise authentication mechanisms + if (flag(StreamSecured)) { + if (flag(StreamAuthenticated)) + m_features.remove(XMPPNamespace::Sasl); + else if (!m_features.get(XMPPNamespace::Sasl)) + m_flags |= JBStream::StreamAuthenticated; + } + // Send start and features + XmlElement* s = buildStreamStart(); + XmlElement* f = 0; + if (flag(StreamRemoteVer1)) + f = m_features.buildStreamFeatures(); + if (f && caps) + f->addChild(caps); + else + TelEngine::destruct(caps); + State newState = Features; + if (m_type == c2s) { + // Change stream state to Running if authenticated and there is no required + // feature to negotiate + if (flag(StreamAuthenticated) && !firstRequiredFeature()) + newState = Running; + } + sendStreamXml(newState,s,f); +} + +// Authenticate an incoming stream +bool JBStream::authenticated(bool ok, const String& rsp, XMPPError::Type error) +{ + Lock lock(this); + if (m_state != Auth || !incoming()) + return false; + DDebug(this,DebugAll,"authenticated(%s,'%s',%s) local=%s [%p]", + String::boolText(ok),rsp.safe(),XMPPUtils::s_error[error].c_str(), + m_local.c_str(),this); + if (ok) { + m_flags |= StreamAuthenticated; + if (m_type == c2s) { + // Set remote party node if not provided + if (m_type == c2s && m_sasl && m_sasl->m_params && !m_remote.node()) { + m_remote.set(m_sasl->m_params->getValue("username"),m_local.domain(),""); + Debug(this,DebugAll,"Remote party set to '%s' [%p]",m_remote.c_str(),this); + } + m_features.remove(XMPPNamespace::Sasl); + String text; + if (m_sasl) + m_sasl->buildAuthRspReply(text,rsp); + XmlElement* s = XMPPUtils::createElement(XmlTag::Success, + XMPPNamespace::Sasl,text); + ok = sendStreamXml(WaitStart,s); + } + else if (m_type == s2s) { + XmlElement* rsp = XMPPUtils::createDialbackResult(m_local,m_remote,true); + ok = sendStreamXml(Running,rsp); + } + } + else { + if (m_type == c2s) { + XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl,error); + ok = sendStreamXml(Features,failure); + } + else if (m_type == s2s) { + XmlElement* rsp = XMPPUtils::createDialbackResult(m_local,m_remote,false); + ok = sendStreamXml(state(),rsp); + if (ok) + terminate(0,true,0,XMPPError::NotAuthorized); + } + } + TelEngine::destruct(m_sasl); + return ok; +} + +// Terminate the stream. Send stream end tag or error. +// Reset the stream. Deref stream if destroying. +void JBStream::terminate(int location, bool destroy, XmlElement* xml, int error, + const char* reason, bool final) +{ + XDebug(this,DebugAll,"terminate(%d,%u,%p,%u,%s,%u) state=%s [%p]", + location,destroy,xml,error,reason,final,stateName(),this); + Lock lock(this); + m_pending.clear(); + // Already in destroy + if (state() == Destroy) { + TelEngine::destruct(xml); + return; + } + bool sendEndTag = true; + destroy = destroy || final || flag(NoAutoRestart); + if (error == XMPPError::NoError && m_engine->exiting()) + error = XMPPError::Shutdown; + // Last check for sendEndTag + if (sendEndTag) { + // Prohibitted states or socket read/write error + if (m_state == Destroy || m_state == Securing || m_state == Connecting) + sendEndTag = false; + else if (error == XMPPError::SocketError) { + sendEndTag = false; + reason = "I/O error"; + } + } + Debug(this,DebugAll, + "Terminate by '%s' state=%s destroy=%u error=%s reason='%s' final=%u [%p]", + lookup(location,s_location),stateName(),destroy, + XMPPUtils::s_error[error].c_str(),reason,final,this); + if (sendEndTag) { + XmlElement* start = 0; + if (m_state == Starting && incoming()) + start = buildStreamStart(); + XmlElement* end = new XmlElement(String("/stream:stream"),false); + if (error != XMPPError::NoError && location < 1) { + XmlElement* e = XMPPUtils::createStreamError(error,reason); + if (!start) + sendStreamXml(m_state,e,end); + else + sendStreamXml(m_state,start,e,end); + } + else { + if (!start) + sendStreamXml(m_state,end); + else + sendStreamXml(m_state,start,end); + } + } + resetConnection(); + m_outStreamXml.clear(); + + // Always set termination event, except when called from destructor + if (!(final || m_terminateEvent)) { + // TODO: Cancel all outgoing elements without id + m_terminateEvent = new JBEvent(destroy ? JBEvent::Destroy : JBEvent::Terminated, + this,xml); + xml = 0; + if (!m_terminateEvent->m_text) { + if (TelEngine::null(reason)) + m_terminateEvent->m_text = XMPPUtils::s_error[error]; + else + m_terminateEvent->m_text = reason; + } + } + TelEngine::destruct(xml); + + changeState(destroy ? Destroy : Idle); +} + +// Close the stream. Release memory +void JBStream::destroyed() +{ + terminate(0,true,0,XMPPError::NoError,"",true); + resetConnection(); + if (m_engine) + m_engine->removeStream(this,false); + TelEngine::destruct(m_terminateEvent); + DDebug(this,DebugAll,"Destroyed local=%s remote=%s [%p]", + m_local.safe(),m_remote.safe(),this); + RefObject::destroyed(); +} + +// Check if stream state processor can continue +// This method is called from getEvent() with the stream locked +bool JBStream::canProcess(u_int64_t time) +{ + if (outgoing()) { + // Increase stream restart counter if it's time to and should auto restart + if (!flag(NoAutoRestart) && m_timeToFillRestart < time) { + m_timeToFillRestart = time + m_engine->m_restartUpdInterval; + if (m_restart < m_engine->m_restartMax) { + m_restart++; + Debug(this,DebugAll,"Restart count set to %u max=%u [%p]", + m_restart,m_engine->m_restartMax,this); + } + } + if (state() == Idle) { + // Re-connect + if (m_restart) { + changeState(Connecting); + m_restart--; + m_engine->connectStream(this); + return false; + } + // Destroy if not auto-restarting + if (flag(NoAutoRestart)) { + terminate(0,true,0); + return false; + } + } + } + else if (state() == Idle && flag(NoAutoRestart)) { + terminate(0,true,0); + return false; + } + return true; +} + +// Process stream state. Get XML from parser's queue and process it +// This method is called from getEvent() with the stream locked +void JBStream::process(u_int64_t time) +{ + if (!m_xmlDom) + return; + XDebug(this,DebugAll,"JBStream::process() [%p]",this); + XmlDocument* doc = m_xmlDom->document(); + if (!doc) { + Debug(this,DebugGoOn,"The parser is not a document! [%p]",this); + terminate(0,true,0,XMPPError::Internal); + return; + } + XmlElement* root = doc->root(false); + while (true) { + sendPending(); + if (m_terminateEvent || !root) + break; + // Check for stream termination + if (root->completed()) { + bool error = false; + // Check if received an error + XmlElement* xml = 0; + while (0 != (xml = root->pop())) { + if (streamError(xml)) { + error = true; + break; + } + TelEngine::destruct(xml); + } + if (error) + break; + Debug(this,DebugAll,"Remote closed the stream in state %s [%p]", + stateName(),this); + terminate(1,false,0); + break; + } + + if (m_state == WaitStart) { + // Print the declaration + XmlDeclaration* dec = doc->declaration(); + if (dec) + m_engine->printXml(this,false,*dec); + // Print the root. Make sure we don't print its children + if (!root->getChildren().skipNull()) + m_engine->printXml(this,false,*root); + else { + XmlElement tmp(*root); + tmp.clearChildren(); + m_engine->printXml(this,false,tmp); + } + // Check if valid + if (!XMPPUtils::isTag(*root,XmlTag::Stream,XMPPNamespace::Stream)) { + String* ns = root->xmlns(); + Debug(this,DebugMild,"Received invalid stream root '%s' namespace='%s' [%p]", + root->tag(),TelEngine::c_safe(ns),this); + terminate(0,true,0); + break; + } + // Check 'from' and 'to' + JabberID from; + JabberID to; + if (!getJids(root,from,to)) + break; + XDebug(this,DebugAll,"Processing (%p,%s) in state %s [%p]", + root,root->tag(),stateName(),this); + processStart(root,from,to); + break; + } + + XmlElement* xml = root->pop(); + if (!xml) + break; + + // Process received element + // Print it + m_engine->printXml(this,false,*xml); + // Check stream termination + if (streamError(xml)) + break; + // Check 'from' and 'to' + JabberID from; + JabberID to; + if (!getJids(xml,from,to)) + break; + // Restart the idle timer + if (m_state == Running && m_engine->m_idleTimeout) + m_idleTimeout = time + m_engine->m_idleTimeout; + // Check if a received stanza is valid and allowed in current state + if (!checkStanzaRecv(xml,from,to)) + break; + + XDebug(m_engine,DebugAll,"Processing (%p,%s) in state %s [%p]", + xml,xml->tag(),stateName(),this); + + // Process here dialback verify + if (m_type == s2s && isDbVerify(*xml)) { + switch (state()) { + case Running: + case Features: + case Starting: + case Challenge: + case Auth: + m_events.append(new JBEvent(JBEvent::DbVerify,this,xml,from,to)); + break; + default: + dropXml(xml,"dialback verify in unsupported state"); + } + continue; + } + + switch (m_state) { + case Running: + processRunning(xml,from,to); + break; + case Features: + if (m_incoming) + processFeaturesIn(xml,from,to); + else + processFeaturesOut(xml,from,to); + break; + case WaitStart: + case Starting: + processStart(xml,from,to); + TelEngine::destruct(xml); + break; + case Challenge: + processChallenge(xml,from,to); + break; + case Auth: + processAuth(xml,from,to); + break; + case WaitTlsRsp: + processWaitTlsRsp(xml,from,to); + break; + case Register: + processRegister(xml,from,to); + break; + default: + dropXml(xml,"unhandled stream state in process()"); + } + break; + } + XDebug(this,DebugAll,"JBStream::process() exiting [%p]",this); +} + +// Process elements in Running state +bool JBStream::processRunning(XmlElement* xml, const JabberID& from, const JabberID& to) +{ + if (!xml) + return true; + int t, ns; + if (!XMPPUtils::getTag(*xml,t,ns)) + return dropXml(xml,"failed to retrieve element tag"); + switch (t) { + case XmlTag::Message: + if (ns != m_xmlns) + break; + m_events.append(new JBEvent(JBEvent::Message,this,xml,from,to)); + return true; + case XmlTag::Presence: + if (ns != m_xmlns) + break; + m_events.append(new JBEvent(JBEvent::Presence,this,xml,from,to)); + return true; + case XmlTag::Iq: + if (ns != m_xmlns) + break; + m_events.append(new JBEvent(JBEvent::Iq,this,xml,from,to,xml->findFirstChild())); + return true; + default: + m_events.append(new JBEvent(JBEvent::Unknown,this,xml,from,to)); + return true; + } + // Invalid stanza namespace + XmlElement* rsp = XMPPUtils::createError(xml,XMPPError::TypeModify, + XMPPError::InvalidNamespace,"Only stanzas in default namespace are allowed"); + sendStanza(rsp); + return true; +} + +// Check stream timeouts +// This method is called from getEvent() with the stream locked, after +void JBStream::checkTimeouts(u_int64_t time) +{ + // Running: check ping and idle timers + if (m_state == Running) { + if (m_pingTimeout) { + if (m_pingTimeout < time) + terminate(0,false,0,XMPPError::ConnTimeout,"Ping timeout"); + } + else if (m_nextPing && time >= m_nextPing) { + m_pingId = (unsigned int)time; + // TODO: Send it + Debug(this,DebugStub,"JBStream::checkTimeouts() sendPing() not implemented"); + } + else if (m_idleTimeout && m_idleTimeout < time) + terminate(0,true,0,XMPPError::ConnTimeout,"Stream idle"); + return; + } + // Stream setup timer + if (m_setupTimeout && m_setupTimeout < time) { + terminate(0,m_incoming,0,XMPPError::Policy,"Stream setup timeout"); + return; + } + // Stream start timer + if (m_startTimeout && m_startTimeout < time) { + terminate(0,m_incoming,0,XMPPError::Policy,"Stream start timeout"); + return; + } + // Stream connect timer + if (m_connectTimeout && m_connectTimeout < time) { + terminate(0,m_incoming,0,XMPPError::ConnTimeout,"Stream connect timeout"); + return; + } +} + +// Reset the stream's connection. Build a new XML parser if the socket is valid +void JBStream::resetConnection(Socket* sock) +{ + XDebug(this,DebugAll,"JBStream::resetConnection(%p) [%p]",sock,this); + // Release the old one + if (m_socket) { + // Wait for the socket to become available (not reading or writing) + Socket* tmp = 0; + while (true) { + Lock lock(this); + if (!(m_socket && (socketReading() || socketWriting()))) { + tmp = m_socket; + m_socket = 0; + m_socketFlags = 0; + if (m_xmlDom) { + delete m_xmlDom; + m_xmlDom = 0; + } + break; + } + lock.drop(); + Thread::yield(false); + } + if (tmp) { + tmp->setLinger(-1); + tmp->terminate(); + delete tmp; + } + } + if (sock) { + Lock lock(this); + if (m_socket) { + Debug(this,DebugWarn,"Duplicate attempt to set socket! [%p]",this); + delete sock; + return; + } + m_xmlDom = new XmlDomParser(debugName()); + m_xmlDom->debugChain(this); + m_socket = sock; + if (debugAt(DebugAll)) { + SocketAddr l, r; + localAddr(l); + remoteAddr(r); + Debug(this,DebugAll,"Connection set local=%s:%d remote=%s:%d [%p]", + l.host().c_str(),l.port(),r.host().c_str(),r.port(),this); + } + m_socket->setReuse(true); + m_socket->setBlocking(false); + socketSetCanRead(true); + } +} + +// Build a stream start XML element +XmlElement* JBStream::buildStreamStart() +{ + XmlElement* start = new XmlElement(XMPPUtils::s_tag[XmlTag::Stream],false); + if (incoming()) + start->setAttribute("id",m_id); + XMPPUtils::setStreamXmlns(*start); + start->setAttribute(XmlElement::s_ns,XMPPUtils::s_ns[m_xmlns]); + start->setAttributeValid("from",m_local.bare()); + start->setAttributeValid("to",m_remote.bare()); + start->setAttribute("version","1.0"); + start->setAttribute("xml:lang","en"); + return start; +} + +// Process received elements in WaitStart state +// WaitStart: Incoming: waiting for stream start +// Outgoing: idem (our stream start was already sent) +// Return false if stream termination was initiated +bool JBStream::processStart(const XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + Debug(this,DebugStub,"JBStream::processStart(%s) [%p]",xml->tag(),this); + return true; +} + +// Process elements in Register state +bool JBStream::processRegister(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + dropXml(xml,"can't process in this state"); + terminate(0,true,0,XMPPError::Internal); + return false; +} + +// Process elements in Auth state +bool JBStream::processAuth(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + return dropXml(xml,"can't process in this state"); +} + +// Check if a received start start element's namespaces are correct. +bool JBStream::processStreamStart(const XmlElement* xml) +{ + if (m_state == Starting) + return true; + changeState(Starting); + if (!XMPPUtils::hasDefaultXmlns(*xml,m_xmlns)) { + Debug(this,DebugNote,"Received '%s' with invalid xmlns='%s' [%p]", + xml->tag(),TelEngine::c_safe(xml->xmlns()),this); + terminate(0,m_incoming,0,XMPPError::InvalidNamespace); + return false; + } + XMPPError::Type error = XMPPError::NoError; + const char* reason = 0; + while (true) { + if (m_type != c2s && m_type != s2s) { + Debug(this,DebugStub,"processStreamStart() type %u not handled!",m_type); + error = XMPPError::Internal; + break; + } + // Check xmlns:stream + String* nsStr = xml->getAttribute("xmlns:stream"); + if (!nsStr || *nsStr != XMPPUtils::s_ns[XMPPNamespace::Stream]) { + Debug(this,DebugNote,"Received '%s' with invalid xmlns:stream='%s' [%p]", + xml->tag(),TelEngine::c_safe(nsStr),this); + error = XMPPError::InvalidNamespace; + break; + } + // Check version + String ver(xml->getAttribute("version")); + int remoteVersion = -1; + if (ver) { + int pos = ver.find('.'); + if (pos > 0) + remoteVersion = ver.substr(0,pos).toInteger(-1); + } + if (remoteVersion == 1) + m_flags |= StreamRemoteVer1; + else if (remoteVersion < 1) { + if (m_type == c2s) + error = XMPPError::UnsupportedVersion; + else if (m_type == s2s) { + // Accept invalid/unsupported version on if TLS is not required + if (!flag(TlsRequired)) { + // Check dialback + if (!xml->hasAttribute("xmlns:db",XMPPUtils::s_ns[XMPPNamespace::Dialback])) { + Debug(this,DebugNote,"Received non dialback '%s' [%p]", + xml->tag(),this); + error = XMPPError::InvalidNamespace; + } + } + else + error = XMPPError::EncryptionRequired; + } + else + error = XMPPError::Internal; + } + else if (remoteVersion > 1) + error = XMPPError::UnsupportedVersion; + if (error != XMPPError::NoError) { + Debug(this,DebugNote,"Received '%s' with unacceptable version='%s' [%p]", + xml->tag(),ver.c_str(),this); + break; + } + // Set stream id: generate one for incoming, get it from xml if outgoing + if (incoming()) { + // Generate a random, variable length stream id + MD5 md5(String((int)(int64_t)this)); + md5 << m_name << String((int)Time::msecNow()); + m_id = md5.hexDigest(); + m_id << "_" << String((int)::random()); + } + else { + m_id = xml->getAttribute("id"); + if (!m_id) + reason = "Missing stream id"; + else if (m_engine->checkDupId(this)) + reason = "Duplicate stream id"; + if (reason) { + Debug(this,DebugNote,"Received '%s' with invalid stream id='%s' [%p]", + xml->tag(),m_id.c_str(),this); + error = XMPPError::InvalidId; + break; + } + } + XDebug(this,DebugAll,"Stream id set to '%s' [%p]",m_id.c_str(),this); + break; + } + if (error == XMPPError::NoError) + return true; + terminate(0,m_incoming,0,error,reason); + return false; +} + +// Check if a received element is a stream error one +bool JBStream::streamError(XmlElement* xml) +{ + if (!(xml && XMPPUtils::isTag(*xml,XmlTag::Error,XMPPNamespace::Stream))) + return false; + String text; + String error; + XMPPUtils::decodeError(xml,false,error,text); + Debug(this,DebugAll,"Received stream error '%s' text='%s' in state %s [%p]", + error.c_str(),text.c_str(),stateName(),this); + int err = XMPPUtils::s_error[error]; + if (err >= XMPPError::Count) + err = XMPPError::NoError; + terminate(1,false,xml,err,text); + return true; +} + +// Check if a received element has valid 'from' and 'to' jid attributes +bool JBStream::getJids(XmlElement* xml, JabberID& from, JabberID& to) +{ + if (!xml) + return true; + from = xml->getAttribute("from"); + to = xml->getAttribute("to"); + XDebug(this,DebugAll,"Got jids xml='%s' from='%s' to='%s' [%p]", + xml->tag(),from.c_str(),to.c_str(),this); + if (to.valid() && from.valid()) + return true; + Debug(this,DebugNote,"Received '%s' with bad from='%s' or to='%s' [%p]", + xml->tag(),from.c_str(),to.c_str(),this); + terminate(0,m_incoming,xml,XMPPError::BadAddressing); + return false; +} + +// Check if a received element is a presence, message or iq qualified by the stream +// namespace and the stream is not authenticated +// Validate 'from' for c2s streams +// Validate s2s 'to' domain and 'from' jid +bool JBStream::checkStanzaRecv(XmlElement* xml, JabberID& from, JabberID& to) +{ + if (!XMPPUtils::isStanza(*xml)) + return true; + + // RFC 3920bis 5.2: Accept stanzas only if the stream was authenticated + // Accept IQs in jabber:iq:register namespace + // They might be received on a non authenticated stream) + if (!flag(StreamAuthenticated)) { + bool isIq = XMPPUtils::isTag(*xml,XmlTag::Iq,m_xmlns); + bool valid = isIq && XMPPUtils::findFirstChild(*xml,XmlTag::Count, + XMPPNamespace::IqRegister); + // Outgoing client stream: check register responses + if (!valid && outgoing()) { + JBClientStream* c2s = clientStream(); + valid = c2s && c2s->isRegisterId(*xml); + } + if (!valid) { + terminate(0,false,xml,XMPPError::NotAuthorized, + "Can't accept stanza on non authorized stream"); + return false; + } + } + + switch (m_type) { + case c2s: + if (m_incoming) { + // Check for valid from + if (from && !m_remote.match(from)) { + XmlElement* e = XMPPUtils::createError(xml, + XMPPError::TypeModify,XMPPError::BadAddressing); + sendStanza(e); + return false; + } + // Make sure the upper layer always has the full jid + if (!from) + from = m_remote; + else if (!from.resource()) + from.resource(m_remote.resource()); + } + else { + XDebug(this,DebugStub, + "Possible checkStanzaRecv() unhandled outgoing c2s stream [%p]",this); + } + break; + case s2s: + // RFC 3920bis 9.1.1.2 and 9.1.2.1: + // Validate 'to' and 'from' + if (!(to && from)) { + terminate(0,m_incoming,xml,XMPPError::BadAddressing); + return false; + } + if (!m_engine->hasDomain(to.domain())) { + terminate(0,m_incoming,xml,XMPPError::HostUnknown); + return false; + } + if (from.domain() != m_remote.domain()) { + terminate(0,m_incoming,xml,XMPPError::InvalidFrom); + return false; + } + break; + default: + Debug(this,DebugStub,"checkStanzaRecv() unhandled stream type=%s [%p]", + typeName(),this); + } + return true; +} + +// Change stream state. Reset state depending data +void JBStream::changeState(State newState, u_int64_t time) +{ + if (newState == m_state) + return; + DDebug(this,DebugAll,"Changing state from '%s' to '%s' [%p]", + stateName(),lookup(newState,s_stateName),this); + // Always reset the idle timer: something happened + m_idleTimeout = m_engine->m_idleTimeout ? time + m_engine->m_idleTimeout : 0; + // Set/reset state depending data + switch (m_state) { + case WaitStart: + m_startTimeout = 0; + break; + case Securing: + m_flags |= StreamSecured; + socketSetCanRead(true); + break; + case Connecting: + m_connectTimeout = 0; + break; + case Register: + if (type() == c2s) + clientStream()->m_registerReq = 0; + break; + default: ; + } + switch (newState) { + case WaitStart: + if (m_engine->m_setupTimeout) + m_setupTimeout = time + timerMultiplier(this) * m_engine->m_setupTimeout; + else + m_setupTimeout = 0; + m_startTimeout = time + timerMultiplier(this) * m_engine->m_startTimeout; + DDebug(this,DebugAll,"Set timeouts start=" FMT64 " setup=" FMT64 " [%p]", + m_startTimeout,m_setupTimeout,this); + if (m_xmlDom) { + m_xmlDom->reset(); + DDebug(this,DebugAll,"XML parser reset [%p]",this); + } + break; + case Idle: + m_events.clear(); + case Destroy: + m_id = ""; + m_setupTimeout = 0; + m_startTimeout = 0; + // Reset all internal flags + m_flags &= ~InternalFlags; + if (type() == c2s) + clientStream()->m_registerReq = 0; + break; + case Running: + m_flags |= StreamSecured | StreamAuthenticated; + m_setupTimeout = 0; + m_startTimeout = 0; + if (m_state != Running) + m_events.append(new JBEvent(JBEvent::Running,this,0)); + break; + case Connecting: + if (m_engine->m_connectTimeout) + m_connectTimeout = time + m_engine->m_connectTimeout; + else + m_connectTimeout = 0; + DDebug(this,DebugAll,"Set connect timeout " FMT64 " [%p]",m_connectTimeout,this); + break; + case Securing: + socketSetCanRead(false); + break; + default: ; + } + m_state = newState; +} + +// Check for pending events. Set the last event +void JBStream::checkPendingEvent() +{ + if (m_lastEvent) + return; + if (!m_terminateEvent) { + GenObject* gen = m_events.remove(false); + if (gen) + m_lastEvent = static_cast(gen); + return; + } + // Check for register events and raise them before the terminate event + for (ObjList* o = m_events.skipNull(); o; o = o->skipNext()) { + JBEvent* ev = static_cast(o->get()); + if (ev->type() == JBEvent::RegisterOk || ev->type() == JBEvent::RegisterFailed) { + m_lastEvent = ev; + m_events.remove(ev,false); + return; + } + } + m_lastEvent = m_terminateEvent; + m_terminateEvent = 0; +} + +// Send pending stream XML or stanzas +bool JBStream::sendPending(bool streamOnly) +{ + if (!m_socket) + return false; + XDebug(this,DebugAll,"JBStream::sendPending() [%p]",this); + // Always try to send pending stream XML first + if (m_outStreamXml) { + unsigned int len = m_outStreamXml.length(); + if (!writeSocket(m_outStreamXml.c_str(),len)) { + terminate(0,m_incoming,0,XMPPError::SocketError); + return false; + } + bool all = (len == m_outStreamXml.length()); + if (all) + m_outStreamXml.clear(); + else + m_outStreamXml = m_outStreamXml.substr(len); + // Start TLS now for incoming streams + if (m_incoming && m_state == Securing) { + if (all) { + m_engine->encryptStream(this); + m_flags |= StreamTls; + socketSetCanRead(true); + } + return true; + } + if (streamOnly || !all) + return true; + } + + // Send pending stanzas + if (m_state != Running || streamOnly) + return true; + ObjList* obj = m_pending.skipNull(); + if (!obj) + return true; + XmlElementOut* eout = static_cast(obj->get()); + XmlElement* xml = eout->element(); + if (!xml) { + m_pending.remove(eout,true); + return true; + } + // Print the element only if it's the first time we try to send it + if (!eout->sent()) + m_engine->printXml(this,true,*xml); + u_int32_t len; + const char* data = eout->getData(len); + if (writeSocket(data,len)) { + // Adjust element's buffer. Remove it from list on completion + eout->dataSent(len); + unsigned int rest = eout->dataCount(); + if (!rest) { + DDebug(this,DebugAll,"Sent element (%p,%s) [%p]",xml,xml->tag(),this); + m_pending.remove(eout,true); + } + else + DDebug(this,DebugAll,"Partially sent element (%p,%s) sent=%u rest=%u [%p]", + xml,xml->tag(),len,rest,this); + return true; + } + // Error + Debug(this,DebugNote,"Failed to send (%p,%s) in state=%s [%p]", + xml,xml->tag(),stateName(),this); + terminate(0,m_incoming,0,XMPPError::SocketError); + return false; +} + +// Write data to socket +bool JBStream::writeSocket(const char* data, unsigned int& len) +{ + if (!(data && m_socket)) { + len = 0; + return m_socket != 0; + } + Lock lock(this); + if (!m_socket) { + len = 0; + return false; + } + socketSetWriting(true); + lock.drop(); + XDebug(this,DebugInfo,"Sending %s [%p]",data,this); + int w = m_socket->writeData(data,len); + if (w != Socket::socketError()) + len = w; + else + len = 0; +#ifdef XDEBUG + String sent(data,len); + Debug(this,DebugInfo,"Sent %s [%p]",sent.c_str(),this); +#endif + Lock lck(this); + // Check if something changed + if (!(m_socket && socketWriting())) { + Debug(this,DebugAll,"Socket deleted while writing [%p]",this); + return true; + } + socketSetWriting(false); + if (w != Socket::socketError() || m_socket->canRetry()) + return true; + String tmp; + Thread::errorString(tmp,m_socket->error()); + Debug(this,DebugWarn,"Socket send error: %d: '%s' [%p]", + m_socket->error(),tmp.c_str(),this); + lck.drop(); + // Terminate the connection now: avoid loop back + resetConnection(); + return false; +} + +// Update stream flags and remote connection data from engine +void JBStream::updateFromRemoteDef() + { + m_engine->lock(); + JBRemoteDomainDef* domain = m_engine->remoteDomainDef(m_remote.domain()); + // Update flags + m_flags = (domain->m_flags & StreamFlags); + // Update connection data + if (outgoing() && state() == Idle) { + m_connectAddr = domain->m_address; + m_connectPort = domain->m_port; + } + m_engine->unlock(); +} + +// Retrieve the first required feature in the list +XMPPFeature* JBStream::firstRequiredFeature() +{ + for (ObjList* o = m_features.skipNull(); o; o = o->skipNext()) { + XMPPFeature* f = static_cast(o->get()); + if (f->required()) + return f; + } + return 0; +} + +// Drop (delete) received XML element +bool JBStream::dropXml(XmlElement*& xml, const char* reason) +{ + if (!xml) + return true; + Debug(this,DebugStub,"Dropping xml=(%p,%s) ns=%s in state=%s reason='%s' [%p]", + xml,xml->tag(),TelEngine::c_safe(xml->xmlns()),stateName(),reason,this); + TelEngine::destruct(xml); + return true; +} + +// Process incoming elements in Challenge state +// Return false if stream termination was initiated +bool JBStream::processChallenge(XmlElement* xml, const JabberID& from, const JabberID& to) +{ + int t, n; + if (!XMPPUtils::getTag(*xml,t,n)) + return dropXml(xml,"failed to retrieve element tag"); + if (n != XMPPNamespace::Sasl) + return dropXml(xml,"expecting sasl namespace"); + if (t == XmlTag::Abort) { + TelEngine::destruct(xml); + TelEngine::destruct(m_sasl); + XmlElement* rsp = XMPPUtils::createFailure(XMPPNamespace::Sasl,XMPPError::Aborted); + sendStreamXml(Features,rsp); + return true; + } + if (t != XmlTag::Response) { + Debug(this,DebugStub,"Unhandled SASL '%s' in %s state [%p]", + xml->tag(),stateName(),this); + TelEngine::destruct(xml); + return true; + } + XMPPError::Type error = XMPPError::NoError; + // Use a while() to set error and break to the end + while (true) { + // Decode non empty auth data + const String& text = xml->getText(); + if (text) { + String tmp; + if (!decodeBase64(tmp,text,this)) { + error = XMPPError::IncorrectEnc; + break; + } + if (m_sasl && !m_sasl->parseMD5ChallengeRsp(tmp)) { + error = XMPPError::MalformedRequest; + break; + } + } + else if (m_sasl) + TelEngine::destruct(m_sasl->m_params); + break; + } + if (error == XMPPError::NoError) { + changeState(Auth); + m_events.append(new JBEvent(JBEvent::Auth,this,xml,from,to)); + } + else { + Debug(this,DebugNote,"Received challenge response error='%s' [%p]", + XMPPUtils::s_error[error].c_str(),this); + XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl,error); + sendStreamXml(Features,failure); + TelEngine::destruct(xml); + } + return true; +} + +// Process incoming 'auth' elements qualified by SASL namespace +// Return false if stream termination was initiated +bool JBStream::processSaslAuth(XmlElement* xml, const JabberID& from, const JabberID& to) +{ + if (!xml) + return true; + if (!XMPPUtils::isTag(*xml,XmlTag::Auth,XMPPNamespace::Sasl)) + return dropXml(xml,"expecting 'auth' in sasl namespace"); + XMPPFeatureSasl* sasl = static_cast(m_features.get(XMPPNamespace::Sasl)); + TelEngine::destruct(m_sasl); + XMPPError::Type error = XMPPError::NoError; + const char* mName = xml->attribute("mechanism"); + int mech = XMPPUtils::authMeth(mName); + // Use a while() to set error and break to the end + while (true) { + if (!sasl->mechanism(mech)) { + error = XMPPError::InvalidMechanism; + break; + } + if (mech == XMPPUtils::AuthMD5) { + // Ignore auth text: we will challenge the client + m_sasl = new SASL(false,m_local.domain()); + String buf; + if (m_sasl->buildMD5Challenge(buf)) { + XDebug(this,DebugAll,"Sending challenge=%s [%p]",buf.c_str(),this); + Base64 b((void*)buf.c_str(),buf.length()); + b.encode(buf); + XmlElement* chg = XMPPUtils::createElement(XmlTag::Challenge, + XMPPNamespace::Sasl,buf); + if (!sendStreamXml(Challenge,chg)) { + TelEngine::destruct(xml); + return false; + } + } + else { + TelEngine::destruct(m_sasl); + error = XMPPError::TempAuthFailure; + break; + } + } + else if (mech == XMPPUtils::AuthPlain) { + // Decode non empty auth data + DataBlock d; + const String& text = xml->getText(); + if (text && text != "=" && !decodeBase64(d,text)) { + error = XMPPError::IncorrectEnc; + break; + } + m_sasl = new SASL(true); + if (!m_sasl->parsePlain(d)) { + error = XMPPError::MalformedRequest; + break; + } + } + else { + // This should never happen: we don't handle a mechanism sent + // to the remote party! + Debug(this,DebugWarn,"Unhandled advertised auth mechanism='%s' [%p]", + mName,this); + error = XMPPError::TempAuthFailure; + break; + } + break; + } + if (error == XMPPError::NoError) { + // Challenge state: we've challenged the remote party + // Otherwise: request auth from upper layer + if (state() == Challenge) + TelEngine::destruct(xml); + else { + changeState(Auth); + m_events.append(new JBEvent(JBEvent::Auth,this,xml,from,to)); + } + } + else { + Debug(this,DebugNote,"Received auth request mechanism='%s' error='%s' [%p]", + mName,XMPPUtils::s_error[error].c_str(),this); + XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl,error); + sendStreamXml(m_state,failure); + TelEngine::destruct(xml); + } + return true; +} + +// Process received elements in Features state (incoming stream) +// Return false if stream termination was initiated +bool JBStream::processFeaturesIn(XmlElement* xml, const JabberID& from, const JabberID& to) +{ + if (!xml) + return true; + const String* t = 0; + const String* nsName = 0; + if (!xml->getTag(t,nsName)) + return dropXml(xml,"invalid tag namespace prefix"); + int ns = nsName ? XMPPUtils::s_ns[*nsName] : XMPPNamespace::Count; + // Check if received unexpected feature + if (!m_features.get(ns)) { + // Check for some features that can be negotiated via 'iq' elements + if (m_type == c2s && *t == XMPPUtils::s_tag[XmlTag::Iq] && ns == m_xmlns) { + XmlElement* child = xml->findFirstChild(); + int chNs = child ? XMPPUtils::ns(*child) : XMPPNamespace::Count; + bool bindOk = chNs == XMPPNamespace::Bind && m_features.get(XMPPNamespace::Bind); + bool regOk = !bindOk && chNs == XMPPNamespace::IqRegister; + // Bind + if (bindOk) { + // We've sent bind feature + // Don't accept it if not authenticated and TLS/SASL must be negotiated + if (!flag(StreamAuthenticated)) { + XMPPFeature* tls = m_features.get(XMPPNamespace::Tls); + XMPPFeature* sasl = m_features.get(XMPPNamespace::Sasl); + if ((tls && tls->required()) || (sasl && sasl->required())) { + XmlElement* e = XMPPUtils::createError(xml,XMPPError::TypeAuth, + XMPPError::NotAllowed); + sendStreamXml(m_state,e); + return true; + } + } + // Remove TLS/SASL features from list: they can't be negotiated anymore + m_flags |= StreamSecured | StreamAuthenticated; + m_features.remove(XMPPNamespace::Tls); + m_features.remove(XMPPNamespace::Sasl); + changeState(Running); + return processRunning(xml,from,to); + } + // Register + if (regOk) { + m_events.append(new JBEvent(JBEvent::Iq,this,xml,xml->findFirstChild())); + return true; + } + } + // s2s waiting for dialback + if (m_type == s2s) { + if (flag(TlsRequired) && !flag(StreamSecured)) + return destroyDropXml(xml,XMPPError::EncryptionRequired, + "required encryption not supported by remote"); + if (*t != s_dbResult || ns != XMPPNamespace::Dialback) + return dropXml(xml,"expecting dialback result"); + // Auth data + m_local = to; + m_remote = from; + if (!(m_local && engine()->hasDomain(m_local))) + return destroyDropXml(xml,XMPPError::HostUnknown, + "dialback result with unknown 'to' domain"); + if (!m_remote) + return destroyDropXml(xml,XMPPError::BadAddressing, + "dialback result with empty 'from' domain"); + const char* key = xml->getText(); + if (TelEngine::null(key)) + return destroyDropXml(xml,XMPPError::NotAcceptable, + "dialback result with empty key"); + m_flags |= StreamSecured; + changeState(Auth); + JBEvent* ev = new JBEvent(JBEvent::DbResult,this,xml,from,to); + ev->m_text = key; + m_events.append(ev); + return true; + } + // Check if all remaining features are optional + XMPPFeature* req = firstRequiredFeature(); + if (req) { + Debug(this,DebugInfo, + "Received '%s' while having '%s' required feature not negotiated [%p]", + xml->tag(),req->c_str(),this); + // TODO: terminate the stream? + return dropXml(xml,"required feature negotiation not completed"); + } + // No more required features: change state to Running + // Remove TLS/SASL features from list: they can't be negotiated anymore + m_flags |= StreamSecured | StreamAuthenticated; + m_features.remove(XMPPNamespace::Tls); + m_features.remove(XMPPNamespace::Sasl); + changeState(Running); + return processRunning(xml,from,to); + } + // Stream enchryption + if (ns == XMPPNamespace::Tls) { + if (*t != XMPPUtils::s_tag[XmlTag::Starttls]) + return dropXml(xml,"expecting tls 'starttls' element"); + if (!flag(StreamSecured)) { + // Change state before trying to send the element + // to signal to sendPending() to enchrypt the stream after sending it + changeState(Securing); + sendStreamXml(WaitStart, + XMPPUtils::createElement(XmlTag::Proceed,XMPPNamespace::Tls)); + } + else { + Debug(this,DebugNote,"Received '%s' element while already secured [%p]", + xml->tag(),this); + // We shouldn't have Starttls in features list + // Something went wrong: terminate the stream + terminate(0,true,xml,XMPPError::Internal,"Stream already secured"); + return false; + } + return true; + } + // Stream auth + if (ns == XMPPNamespace::Sasl) { + if (*t != XMPPUtils::s_tag[XmlTag::Auth]) + return dropXml(xml,"expecting sasl 'auth' element"); + if (!flag(StreamAuthenticated)) { + // Check if we must negotiate TLS before authentication + XMPPFeature* tls = m_features.get(XMPPNamespace::Tls); + if (tls) { + if (!flag(StreamSecured) && tls->required()) { + TelEngine::destruct(xml); + XmlElement* failure = XMPPUtils::createFailure(XMPPNamespace::Sasl, + XMPPError::EncryptionRequired); + sendStreamXml(m_state,failure); + return true; + } + setSecured(); + } + } + else { + // Remote party requested authentication while already done: + // Reset our flag and let it authenticate again + Debug(this,DebugNote, + "Received auth request while already authenticated [%p]", + this); + m_flags &= ~StreamAuthenticated; + } + return processSaslAuth(xml,from,to); + } + return dropXml(xml,"unhandled stream feature"); +} + +// Process received elements in Features state (outgoing stream) +// Return false if stream termination was initiated +bool JBStream::processFeaturesOut(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + if (!xml) + return true; + if (!XMPPUtils::isTag(*xml,XmlTag::Features,XMPPNamespace::Stream)) + return dropXml(xml,"expecting stream features"); + m_features.fromStreamFeatures(*xml); + // Check TLS + if (!flag(StreamSecured)) { + XMPPFeature* tls = m_features.get(XMPPNamespace::Tls); + if (tls) { + TelEngine::destruct(xml); + XmlElement* x = XMPPUtils::createElement(XmlTag::Starttls, + XMPPNamespace::Tls); + return sendStreamXml(WaitTlsRsp,x); + } + if (flag(TlsRequired)) + return destroyDropXml(xml,XMPPError::EncryptionRequired, + "required encryption not supported by remote"); + m_flags |= StreamSecured; + } + // Check auth + if (!flag(StreamAuthenticated)) { + JBServerStream* server = serverStream(); + if (server) { + TelEngine::destruct(xml); + return server->sendDialback(); + } + JBClientStream* client = clientStream(); + if (client) { + // Start auth or request registration data + TelEngine::destruct(xml); + if (!flag(RegisterUser)) + return client->startAuth(); + return client->requestRegister(false); + } + } + JBClientStream* client = clientStream(); + if (client) { + TelEngine::destruct(xml); + return client->bind(); + } + return dropXml(xml,"incomplete features process for outgoing stream"); +} + +// Process received elements in WaitTlsRsp state (outgoing stream) +// The element will be consumed +// Return false if stream termination was initiated +bool JBStream::processWaitTlsRsp(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + if (!xml) + return true; + int t,n; + const char* reason = 0; + if (XMPPUtils::getTag(*xml,t,n)) { + if (n == XMPPNamespace::Tls) { + // Accept proceed and failure + if (t != XmlTag::Proceed && t != XmlTag::Failure) + reason = "expecting tls 'proceed' or 'failure'"; + } + else + reason = "expecting tls namespace"; + } + else + reason = "failed to retrieve element tag"; + if (reason) { + // TODO: Unacceptable response to starttls request + // Restart socket read or terminate the stream ? + socketSetCanRead(true); + return dropXml(xml,reason); + } + if (t == XmlTag::Proceed) { + TelEngine::destruct(xml); + changeState(Securing); + m_engine->encryptStream(this); + socketSetCanRead(true); + m_flags |= StreamTls; + XmlElement* s = buildStreamStart(); + return sendStreamXml(WaitStart,s); + } + // TODO: Implement TLS usage reset if the stream is going to re-connect + terminate(1,false,xml,XMPPError::NoError,"Server can't start TLS"); + return false; +} + +// Set stream namespace from type +void JBStream::setXmlns() +{ + switch (m_type) { + case c2s: + m_xmlns = XMPPNamespace::Client; + break; + case s2s: + m_xmlns = XMPPNamespace::Server; + break; + } +} + +// Event termination notification +void JBStream::eventTerminated(const JBEvent* ev) +{ + if (ev && ev == m_lastEvent) { + m_lastEvent = 0; + XDebug(this,DebugAll,"Event (%p,%s) terminated [%p]",ev,ev->name(),this); + } +} + + +/* + * JBClientStream + */ +JBClientStream::JBClientStream(JBEngine* engine, Socket* socket) + : JBStream(engine,socket,c2s), + m_userData(0), m_registerReq(0) +{ +} + +JBClientStream::JBClientStream(JBEngine* engine, const JabberID& jid, const String& account, + const NamedList& params) + : JBStream(engine,c2s,jid,jid.domain(),account,¶ms), + m_userData(0), m_registerReq(0) +{ + m_password = params.getValue("password"); +} + +// Bind a resource to an incoming stream +void JBClientStream::bind(const String& resource, const char* id, XMPPError::Type error) +{ + DDebug(this,DebugAll,"bind(%s,'%s') [%p]",resource.c_str(), + XMPPUtils::s_error[error].c_str(),this); + Lock lock(this); + if (!incoming() || m_remote.resource()) + return; + XmlElement* xml = 0; + if (resource) { + m_remote.resource(resource); + xml = XMPPUtils::createIq(XMPPUtils::IqResult,0,0,id); + XmlElement* bind = XMPPUtils::createElement(XmlTag::Bind, + XMPPNamespace::Bind); + bind->addChild(XMPPUtils::createElement(XmlTag::Jid,m_remote)); + xml->addChild(bind); + } + else { + if (error == XMPPError::NoError) + error = XMPPError::NotAllowed; + xml = XMPPUtils::createError(XMPPError::TypeModify,error); + } + // Remove non-negotiable bind feature on success + if (sendStanza(xml) && resource) + m_features.remove(XMPPNamespace::Bind); +} + +// Request account setup (or info) an outgoing stream +bool JBClientStream::requestRegister(bool data, bool set, const String& newPass) +{ + if (incoming()) + return true; + + Lock lock(this); + DDebug(this,DebugAll,"requestRegister(%u,%u) [%p]",data,set,this); + XmlElement* req = 0; + if (data) { + // Register new user, change the account or remove it + if (set) { + // TODO: Allow user account register/change through unsecured streams ? + String* pass = 0; + if (!flag(StreamAuthenticated)) + pass = &m_password; + else if (newPass) { + m_newPassword = newPass; + pass = &m_newPassword; + } + if (!pass) + return false; + m_registerReq = '2'; + req = XMPPUtils::createRegisterQuery(0,0,String(m_registerReq), + m_local.node(),*pass); + } + else if (flag(StreamAuthenticated)) { + m_registerReq = '3'; + req = XMPPUtils::createRegisterQuery(XMPPUtils::IqSet,0,0, + String(m_registerReq),XMPPUtils::createElement(XmlTag::Remove)); + } + else + return false; + } + else { + // Request register info + m_registerReq = '1'; + req = XMPPUtils::createRegisterQuery(XMPPUtils::IqGet,0,0,String(m_registerReq)); + } + if (!flag(StreamAuthenticated) || state() != Running) + return sendStreamXml(Register,req); + return sendStanza(req); +} + +// Process elements in Running state +bool JBClientStream::processRunning(XmlElement* xml, const JabberID& from, const JabberID& to) +{ + if (!xml) + return true; + // Check if a resource was bound to an incoming stream + // Accept only 'iq' with bind namespace only if we've sent 'bind' feature + if (incoming()) { + if (!m_remote.resource()) { + if (XMPPUtils::isTag(*xml,XmlTag::Iq,m_xmlns)) { + XmlElement* child = XMPPUtils::findFirstChild(*xml,XmlTag::Bind,XMPPNamespace::Bind); + if (child && m_features.get(XMPPNamespace::Bind)) { + m_events.append(new JBEvent(JBEvent::Bind,this,xml,from,to,child)); + return true; + } + } + XmlElement* e = XMPPUtils::createError(xml,XMPPError::TypeCancel, + XMPPError::NotAllowed,"No resource bound to the stream"); + sendStanza(e); + return true; + } + } + else if (m_registerReq && XMPPUtils::isTag(*xml,XmlTag::Iq,m_xmlns) && + isRegisterId(*xml) && XMPPUtils::isResponse(*xml)) + return processRegister(xml,from,to); + return JBStream::processRunning(xml,from,to); +} + +// Process received elements in WaitStart state +// WaitStart: Incoming: waiting for stream start +// Outgoing: idem (our stream start was already sent) +// Return false if stream termination was initiated +bool JBClientStream::processStart(const XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + XDebug(this,DebugAll,"JBClientStream::processStart(%s) [%p]",xml->tag(),this); + + // Check element + if (!processStreamStart(xml)) + return false; + + // RFC3920 5.3.1: + // The 'from' attribute must be set for response stream start + if (outgoing()) { + if (from.null()) { + Debug(this,DebugNote,"Received '%s' with empty 'from' [%p]",xml->tag(),this); + terminate(0,false,0,XMPPError::BadAddressing,"Missing 'from' attribute"); + return false; + } + } + else { + if (!flag(StreamAuthenticated)) { + m_remote.set(from); + m_local.set(to); + } + } + m_remote.resource(""); + // RFC3920 5.3.1: The 'to' attribute must always be set + // RFC3920: The 'to' attribute is optional + bool validTo = !to.null(); + if (validTo) { + if (outgoing()) + validTo = (m_local.bare() == to); + else + validTo = engine()->hasDomain(to.domain()); + } +#ifdef RFC3920 + else + validTo = outgoing(); +#endif + if (!validTo) { + Debug(this,DebugNote,"Received '%s' with invalid to='%s' [%p]", + xml->tag(),to.c_str(),this); + terminate(0,false,0, + to.null() ? XMPPError::BadAddressing : XMPPError::HostUnknown, + "Invalid 'to' attribute"); + return false; + } + if (incoming()) { + m_events.append(new JBEvent(JBEvent::Start,this,0,from,to)); + return true; + } + // Wait features ? + if (flag(StreamRemoteVer1)) { + changeState(Features); + return true; + } + Debug(this,DebugStub,"Outgoing client stream: unsupported remote version (expecting 1.x)"); + terminate(0,true,0,XMPPError::Internal,"Unsupported version"); + return false; +} + +// Process elements in Auth state +bool JBClientStream::processAuth(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + if (!xml) + return true; + if (incoming()) + return destroyDropXml(xml,XMPPError::Internal,"invalid state for incoming stream"); + int t,n; + if (!XMPPUtils::getTag(*xml,t,n)) + return destroyDropXml(xml,XMPPError::Internal,"failed to retrieve element tag"); + + // Authenticating + if (!flag(StreamAuthenticated)) { + // TODO: The server might challenge us again + // Implement support for multiple challenge/response steps + if (n != XMPPNamespace::Sasl) + return destroyDropXml(xml,XMPPError::InvalidNamespace, + "element with non SASL namespace"); + if (!m_sasl) + return destroyDropXml(xml,XMPPError::Internal,"no SASL data"); + if (t == XmlTag::Failure) { + terminate(0,true,xml); + return false; + } + if (!m_sasl->m_plain) { + // Digest MD5 + if (flag(StreamWaitChallenge)) { + if (t != XmlTag::Challenge) + return destroyDropXml(xml,XMPPError::BadRequest,"expecting challenge"); + String tmp; + if (!decodeBase64(tmp,xml->getText(),this)) + return destroyDropXml(xml,XMPPError::IncorrectEnc, + "challenge with incorrect encoding"); + if (!m_sasl->parseMD5Challenge(tmp)) + return destroyDropXml(xml,XMPPError::MalformedRequest, + "invalid challenge format"); + TelEngine::destruct(xml); + m_sasl->setAuthParams(m_local.node(),m_password); + tmp.clear(); + m_sasl->buildAuthRsp(tmp,"xmpp/" + m_local.domain()); + m_flags &= ~StreamWaitChallenge; + m_flags |= StreamWaitChgRsp; + XmlElement* rsp = XMPPUtils::createElement(XmlTag::Response,XMPPNamespace::Sasl,tmp); + return sendStreamXml(state(),rsp); + } + // Digest MD5 response reply + if (flag(StreamWaitChgRsp)) { +#ifdef RFC3920 + // Expect success or challenge + // challenge is accepted if not already received one + if (t != XmlTag::Success && (t != XmlTag::Challenge || flag(StreamRfc3920Chg))) +#else + // Expect success + if (t != XmlTag::Success) +#endif + return dropXml(xml,"unexpected element"); + if (!flag(StreamRfc3920Chg)) { + String rspAuth; + if (!decodeBase64(rspAuth,xml->getText(),this)) + return destroyDropXml(xml,XMPPError::IncorrectEnc, + "challenge response reply with incorrect encoding"); + if (!rspAuth.startSkip("rspauth=",false)) + return destroyDropXml(xml,XMPPError::BadFormat, + "invalid challenge response reply"); + if (!m_sasl->validAuthReply(rspAuth)) + return destroyDropXml(xml,XMPPError::InvalidAuth, + "incorrect challenge response reply auth"); + } +#ifdef RFC3920 + // Send empty response to challenge + if (t == XmlTag::Challenge) { + m_flags |= StreamRfc3920Chg; + TelEngine::destruct(xml); + XmlElement* rsp = XMPPUtils::createElement(XmlTag::Response, + XMPPNamespace::Sasl); + return sendStreamXml(state(),rsp); + } +#endif + m_flags &= ~(StreamWaitChgRsp | StreamRfc3920Chg); + } + else + return dropXml(xml,"unhandled sasl digest md5 state"); + } + else { + // Plain + if (t != XmlTag::Success) + return dropXml(xml,"unexpected element"); + } + // Authenticated. Bind a resource + Debug(this,DebugAll,"Authenticated [%p]",this); + TelEngine::destruct(xml); + TelEngine::destruct(m_sasl); + m_flags |= StreamAuthenticated; + XmlElement* start = buildStreamStart(); + return sendStreamXml(WaitStart,start); + } + + XMPPUtils::IqType iq = XMPPUtils::iqType(xml->attribute("type")); + String* id = xml->getAttribute("id"); + + // Waiting for bind response + if (flag(StreamWaitBindRsp)) { + // Expecting 'iq' result or error + if (t != XmlTag::Iq || + (iq != XMPPUtils::IqResult && iq != XMPPUtils::IqError) || + !id || *id != "bind_1") + return dropXml(xml,"unexpected element"); + if (iq == XMPPUtils::IqError) { + Debug(this,DebugNote,"Resource binding failed [%p]",this); + terminate(0,true,xml); + return false; + } + // Check it + bool ok = false; + while (true) { + XmlElement* bind = XMPPUtils::findFirstChild(*xml,XmlTag::Bind,XMPPNamespace::Bind); + if (!bind) + break; + XmlElement* tmp = bind->findFirstChild(&XMPPUtils::s_tag[XmlTag::Jid]); + if (!tmp) + break; + JabberID jid(tmp->getText()); + if (jid.bare() != m_local.bare()) + break; + ok = true; + if (m_local.resource() != jid.resource()) { + m_local.resource(jid.resource()); + Debug(this,DebugAll,"Resource set to '%s' [%p]", + local().resource().c_str(),this); + } + break; + } + if (!ok) + return destroyDropXml(xml,XMPPError::UndefinedCondition, + "unacceptable bind response"); + m_flags &= ~StreamWaitBindRsp; + TelEngine::destruct(xml); + if (!m_features.get(XMPPNamespace::Session)) { + changeState(Running); + return true; + } + // Send session + XmlElement* sess = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"sess_1"); + sess->addChild(XMPPUtils::createElement(XmlTag::Session,XMPPNamespace::Session)); + m_flags |= StreamWaitSessRsp; + return sendStreamXml(state(),sess); + } + + // Waiting for session response + if (flag(StreamWaitSessRsp)) { + // Expecting 'iq' result or error + if (t != XmlTag::Iq || + (iq != XMPPUtils::IqResult && iq != XMPPUtils::IqError) || + !id || *id != "sess_1") + return dropXml(xml,"unexpected element"); + if (iq == XMPPUtils::IqError) { + Debug(this,DebugNote,"Session failed [%p]",this); + terminate(0,true,xml); + return false; + } + TelEngine::destruct(xml); + m_flags &= ~StreamWaitBindRsp; + changeState(Running); + return true; + } + + return dropXml(xml,"unhandled"); +} + +// Process elements in Register state +bool JBClientStream::processRegister(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + if (!xml) + return true; + int t, ns; + if (!XMPPUtils::getTag(*xml,t,ns)) + return dropXml(xml,"failed to retrieve element tag"); + if (t != XmlTag::Iq) + return dropXml(xml,"expecting 'iq'"); + XMPPUtils::IqType iq = XMPPUtils::iqType(xml->attribute("type")); + if (iq != XMPPUtils::IqResult && iq != XMPPUtils::IqError) + return dropXml(xml,"expecting 'iq' response"); + if (!isRegisterId(*xml)) + return dropXml(xml,"unexpected response id"); + if (iq == XMPPUtils::IqError) { + m_events.append(new JBEvent(JBEvent::RegisterFailed,this,xml,from,to)); + // Don't terminate if the user requested account change after authentication + if (!flag(StreamAuthenticated)) + terminate(0,true,0,XMPPError::NoError); + return flag(StreamAuthenticated); + } + // Requested registration data + if (m_registerReq == '1') { + // XEP-0077: check for username and password children or + // instructions + XmlElement* query = XMPPUtils::findFirstChild(*xml,XmlTag::Query, + XMPPNamespace::IqRegister); + if (query && XMPPUtils::findFirstChild(*query,XmlTag::Username) && + XMPPUtils::findFirstChild(*query,XmlTag::Password)) { + TelEngine::destruct(xml); + return requestRegister(true); + } + m_events.append(new JBEvent(JBEvent::RegisterFailed,this,xml,from,to)); + // Don't terminate if the user requested account change after authentication + if (!flag(StreamAuthenticated)) + terminate(0,true,0,XMPPError::NoError); + return flag(StreamAuthenticated); + } + // Requested registration/change + if (m_registerReq == '2') { + m_events.append(new JBEvent(JBEvent::RegisterOk,this,xml,from,to)); + // Reset register user flag + m_flags &= ~RegisterUser; + // Done if account changed after authentication + if (flag(StreamAuthenticated)) { + m_password = m_newPassword; + return true; + } + // Start auth + changeState(Features); + return startAuth(); + } + // Requested account removal + if (m_registerReq == '3') { + terminate(0,true,xml,XMPPError::Reg,"Account removed"); + return false; + } + return destroyDropXml(xml,XMPPError::Internal,"unhandled state"); +} + +// Release memory +void JBClientStream::destroyed() +{ + userData(0); + JBStream::destroyed(); +} + +// Start outgoing stream authentication +bool JBClientStream::startAuth() +{ + if (incoming() || state() != Features) + return false; + + TelEngine::destruct(m_sasl); + + XMPPFeature* f = m_features.get(XMPPNamespace::Sasl); + XMPPFeatureSasl* sasl = 0; + if (f) + sasl = static_cast(f->getObject("XMPPFeatureSasl")); + if (!sasl) { + terminate(0,true,0,XMPPError::NoError,"Missing authentication data"); + return false; + } + + // RFC 3920 SASL auth + int mech = XMPPUtils::AuthNone; + if (sasl->mechanism(XMPPUtils::AuthMD5)) + mech = XMPPUtils::AuthMD5; + else if (sasl->mechanism(XMPPUtils::AuthPlain) && flag(AllowPlainAuth)) + mech = XMPPUtils::AuthPlain; + else { + terminate(0,true,0,XMPPError::NoError,"Unsupported authentication mechanism"); + return false; + } + + m_sasl = new SASL(mech == XMPPUtils::AuthPlain); + String rsp; + if (m_sasl->m_plain) { + m_sasl->setAuthParams(m_local.node(),m_password); + if (!m_sasl->buildAuthRsp(rsp)) { + terminate(0,true,0,XMPPError::NoError,"Invalid auth data length for plain auth"); + return false; + } + } + else + m_flags |= StreamWaitChallenge; + // MD5: send auth element, wait challenge + // Plain auth: send auth element with credentials and wait response (success/failure) + XmlElement* xml = XMPPUtils::createElement(XmlTag::Auth,XMPPNamespace::Sasl,rsp); + xml->setAttribute("mechanism",lookup(mech,XMPPUtils::s_authMeth)); + return sendStreamXml(Auth,xml); +} + +// Start resource binding on outgoing stream +bool JBClientStream::bind() +{ + Debug(this,DebugAll,"Binding resource [%p]",this); + XmlElement* bind = XMPPUtils::createElement(XmlTag::Bind,XMPPNamespace::Bind); + if (m_local.resource()) + bind->addChild(XMPPUtils::createElement(XmlTag::Resource,m_local.resource())); + XmlElement* b = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"bind_1"); + b->addChild(bind); + m_flags |= StreamWaitBindRsp; + return sendStreamXml(Auth,b); +} + + +/* + * JBServerStream + */ +// Build an incoming stream from a socket +JBServerStream::JBServerStream(JBEngine* engine, Socket* socket) + : JBStream(engine,socket,s2s), + m_dbKey(0) +{ +} + +// Build an outgoing stream +JBServerStream::JBServerStream(JBEngine* engine, const JabberID& local, + const JabberID& remote, const char* dbId, const char* dbKey, bool dbOnly) + : JBStream(engine,s2s,local,remote), + m_dbKey(0) +{ + if (!(TelEngine::null(dbId) || TelEngine::null(dbKey))) + m_dbKey = new NamedString(dbId,dbKey); + if (dbOnly) + m_flags |= DialbackOnly | NoAutoRestart; +} + +// Send a dialback key response. +// If the stream is in Dialback state change it's state to Running if valid or +// terminate it if invalid +bool JBServerStream::sendDbResult(const JabberID& from, const JabberID& to, bool valid) +{ + if (incoming() && state() == Auth && from == m_local && to == m_remote) + return authenticated(valid); + XmlElement* rsp = XMPPUtils::createDialbackResult(from,to,valid); + return sendStanza(rsp); +} + +// Send dialback data (key/verify) +bool JBServerStream::sendDialback() +{ + State newState = Running; + XmlElement* result = 0; + if (!flag(DialbackOnly)) { + if (flag(StreamAuthenticated)) + newState = Running; + else { + String key; + engine()->buildDialbackKey(id(),key); + result = XMPPUtils::createDialbackKey(m_local,m_remote,key); + newState = Auth; + } + } + else if (!m_dbKey) { + // Dialback only with no key? + Debug(this,DebugGoOn,"Outgoing dialback stream with no key! [%p]",this); + terminate(0,true,0,XMPPError::Internal); + return false; + } + if (m_dbKey) { + XmlElement* db = XMPPUtils::createDialbackVerify(m_local,m_remote, + m_dbKey->name(),*m_dbKey); + if (result) + return sendStreamXml(newState,result,db); + return sendStreamXml(newState,db); + } + if (result) + return sendStreamXml(newState,result); + changeState(newState); + return true; +} + +// Release memory +void JBServerStream::destroyed() +{ + TelEngine::destruct(m_dbKey); + JBStream::destroyed(); +} + +// Process elements in Running state +bool JBServerStream::processRunning(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + if (!xml) + return true; + // Check the tags of known dialback elements: + // there are servers who don't stamp them with the namespace + // Let other elements stamped with dialback namespace go the upper layer + if (isDbResult(*xml)) { + if (outgoing()) + return dropXml(xml,"dialback result on outgoing stream"); + const char* key = xml->getText(); + // Result: accept already authenticated + if (m_local == to && m_remote == from) { + if (TelEngine::null(key)) + return destroyDropXml(xml,XMPPError::BadFormat, + "dialback result with empty key"); + String tmp; + engine()->buildDialbackKey(id(),tmp); + if (tmp == key) { + TelEngine::destruct(xml); + return sendDbResult(m_local,m_remote,true); + } + return destroyDropXml(xml,XMPPError::NotAuthorized, + "dialback result with invalid key"); + } + JBEvent* ev = new JBEvent(JBEvent::DbResult,this,xml,from,to); + ev->m_text = key; + m_events.append(ev); + return true; + } + // Call default handler + return JBStream::processRunning(xml,from,to); +} + +// Build a stream start XML element +XmlElement* JBServerStream::buildStreamStart() +{ + XmlElement* start = new XmlElement(XMPPUtils::s_tag[XmlTag::Stream],false); + if (incoming()) + start->setAttribute("id",m_id); + XMPPUtils::setStreamXmlns(*start); + start->setAttribute(XmlElement::s_ns,XMPPUtils::s_ns[m_xmlns]); + start->setAttribute(XmlElement::s_nsPrefix + "db",XMPPUtils::s_ns[XMPPNamespace::Dialback]); + if (!dialback()) { + start->setAttributeValid("from",m_local.bare()); + start->setAttributeValid("to",m_remote.bare()); + if (!flag(StreamSecured) || flag(TlsRequired)) { + start->setAttribute("version","1.0"); + start->setAttribute("xml:lang","en"); + } + } + return start; +} + +// Process received elements in WaitStart state +// WaitStart: Incoming: waiting for stream start +// Outgoing: idem (our stream start was already sent) +// Return false if stream termination was initiated +bool JBServerStream::processStart(const XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + if (!processStreamStart(xml)) + return true; + + if (outgoing()) { + // Wait features ? + if (flag(StreamRemoteVer1)) { + changeState(Features); + return true; + } + // Stream not secured + if (!flag(StreamSecured)) { + // Accept dialback auth stream + // The namspace presence was already checked in checkStreamStart() + if (flag(TlsRequired)) { + terminate(0,false,0,XMPPError::EncryptionRequired); + return false; + } + m_flags |= StreamSecured; + } + m_flags |= StreamSecured; + return sendDialback(); + } + + // Incoming stream + m_local = to; + m_remote = from; + if (m_local && !engine()->hasDomain(m_local)) { + terminate(0,true,0,XMPPError::HostUnknown); + return false; + } + updateFromRemoteDef(); + m_events.append(new JBEvent(JBEvent::Start,this,0,from,to)); + return true; +} + +// Process elements in Auth state +bool JBServerStream::processAuth(XmlElement* xml, const JabberID& from, + const JabberID& to) +{ + if (incoming()) + return dropXml(xml,"invalid state for incoming stream"); + // Waiting for db:result + if (!isDbResult(*xml)) + return dropXml(xml,"expecting dialback result"); + // Result + // Outgoing stream waiting for dialback key response + if (outgoing()) { + if (m_remote != from || m_local != to) + return destroyDropXml(xml,XMPPError::BadAddressing, + "dialback response with invalid 'from'"); + // Expect dialback key response + String type(xml->getAttribute("type")); + if (type != "valid") { + terminate(1,false,xml,XMPPError::NoError); + return false; + } + // Stream authenticated + TelEngine::destruct(xml); + m_flags |= StreamAuthenticated; + changeState(Running); + return true; + } + return dropXml(xml,"incomplete state process"); +} + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjabber/jgengine.cpp b/libs/yjabber/jgengine.cpp new file mode 100644 index 00000000..de9158ec --- /dev/null +++ b/libs/yjabber/jgengine.cpp @@ -0,0 +1,323 @@ +/** + * jgengine.cpp + * Yet Another Jingle Stack + * This file is part of the YATE Project http://YATE.null.ro + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +using namespace TelEngine; + +const TokenDict JGEvent::s_typeName[] = { + {"Jingle", Jingle}, + {"ResultOk", ResultOk}, + {"ResultError", ResultError}, + {"ResultTimeout", ResultTimeout}, + {"Terminated", Terminated}, + {"Destroy", Destroy}, + {0,0} +}; + + +/* + * JGEngine + */ +// Constructor +JGEngine::JGEngine(const char* name) + : Mutex(true,"JGEngine"), + m_sessionId(1), m_stanzaTimeout(20000), m_pingInterval(300000) +{ + debugName(name); +} + +JGEngine::~JGEngine() +{ +} + +// Create private stream(s) to get events from sessions +void JGEngine::initialize(const NamedList& params) +{ + int lvl = params.getIntValue("debug_level",-1); + if (lvl != -1) + debugLevel(lvl); + + int timeout = params.getIntValue("stanza_timeout",(int)m_stanzaTimeout); + m_stanzaTimeout = timeout > 10000 ? timeout : 10000; + int ping = params.getIntValue("ping_interval",(int)m_pingInterval); + if (ping == 0) + m_pingInterval = 0; + else if (ping < 60000) + m_pingInterval = 60000; + else + m_pingInterval = ping; + // Make sure we don't ping before a ping times out + if (m_pingInterval && m_stanzaTimeout && m_pingInterval <= m_stanzaTimeout) + m_pingInterval = m_stanzaTimeout + 100; + + if (debugAt(DebugInfo)) { + String s; + s << " stanza_timeout=" << (unsigned int)m_stanzaTimeout; + s << " ping_interval=" << (unsigned int)m_pingInterval; + Debug(this,DebugInfo,"Jabber Jingle service initialized:%s [%p]", + s.c_str(),this); + } +} + +// Make an outgoing call +JGSession* JGEngine::call(JGSession::Version ver, const JabberID& caller, + const JabberID& called, const ObjList& contents, XmlElement* extra, + const char* msg, const char* subject) +{ + DDebug(this,DebugAll,"call() from '%s' to '%s'",caller.c_str(),called.c_str()); + JGSession* session = 0; + switch (ver) { + case JGSession::Version1: + session = new JGSession1(this,caller,called); + break; + case JGSession::Version0: + session = new JGSession0(this,caller,called); + break; + case JGSession::VersionUnknown: + Debug(this,DebugNote,"Unhandled session version %d in call()",ver); + return 0; + } + if (session) { + if (!TelEngine::null(msg)) + sendMessage(session,msg); + if (session->initiate(contents,extra,subject)) { + Lock lock(this); + m_sessions.append(session); + return (session && session->ref()) ? session : 0; + } + } + TelEngine::destruct(session); + Debug(this,DebugNote,"Outgoing call from '%s' to '%s' failed to initiate", + caller.c_str(),called.c_str()); + return 0; +} + +// Send a session's stanza. +bool JGEngine::sendStanza(JGSession* session, XmlElement*& stanza) +{ + Debug(this,DebugStub,"JGEngine::sendStanza() not implemented!"); + TelEngine::destruct(stanza); + return false; +} + +// Send a chat message on behalf of a session +bool JGEngine::sendMessage(JGSession* session, const char* body) +{ + XmlElement* x = XMPPUtils::createMessage(XMPPUtils::Chat,0,0,0,body); + return sendStanza(session,x); +} + +// Get events from sessions +JGEvent* JGEngine::getEvent(u_int64_t time) +{ + JGEvent* event = 0; + lock(); + ListIterator iter(m_sessions); + for (;;) { + JGSession* session = static_cast(iter.get()); + // End of iteration? + if (!session) + break; + RefPointer s = session; + // Dead pointer? + if (!s) + continue; + unlock(); + if (0 != (event = s->getEvent(time))) { + if (event->type() == JGEvent::Destroy) { + DDebug(this,DebugAll,"Deleting internal event (%p,Destroy)",event); + delete event; + } + else + return event; + } + lock(); + } + unlock(); + return 0; +} + +// Ask this engine to accept an incoming xml 'iq' element +bool JGEngine::acceptIq(XMPPUtils::IqType type, const JabberID& from, const JabberID& to, + const String& id, XmlElement* xml, XMPPError::Type& error, String& text) +{ + error = XMPPError::NoError; + if (!xml) + return false; + if (type == XMPPUtils::IqResult || type == XMPPUtils::IqError) { + for (ObjList* o = m_sessions.skipNull(); o; o = o->skipNext()) { + JGSession* session = static_cast(o->get()); + if (session->acceptIq(type,from,to,id,xml)) + return true; + } + return false; + } + if (type != XMPPUtils::IqGet && type != XMPPUtils::IqSet) + return false; + // Handle set/get iq + XmlElement* child = xml->findFirstChild(); + if (!child) + return false; + String sid; + JGSession::Version ver = JGSession::VersionUnknown; + int ns = XMPPUtils::xmlns(*child); + bool fileTransfer = false; + // Jingle or file transfer stanzas (jingle stanzas can only have type='set') + // Set version and session id + if (ns == XMPPNamespace::Jingle) { + if (type == XMPPUtils::IqSet) { + ver = JGSession::Version1; + sid = child->getAttribute("sid"); + } + } + else if (ns == XMPPNamespace::JingleSession) { + if (type == XMPPUtils::IqSet) { + ver = JGSession::Version0; + sid = child->getAttribute("id"); + } + } + else if (ns == XMPPNamespace::ByteStreams && XMPPUtils::isUnprefTag(*child,XmlTag::Query)) { + fileTransfer = true; + sid = child->getAttribute("sid"); + } + else + return false; + if (!sid) { + if (!fileTransfer) { + error = XMPPError::BadRequest; + if (type == XMPPUtils::IqSet) + text = "Missing session id attribute"; + } + return false; + } + Lock lock(this); + DDebug(this,DebugAll,"Accepting xml child=%s sid=%s version=%d filetransfer=%u", + child->tag(),sid.c_str(),ver,fileTransfer); + // Check for an existing session destination + for (ObjList* o = m_sessions.skipNull(); o; o = o->skipNext()) { + JGSession* session = static_cast(o->get()); + if (session->acceptIq(type,from,to,sid,xml)) + return true; + } + // Check if this an incoming session request + JGSession* session = 0; + if (ver != JGSession::VersionUnknown) { + JGSession::Action action = JGSession::lookupAction(child->attribute("type"),ver); + if (action == JGSession::ActInitiate) { + switch (ver) { + case JGSession::Version1: + session = new JGSession1(this,to,from,xml,sid); + break; + case JGSession::Version0: + session = new JGSession0(this,to,from,xml,sid); + break; + default: + error = XMPPError::ServiceUnavailable; + Debug(this,DebugStub,"JGEngine::accept(): unhandled session version %d",ver); + } + } + else { + error = XMPPError::Request; + text = "Unknown session"; + } + if (session) + m_sessions.append(session); + return error == XMPPError::NoError; + } + return false; +} + +// Default event processor +void JGEngine::defProcessEvent(JGEvent* event) +{ + if (!event) + return; + DDebug(this,DebugAll,"JGEngine::defprocessEvent. Deleting event (%p,%u)", + event,event->type()); + delete event; +} + +// Process generated events +void JGEngine::processEvent(JGEvent* event) +{ + Debug(this,DebugStub,"JGEngine::processEvent. Calling default processor"); + defProcessEvent(event); +} + +// Create a local session id +void JGEngine::createSessionId(String& id) +{ + Lock lock(this); + id = "JG"; + id << (unsigned int)m_sessionId << "_" << (int)random(); + m_sessionId++; +} + + +/** + * JGEvent + */ +JGEvent::~JGEvent() +{ + if (m_session) { + if (!m_confirmed) + confirmElement(XMPPError::UndefinedCondition,"Unhandled"); + m_session->eventTerminated(this); + TelEngine::destruct(m_session); + } + TelEngine::destruct(releaseXml()); + XDebug(DebugAll,"JGEvent::~JGEvent [%p]",this); +} + +void JGEvent::init(JGSession* session) +{ + XDebug(DebugAll,"JGEvent::JGEvent [%p]",this); + if (session && session->ref()) + m_session = session; + if (m_element) { + m_id = m_element->getAttribute("id"); + if (m_session) + switch (m_session->version()) { + case JGSession::Version1: + m_jingle = XMPPUtils::findFirstChild(*m_element,XmlTag::Jingle); + break; + case JGSession::Version0: + m_jingle = XMPPUtils::findFirstChild(*m_element,XmlTag::Session); + break; + case JGSession::VersionUnknown: + ; + } + } +} + +// Set the jingle action as enumeration. Set confirmation flag if +// the element don't require it +void JGEvent::setAction(JGSession::Action act) +{ + m_action = act; + m_confirmed = !(m_element && act != JGSession::ActCount); +} + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/session.cpp b/libs/yjabber/session.cpp similarity index 54% rename from libs/yjingle/session.cpp rename to libs/yjabber/session.cpp index 0948cc7a..aca769cf 100644 --- a/libs/yjingle/session.cpp +++ b/libs/yjabber/session.cpp @@ -22,33 +22,195 @@ */ #include +#include using namespace TelEngine; +static String s_bandwidth = "bandwidth"; + +const TokenDict JGRtpMediaList::s_media[] = { + {"audio", Audio}, + {0,0} +}; + +const TokenDict JGRtpCandidates::s_type[] = { + {"ice-udp", RtpIceUdp}, + {"raw-udp", RtpRawUdp}, + {0,0}, +}; + +// The list containing the text values for Senders enumeration +const TokenDict JGSessionContent::s_senders[] = { + {"both", SendBoth}, + {"initiator", SendInitiator}, + {"responder", SendResponder}, + {0,0} +}; + +// The list containing the text values for Creator enumeration +const TokenDict JGSessionContent::s_creator[] = { + {"initiator", CreatorInitiator}, + {"responder", CreatorResponder}, + {0,0} +}; + +// Jingle versions +const TokenDict JGSession::s_versions[] = { + {"0", Version0}, + {"1", Version1}, + {0,0} +}; + +// Jingle session states +const TokenDict JGSession::s_states[] = { + {"Idle", Idle}, + {"Pending", Pending}, + {"Active", Active}, + {"Ending", Ending}, + {"Destroy", Destroy}, + {0,0} +}; + +// Jingle termination reasons +const TokenDict JGSession::s_reasons[] = { + // Session terminate + {"success", ReasonOk}, + {"busy", ReasonBusy}, + {"decline", ReasonDecline}, + {"cancel", ReasonCancel}, + {"expired", ReasonExpired}, + {"connectivity-error", ReasonConn}, + {"failed-application", ReasonFailApp}, + {"failed-transport", ReasonFailTransport}, + {"gone", ReasonGone}, + {"incompatible-parameters", ReasonParams}, + {"media-error", ReasonMedia}, + {"unsupported-transports", ReasonTransport}, + {"unsupported-applications", ReasonApp}, + {"general-error", ReasonUnknown}, + {"alternative-session", ReasonAltSess}, + {"timeout", ReasonTimeout}, + {"security-error", ReasonSecurity}, + // Session transfer (XEP 0251) + {"transferred", Transferred}, + // RTP errors + {"crypto-required", CryptoRequired}, + {"invalid-crypto", InvalidCrypto}, + {0,0} +}; + +// RTP session info (XEP 0167) +const TokenDict JGSession::s_rtpInfo[] = { + {"active", RtpActive}, + {"hold", RtpHold}, + {"mute", RtpMute}, + {"ringing", RtpRinging}, + {0,0} +}; + +// Jingle actions for version 0 +const TokenDict JGSession::s_actions0[] = { + {"accept", ActAccept}, + {"initiate", ActInitiate}, + {"terminate", ActTerminate}, + {"reject", ActReject}, + {"info", ActInfo}, + {"transport-info", ActTransportInfo}, + {"transport-accept", ActTransportAccept}, + {"content-info", ActContentInfo}, + {"candidates", ActCandidates}, + {"DTMF", ActDtmf}, + {"ringing", ActRinging}, + {"mute", ActMute}, + {0,0} +}; + +// Jingle actions for version 1 +const TokenDict JGSession::s_actions1[] = { + {"session-accept", ActAccept}, + {"session-initiate", ActInitiate}, + {"session-terminate", ActTerminate}, + {"session-info", ActInfo}, + {"description-info", ActDescriptionInfo}, + {"transport-info", ActTransportInfo}, + {"transport-accept", ActTransportAccept}, + {"transport-reject", ActTransportReject}, + {"transport-replace", ActTransportReplace}, + {"content-accept", ActContentAccept}, + {"content-add", ActContentAdd}, + {"content-modify", ActContentModify}, + {"content-reject", ActContentReject}, + {"content-remove", ActContentRemove}, + {"transfer", ActTransfer}, + {"DTMF", ActDtmf}, + {"ringing", ActRinging}, + {"trying", ActTrying}, + {"received", ActReceived}, + {"hold", ActHold}, + {"active", ActActive}, + {"mute", ActMute}, + {"streamhost", ActStreamHost}, + {0,0} +}; + +// Output a debug message on unhandled actions +// Confirm received element +static void unhandledAction(JGSession* sess, XmlElement*& xml, int act, + XmlElement* ch = 0) +{ + Debug(sess->engine(),DebugStub, + "Call(%s). Unhandled action '%s' child=(%p,%s,%s) [%p]", + sess->sid().c_str(),JGSession::lookupAction(act,sess->version()), + ch,ch ? ch->tag() : 0,ch ? TelEngine::c_safe(ch->xmlns()) : 0,sess); + sess->confirmError(xml,XMPPError::FeatureNotImpl); +} + +// Decode a jingle termination reason +static void decodeJingleReason(XmlElement& xml, const char*& reason, const char*& text) +{ + String* ns = xml.xmlns(); + if (!ns) + return; + XmlElement* res = xml.findFirstChild(&XMPPUtils::s_tag[XmlTag::Reason],ns); + if (!res) + return; + for (XmlElement* r = res->findFirstChild(); r; r = res->findNextChild(r)) { + const String* t; + const String* n; + if (!(r->getTag(t,n) && n && *n == *ns)) + continue; + if (*t != XMPPUtils::s_tag[XmlTag::Text]) + reason = *t; + else + text = r->getText(); + if (reason && text) + return; + } +} + // Utility: add session content(s) to an already created stanza's jingle child -static void addJingleContents(XMLElement* xml, const ObjList& contents, bool minimum, +static void addJingleContents(XmlElement* xml, const ObjList& contents, bool minimum, bool addDesc, bool addTrans, bool addCandidates, bool addAuth = true) { if (!xml) return; - XMLElement* jingle = xml->findFirstChild(XMLElement::Jingle); + XmlElement* jingle = XMPPUtils::findFirstChild(*xml,XmlTag::Jingle); if (!jingle) return; for (ObjList* o = contents.skipNull(); o; o = o->skipNext()) { JGSessionContent* c = static_cast(o->get()); jingle->addChild(c->toXml(minimum,addDesc,addTrans,addCandidates,addAuth)); } - TelEngine::destruct(jingle); } // Utility: add session content(s) to an already created stanza's jingle child // This method is used by the version 0 of the session -static void addJingleContents0(String& name, XMLElement* xml, const ObjList& contents, bool minimal, - bool addDesc, bool addTrans) +static void addJingleContents0(String& name, XmlElement* xml, const ObjList& contents, bool minimal, + bool addDesc, bool addTrans, int action = JGSession::ActCount) { if (!xml) return; - XMLElement* jingle = xml->findFirstChild(XMLElement::Session); + XmlElement* jingle = XMPPUtils::findFirstChild(*xml,XmlTag::Session); if (!jingle) return; for (ObjList* o = contents.skipNull(); o; o = o->skipNext()) { @@ -57,24 +219,30 @@ static void addJingleContents0(String& name, XMLElement* xml, const ObjList& con continue; name = c->toString(); if (addDesc) { - XMLElement* desc = XMPPUtils::createElement(XMLElement::Description, + XmlElement* desc = XMPPUtils::createElement(XmlTag::Description, XMPPNamespace::JingleAudio); for (ObjList* o = c->m_rtpMedia.skipNull(); o; o = o->skipNext()) { JGRtpMedia* a = static_cast(o->get()); - desc->addChild(a->toXML()); + desc->addChild(a->toXml()); } - JGRtpMedia* te = new JGRtpMedia("106","telephone-event","8000","",""); - desc->addChild(te->toXML()); + JGRtpMedia* te = JGRtpMedia::telEvent(); + desc->addChild(te->toXml()); TelEngine::destruct(te); jingle->addChild(desc); } if (addTrans) { - XMLElement* trans = XMPPUtils::createElement(XMLElement::Transport, - XMPPNamespace::JingleTransport); - if (!minimal) { + XmlElement* parent = 0; + if (action == JGSession::ActTransportInfo) { + parent = XMPPUtils::createElement(XmlTag::Transport, + XMPPNamespace::JingleTransport); + jingle->addChild(parent); + } + else if (action == JGSession::ActCandidates) + parent = jingle; + if (!minimal && parent) { for (ObjList* o = c->m_rtpLocalCandidates.skipNull(); o; o = o->skipNext()) { JGRtpCandidate* rc = static_cast(o->get()); - XMLElement* xml = new XMLElement(XMLElement::Candidate); + XmlElement* xml = XMPPUtils::createElement(XmlTag::Candidate); xml->setAttribute("name","rtp"); xml->setAttributeValid("generation",rc->m_generation); xml->setAttributeValid("address",rc->m_address); @@ -85,39 +253,35 @@ static void addJingleContents0(String& name, XMLElement* xml, const ObjList& con xml->setAttribute("password",c->m_rtpLocalCandidates.m_password); xml->setAttributeValid("type","local"); xml->setAttributeValid("preference","1"); - trans->addChild(xml); + parent->addChild(xml); } } - jingle->addChild(trans); } } - TelEngine::destruct(jingle); } // Utility: add xml element child to an already created stanza's jingle child -static void addJingleChild(XMLElement* xml, XMLElement* child) +static void addJingleChild(XmlElement* xml, XmlElement* child) { if (!(xml && child)) return; - XMLElement* jingle = xml->findFirstChild(XMLElement::Jingle); + XmlElement* jingle = XMPPUtils::findFirstChild(*xml,XmlTag::Jingle); if (jingle) jingle->addChild(child); else TelEngine::destruct(child); - TelEngine::destruct(jingle); } // Utility: add xml element child to an already created stanza's jingle child -static void addJingleChild0(XMLElement* xml, XMLElement* child) +static void addJingleChild0(XmlElement* xml, XmlElement* child) { if (!(xml && child)) return; - XMLElement* jingle = xml->findFirstChild(XMLElement::Session); + XmlElement* jingle = XMPPUtils::findFirstChild(*xml,XmlTag::Session); if (jingle) jingle->addChild(child); else TelEngine::destruct(child); - TelEngine::destruct(jingle); } // Utility: add NamedList param only if not empty @@ -129,50 +293,48 @@ static inline void addParamValid(NamedList& list, const char* param, const char* } -/** +/* * JGRtpMedia */ -XMLElement* JGRtpMedia::toXML() const +XmlElement* JGRtpMedia::toXml() const { - XMLElement* p = new XMLElement(XMLElement::PayloadType); + XmlElement* p = XMPPUtils::createElement(XmlTag::PayloadType); p->setAttribute("id",m_id); p->setAttributeValid("name",m_name); p->setAttributeValid("clockrate",m_clockrate); p->setAttributeValid("channels",m_channels); + p->setAttributeValid("ptime",m_pTime); + p->setAttributeValid("maxptime",m_maxPTime); unsigned int n = m_params.length(); for (unsigned int i = 0; i < n; i++) { NamedString* s = m_params.getParam(i); - if (!s) - continue; - XMLElement* param = new XMLElement(XMLElement::Parameter); - param->setAttributeValid("name",s->name()); - param->setAttributeValid("value",*s); - p->addChild(param); + if (s) + p->addChild(XMPPUtils::createParameter(*s)); } return p; } -void JGRtpMedia::fromXML(XMLElement* xml) +void JGRtpMedia::fromXml(XmlElement* xml) { if (!xml) { - set("","","","",""); + set("","",""); return; } - set(xml->getAttribute("id"),xml->getAttribute("name"), - xml->getAttribute("clockrate"),xml->getAttribute("channels"),""); - XMLElement* param = xml->findFirstChild(XMLElement::Parameter); - for (; param; param = xml->findNextChild(param,XMLElement::Parameter)) - m_params.addParam(param->getAttribute("name"),param->getAttribute("value")); + set(xml->attribute("id"),xml->attribute("name"), + xml->attribute("clockrate"),"",xml->attribute("channels"), + xml->attribute("ptime"),xml->attribute("maxptime")); + XmlElement* param = XMPPUtils::findFirstChild(*xml,XmlTag::Parameter); + for (; param; param = XMPPUtils::findNextChild(*xml,param,XmlTag::Parameter)) + m_params.addParam(param->attribute("name"),param->attribute("value")); } -/** +/* * JGCrypto */ - -XMLElement* JGCrypto::toXML() const +XmlElement* JGCrypto::toXml() const { - XMLElement* xml = new XMLElement(XMLElement::Crypto); + XmlElement* xml = XMPPUtils::createElement(XmlTag::Crypto); xml->setAttributeValid("crypto-suite",m_suite); xml->setAttributeValid("key-params",m_keyParams); xml->setAttributeValid("session-params",m_sessionParams); @@ -180,25 +342,64 @@ XMLElement* JGCrypto::toXML() const return xml; } -void JGCrypto::fromXML(const XMLElement* xml) +void JGCrypto::fromXml(const XmlElement* xml) { if (!xml) return; m_suite = xml->getAttribute("crypto-suite"); m_keyParams = xml->getAttribute("key-params"); m_sessionParams = xml->getAttribute("session-params"); - assign(xml->getAttribute("tag")); + assign(xml->attribute("tag")); +} + +// Build an 'encryption' element from a list of crypto objects +// XEP 0167 Section 7 +XmlElement* JGCrypto::buildEncryption(const ObjList& list, bool required) +{ + ObjList* c = list.skipNull(); + if (!c) + return 0; + XmlElement* enc = XMPPUtils::createElement(XmlTag::Encryption); + enc->setAttribute("required",String::boolText(required)); + for (; c; c = c->skipNext()) + enc->addChild((static_cast(c->get()))->toXml()); + return enc; +} + +// Decode an 'encryption' element. Clear the list before starting +// XEP 0167 Section 7 +void JGCrypto::decodeEncryption(const XmlElement* xml, ObjList& list, bool& required) +{ + list.clear(); + required = false; + XmlElement* c = xml ? XMPPUtils::findFirstChild(*xml,XmlTag::Crypto) : 0; + if (!c) + return; + String* req = xml->getAttribute("required"); + if (req) + required = (*req == "true") || (*req == "1"); + else + required = false; + for (; c; c = XMPPUtils::findNextChild(*xml,c,XmlTag::Crypto)) + list.append(new JGCrypto(c)); } -/** +/* * JGRtpMediaList */ - -TokenDict JGRtpMediaList::s_media[] = { - {"audio", Audio}, - {0,0} -}; +// Reset the list and data +void JGRtpMediaList::reset() +{ + clear(); + m_ready = false; + m_media = MediaMissing; + m_cryptoRequired = false; + m_cryptoLocal.clear(); + m_cryptoRemote.clear(); + m_ssrc.clear(); + TelEngine::destruct(m_bandwidth); +} // Find a data payload by its id JGRtpMedia* JGRtpMediaList::findMedia(const String& id) @@ -219,54 +420,64 @@ JGRtpMedia* JGRtpMediaList::findSynonym(const String& value) const } // Create a 'description' element and add payload children to it -XMLElement* JGRtpMediaList::toXML(bool telEvent) const +XmlElement* JGRtpMediaList::toXml(bool telEvent) const { if (m_media != Audio) return 0; - XMLElement* desc = XMPPUtils::createElement(XMLElement::Description, + XmlElement* desc = XMPPUtils::createElement(XmlTag::Description, XMPPNamespace::JingleAppsRtp); desc->setAttributeValid("media",lookup(m_media,s_media)); + desc->setAttributeValid("ssrc",m_ssrc); for (ObjList* o = skipNull(); o; o = o->skipNext()) { JGRtpMedia* a = static_cast(o->get()); - desc->addChild(a->toXML()); + desc->addChild(a->toXml()); } if (telEvent) { - JGRtpMedia* te = new JGRtpMedia("106","telephone-event","8000","",""); - desc->addChild(te->toXML()); + JGRtpMedia* te = JGRtpMedia::telEvent(); + desc->addChild(te->toXml()); TelEngine::destruct(te); } - ObjList* c = m_cryptoLocal.skipNull(); - if (c) { - if (m_cryptoMandatory) - desc->addChild(new XMLElement(XMLElement::CryptoRequired)); - for (; c; c = c->skipNext()) - desc->addChild((static_cast(c->get()))->toXML()); + // Bandwidth + if (m_bandwidth && *m_bandwidth) { + XmlElement* b = XMPPUtils::createElement(s_bandwidth,*m_bandwidth); + b->setAttribute("type",m_bandwidth->name()); + desc->addChild(b); } + // Encryption + XmlElement* enc = JGCrypto::buildEncryption(m_cryptoLocal,m_cryptoRequired); + if (enc) + desc->addChild(enc); return desc; } // Fill this list from an XML element's children. Clear before attempting to fill -void JGRtpMediaList::fromXML(XMLElement* xml) +void JGRtpMediaList::fromXml(XmlElement* xml) { - clear(); - m_cryptoMandatory = false; - m_cryptoRemote.clear(); + reset(); if (!xml) return; - m_media = (Media)lookup(xml->getAttribute("media"),s_media,MediaUnknown); - XMLElement* m = xml->findFirstChild(XMLElement::PayloadType); - for (; m; m = xml->findNextChild(m,XMLElement::PayloadType)) - ObjList::append(new JGRtpMedia(m)); - // Check crypto - XMLElement* c = xml->findFirstChild(XMLElement::Crypto); - if (c) { - XMLElement* mandatory = xml->findFirstChild(XMLElement::CryptoRequired); - if (mandatory) { - m_cryptoMandatory = true; - TelEngine::destruct(mandatory); + m_media = (Media)lookup(xml->attribute("media"),s_media,MediaUnknown); + m_ssrc = xml->getAttribute("ssrc"); + String* ns = xml->xmlns(); + if (!ns) + return; + XmlElement* x = 0; + while (0 != (x = xml->findNextChild(x))) { + const String* tag = 0; + const String* n = 0; + if (!(x->getTag(tag,n) && n && *n == *ns)) + continue; + if (*tag == XMPPUtils::s_tag[XmlTag::PayloadType]) + ObjList::append(new JGRtpMedia(x)); + else if (*tag == XMPPUtils::s_tag[XmlTag::Encryption]) + JGCrypto::decodeEncryption(x,m_cryptoRemote,m_cryptoRequired); + else if (*tag == s_bandwidth) { + if (m_bandwidth) + continue; + String* type = x->getAttribute("type"); + if (!TelEngine::null(type)) + m_bandwidth = new NamedString(*type,x->getText()); } - for (; c; c = xml->findNextChild(c,XMLElement::Crypto)) - m_cryptoRemote.append(new JGCrypto(c)); } } @@ -282,29 +493,23 @@ bool JGRtpMediaList::createList(String& dest, bool synonym, const char* sep) } -/** +/* * JGRtpCandidate */ - // Create a 'candidate' element from this object -XMLElement* JGRtpCandidate::toXml(const JGRtpCandidates& container) const +XmlElement* JGRtpCandidate::toXml(const JGRtpCandidates& container) const { if (container.m_type == JGRtpCandidates::Unknown) return 0; - - XMLElement* xml = new XMLElement(XMLElement::Candidate); - + XmlElement* xml = XMPPUtils::createElement(XmlTag::Candidate); xml->setAttributeValid("component",m_component); xml->setAttributeValid("generation",m_generation); - if (container.m_type == JGRtpCandidates::RtpIceUdp) xml->setAttributeValid("foundation",toString()); else if (container.m_type == JGRtpCandidates::RtpRawUdp) xml->setAttributeValid("id",toString()); - xml->setAttributeValid("ip",m_address); xml->setAttributeValid("port",m_port); - if (container.m_type == JGRtpCandidates::RtpIceUdp) { xml->setAttributeValid("network",m_network); xml->setAttributeValid("priority",m_priority); @@ -315,16 +520,14 @@ XMLElement* JGRtpCandidate::toXml(const JGRtpCandidates& container) const } // Fill this object from a candidate element -void JGRtpCandidate::fromXml(XMLElement* xml, const JGRtpCandidates& container) +void JGRtpCandidate::fromXml(XmlElement* xml, const JGRtpCandidates& container) { if (!xml || container.m_type == JGRtpCandidates::Unknown) return; - if (container.m_type == JGRtpCandidates::RtpIceUdp) - assign(xml->getAttribute("foundation")); + assign(xml->attribute("foundation")); else if (container.m_type == JGRtpCandidates::RtpRawUdp) - assign(xml->getAttribute("id")); - + assign(xml->attribute("id")); m_component = xml->getAttribute("component"); m_generation = xml->getAttribute("generation"); m_address = xml->getAttribute("ip"); @@ -338,18 +541,11 @@ void JGRtpCandidate::fromXml(XMLElement* xml, const JGRtpCandidates& container) } -/** +/* * JGRtpCandidates */ - -TokenDict JGRtpCandidates::s_type[] = { - {"ice-udp", RtpIceUdp}, - {"raw-udp", RtpRawUdp}, - {0,0}, -}; - // Create a 'transport' element from this object. Add -XMLElement* JGRtpCandidates::toXML(bool addCandidates, bool addAuth) const +XmlElement* JGRtpCandidates::toXml(bool addCandidates, bool addAuth) const { XMPPNamespace::Type ns; if (m_type == RtpIceUdp) @@ -358,7 +554,7 @@ XMLElement* JGRtpCandidates::toXML(bool addCandidates, bool addAuth) const ns = XMPPNamespace::JingleTransportRawUdp; else return 0; - XMLElement* trans = XMPPUtils::createElement(XMLElement::Transport,ns); + XmlElement* trans = XMPPUtils::createElement(XmlTag::Transport,ns); if (addAuth && m_type == RtpIceUdp) { trans->setAttributeValid("pwd",m_password); trans->setAttributeValid("ufrag",m_ufrag); @@ -370,7 +566,7 @@ XMLElement* JGRtpCandidates::toXML(bool addCandidates, bool addAuth) const } // Fill this object from a given element -void JGRtpCandidates::fromXML(XMLElement* element) +void JGRtpCandidates::fromXml(XmlElement* element) { clear(); m_type = Unknown; @@ -379,17 +575,18 @@ void JGRtpCandidates::fromXML(XMLElement* element) if (!element) return; // Set transport data - if (XMPPUtils::hasXmlns(*element,XMPPNamespace::JingleTransportIceUdp)) + int ns = XMPPUtils::xmlns(*element); + if (ns == XMPPNamespace::JingleTransportIceUdp) m_type = RtpIceUdp; - else if (XMPPUtils::hasXmlns(*element,XMPPNamespace::JingleTransportRawUdp)) + else if (ns == XMPPNamespace::JingleTransportRawUdp) m_type = RtpRawUdp; else return; m_password = element->getAttribute("pwd"); m_ufrag = element->getAttribute("ufrag"); // Get candidates - XMLElement* c = element->findFirstChild(XMLElement::Candidate); - for (; c; c = element->findNextChild(c,XMLElement::Candidate)) + XmlElement* c = XMPPUtils::findFirstChild(*element,XmlTag::Candidate,ns); + for (; c; c = XMPPUtils::findNextChild(*element,c,XmlTag::Candidate,ns)) append(new JGRtpCandidate(c,*this)); } @@ -406,8 +603,6 @@ JGRtpCandidate* JGRtpCandidates::findByComponent(unsigned int component) } // Generate a random password or username to be used with ICE-UDP transport -// Maximum number of characters. The maxmimum value is 256. -// The minimum value is 22 for password and 4 for username void JGRtpCandidates::generateIceToken(String& dest, bool pwd, unsigned int max) { if (pwd) { @@ -434,25 +629,9 @@ void JGRtpCandidates::generateOldIceToken(String& dest) } -/** +/* * JGSessionContent */ - -// The list containing the text values for Senders enumeration -TokenDict JGSessionContent::s_senders[] = { - {"both", SendBoth}, - {"initiator", SendInitiator}, - {"responder", SendResponder}, - {0,0} -}; - -// The list containing the text values for Creator enumeration -TokenDict JGSessionContent::s_creator[] = { - {"initiator", CreatorInitiator}, - {"responder", CreatorResponder}, - {0,0} -}; - // Constructor JGSessionContent::JGSessionContent(Type t, const char* name, Senders senders, Creator creator, const char* disposition) @@ -463,10 +642,10 @@ JGSessionContent::JGSessionContent(Type t, const char* name, Senders senders, } // Build a 'content' XML element from this object -XMLElement* JGSessionContent::toXml(bool minimum, bool addDesc, +XmlElement* JGSessionContent::toXml(bool minimum, bool addDesc, bool addTrans, bool addCandidates, bool addAuth) const { - XMLElement* xml = new XMLElement(XMLElement::Content); + XmlElement* xml = XMPPUtils::createElement(XmlTag::Content); xml->setAttributeValid("name",m_name); xml->setAttributeValid("creator",lookup(m_creator,s_creator)); if (!minimum) { @@ -474,18 +653,18 @@ XMLElement* JGSessionContent::toXml(bool minimum, bool addDesc, xml->setAttributeValid("disposition",m_disposition); } // Add description and transport - XMLElement* desc = 0; - XMLElement* trans = 0; + XmlElement* desc = 0; + XmlElement* trans = 0; if (m_type == RtpIceUdp || m_type == RtpRawUdp) { // Audio content if (addDesc) - desc = m_rtpMedia.toXML(); + desc = m_rtpMedia.toXml(); if (addTrans) - trans = m_rtpLocalCandidates.toXML(addCandidates,addAuth); + trans = m_rtpLocalCandidates.toXml(addCandidates,addAuth); } else if (m_type == FileBSBOffer || m_type == FileBSBRequest) { // File transfer content - XMLElement* file = XMPPUtils::createElement(XMLElement::File, + XmlElement* file = XMPPUtils::createElement(XmlTag::File, XMPPNamespace::SIProfileFileTransfer); unsigned int n = m_fileTransfer.length(); for (unsigned int i = 0; i < n; i++) { @@ -493,16 +672,16 @@ XMLElement* JGSessionContent::toXml(bool minimum, bool addDesc, if (ns) file->setAttributeValid(ns->name(),*ns); } - XMLElement* child = 0; + XmlElement* child = 0; if (m_type == FileBSBOffer) - child = new XMLElement(XMLElement::Offer); + child = XMPPUtils::createElement(XmlTag::Offer); else - child = new XMLElement(XMLElement::Request); + child = XMPPUtils::createElement(XmlTag::Request); child->addChild(file); - desc = XMPPUtils::createElement(XMLElement::Description, + desc = XMPPUtils::createElement(XmlTag::Description, XMPPNamespace::JingleAppsFileTransfer); desc->addChild(child); - trans = XMPPUtils::createElement(XMLElement::Transport, + trans = XMPPUtils::createElement(XmlTag::Transport, XMPPNamespace::JingleTransportByteStreams); } xml->addChild(desc); @@ -511,27 +690,27 @@ XMLElement* JGSessionContent::toXml(bool minimum, bool addDesc, } // Build a content object from an XML element -JGSessionContent* JGSessionContent::fromXml(XMLElement* xml, XMPPError::Type& err, - String& error) +JGSessionContent* JGSessionContent::fromXml(XmlElement* xml, XMPPError::Type& err, + String& error) { static const char* errAttr = "Required attribute is missing: "; static const char* errAttrValue = "Invalid attribute value: "; if (!xml) { - err = XMPPError::SInternal; + err = XMPPError::Internal; return 0; } - err = XMPPError::SNotAcceptable; + err = XMPPError::NotAcceptable; - const char* name = xml->getAttribute("name"); + const char* name = xml->attribute("name"); if (!(name && *name)) { error << errAttr << "name"; return 0; } // Creator (default: initiator) Creator creator = CreatorInitiator; - const char* tmp = xml->getAttribute("creator"); + const char* tmp = xml->attribute("creator"); if (tmp) creator = (Creator)lookup(tmp,s_creator,CreatorUnknown); if (creator == CreatorUnknown) { @@ -540,7 +719,7 @@ JGSessionContent* JGSessionContent::fromXml(XMLElement* xml, XMPPError::Type& er } // Senders (default: both) Senders senders = SendBoth; - tmp = xml->getAttribute("senders"); + tmp = xml->attribute("senders"); if (tmp) senders = (Senders)lookup(tmp,s_senders,SendUnknown); if (senders == SendUnknown) { @@ -549,42 +728,37 @@ JGSessionContent* JGSessionContent::fromXml(XMLElement* xml, XMPPError::Type& er } JGSessionContent* content = new JGSessionContent(Unknown,name,senders,creator, - xml->getAttribute("disposition")); - XMLElement* desc = 0; - XMLElement* trans = 0; + xml->attribute("disposition")); err = XMPPError::NoError; // Use a while() to go to end and cleanup data while (true) { int offer = -1; // Check description - desc = xml->findFirstChild(XMLElement::Description); + XmlElement* desc = XMPPUtils::findFirstChild(*xml,XmlTag::Description); if (desc) { - if (XMPPUtils::hasXmlns(*desc,XMPPNamespace::JingleAppsRtp)) { - content->m_rtpMedia.fromXML(desc); - } + if (XMPPUtils::hasXmlns(*desc,XMPPNamespace::JingleAppsRtp)) + content->m_rtpMedia.fromXml(desc); else if (XMPPUtils::hasXmlns(*desc,XMPPNamespace::JingleAppsFileTransfer)) { content->m_type = UnknownFileTransfer; // Get file and type - XMLElement* dir = desc->findFirstChild(XMLElement::Offer); + XmlElement* dir = XMPPUtils::findFirstChild(*desc,XmlTag::Offer); if (dir) offer = 1; else { - dir = desc->findFirstChild(XMLElement::Request); + dir = XMPPUtils::findFirstChild(*desc,XmlTag::Request); if (dir) offer = 0; } if (dir) { - XMLElement* file = dir->findFirstChild(XMLElement::File); + XmlElement* file = XMPPUtils::findFirstChild(*dir,XmlTag::File); if (file && XMPPUtils::hasXmlns(*file,XMPPNamespace::SIProfileFileTransfer)) { - addParamValid(content->m_fileTransfer,"name",file->getAttribute("name")); - addParamValid(content->m_fileTransfer,"size",file->getAttribute("size")); - addParamValid(content->m_fileTransfer,"hash",file->getAttribute("hash")); - addParamValid(content->m_fileTransfer,"date",file->getAttribute("date")); + addParamValid(content->m_fileTransfer,"name",file->attribute("name")); + addParamValid(content->m_fileTransfer,"size",file->attribute("size")); + addParamValid(content->m_fileTransfer,"hash",file->attribute("hash")); + addParamValid(content->m_fileTransfer,"date",file->attribute("date")); } else offer = -1; - TelEngine::destruct(file); - TelEngine::destruct(dir); } } else @@ -594,10 +768,10 @@ JGSessionContent* JGSessionContent::fromXml(XMLElement* xml, XMPPError::Type& er content->m_rtpMedia.m_media = JGRtpMediaList::MediaMissing; // Check transport - trans = xml->findFirstChild(XMLElement::Transport); + XmlElement* trans = XMPPUtils::findFirstChild(*xml,XmlTag::Transport); if (trans) { if (content->type() != UnknownFileTransfer) { - content->m_rtpRemoteCandidates.fromXML(trans); + content->m_rtpRemoteCandidates.fromXml(trans); if (content->m_rtpRemoteCandidates.m_type == JGRtpCandidates::RtpIceUdp) content->m_type = RtpIceUdp; else if (content->m_rtpRemoteCandidates.m_type == JGRtpCandidates::RtpRawUdp) @@ -615,9 +789,6 @@ JGSessionContent* JGSessionContent::fromXml(XMLElement* xml, XMPPError::Type& er break; } - - TelEngine::destruct(desc); - TelEngine::destruct(trans); if (err == XMPPError::NoError) return content; TelEngine::destruct(content); @@ -625,16 +796,16 @@ JGSessionContent* JGSessionContent::fromXml(XMLElement* xml, XMPPError::Type& er } -/** +/* * JGStreamHost */ // Build an XML element from this stream host -XMLElement* JGStreamHost::toXml() +XmlElement* JGStreamHost::toXml() { if (!length()) return 0; - XMLElement* xml = new XMLElement(XMLElement::StreamHost); + XmlElement* xml = XMPPUtils::createElement(XmlTag::StreamHost); xml->setAttribute("jid",c_str()); if (m_zeroConf.null()) { xml->setAttribute("host",m_address); @@ -646,22 +817,22 @@ XMLElement* JGStreamHost::toXml() } // Build a stream host from an XML element -JGStreamHost* JGStreamHost::fromXml(XMLElement* xml) +JGStreamHost* JGStreamHost::fromXml(XmlElement* xml) { if (!xml) return 0; - const char* jid = xml->getAttribute("jid"); + const char* jid = xml->attribute("jid"); if (TelEngine::null(jid)) return 0; - return new JGStreamHost(jid,xml->getAttribute("host"), - String(xml->getAttribute("port")).toInteger(-1),xml->getAttribute("zeroconf")); + return new JGStreamHost(jid,xml->attribute("host"), + String(xml->attribute("port")).toInteger(-1),xml->attribute("zeroconf")); } // Build a query XML element carrying a list of stream hosts -XMLElement* JGStreamHost::buildHosts(const ObjList& hosts, const char* sid, +XmlElement* JGStreamHost::buildHosts(const ObjList& hosts, const char* sid, const char* mode) { - XMLElement* xml = XMPPUtils::createElement(XMLElement::Query, + XmlElement* xml = XMPPUtils::createElement(XmlTag::Query, XMPPNamespace::ByteStreams); xml->setAttribute("sid",sid); xml->setAttribute("mode",mode); @@ -671,143 +842,68 @@ XMLElement* JGStreamHost::buildHosts(const ObjList& hosts, const char* sid, } // Build a query XML element with a streamhost-used child -XMLElement* JGStreamHost::buildRsp(const char* jid) +XmlElement* JGStreamHost::buildRsp(const char* jid) { - XMLElement* xml = XMPPUtils::createElement(XMLElement::Query, + XmlElement* xml = XMPPUtils::createElement(XmlTag::Query, XMPPNamespace::ByteStreams); - XMLElement* used = new XMLElement(XMLElement::StreamHostUsed); + XmlElement* used = XMPPUtils::createElement(XmlTag::StreamHostUsed); used->setAttribute("jid",jid); xml->addChild(used); return xml; } -/** +/* * JGSession */ - -TokenDict JGSession::s_versions[] = { - {"0", Version0}, - {"1", Version1}, - {0,0} -}; - -TokenDict JGSession::s_states[] = { - {"Idle", Idle}, - {"Pending", Pending}, - {"Active", Active}, - {"Ending", Ending}, - {"Destroy", Destroy}, - {0,0} -}; - -TokenDict JGSession::s_reasons[] = { - {"busy", ReasonBusy}, - {"decline", ReasonDecline}, - {"connectivity-error", ReasonConn}, - {"media-error", ReasonMedia}, - {"unsupported-transports", ReasonTransport}, - {"no-error", ReasonNoError}, - {"success", ReasonOk}, - {"unsupported-applications", ReasonNoApp}, - {"alternative-session", ReasonAltSess}, - {"general-error", ReasonUnknown}, - {"transferred", ReasonTransfer}, - {0,0} -}; - -TokenDict JGSession::s_actions0[] = { - {"accept", ActAccept}, - {"initiate", ActInitiate}, - {"terminate", ActTerminate}, - {"reject", ActTerminate}, - {"info", ActInfo}, - {"transport-info", ActTransportInfo}, - {"transport-accept", ActTransportAccept}, - {"content-info", ActContentInfo}, - {"DTMF", ActDtmf}, - {"ringing", ActRinging}, - {"mute", ActMute}, - {0,0} -}; - -TokenDict JGSession::s_actions1[] = { - {"session-accept", ActAccept}, - {"session-initiate", ActInitiate}, - {"session-terminate", ActTerminate}, - {"session-info", ActInfo}, - {"transport-info", ActTransportInfo}, - {"transport-accept", ActTransportAccept}, - {"transport-reject", ActTransportReject}, - {"transport-replace", ActTransportReplace}, - {"content-accept", ActContentAccept}, - {"content-add", ActContentAdd}, - {"content-modify", ActContentModify}, - {"content-reject", ActContentReject}, - {"content-remove", ActContentRemove}, - {"transfer", ActTransfer}, - {"DTMF", ActDtmf}, - {"ringing", ActRinging}, - {"trying", ActTrying}, - {"received", ActReceived}, - {"hold", ActHold}, - {"active", ActActive}, - {"mute", ActMute}, - {"streamhost", ActStreamHost}, - {0,0} -}; - // Create an outgoing session -JGSession::JGSession(Version ver, JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, const char* msg) +JGSession::JGSession(Version ver, JGEngine* engine, + const JabberID& caller, const JabberID& called) : Mutex(true,"JGSession"), m_version(ver), m_state(Idle), m_timeToPing(0), m_engine(engine), - m_stream(0), m_outgoing(true), - m_localJID(callerJID), - m_remoteJID(calledJID), + m_local(caller), + m_remote(called), m_lastEvent(0), m_recvTerminate(false), m_private(0), m_stanzaId(1) { - if (stream && stream->ref()) - m_stream = stream; // Make sure we don't ping before session-initiate times out - if (m_engine && m_engine->pingInterval()) + if (m_engine->pingInterval()) m_timeToPing = Time::msecNow() + m_engine->stanzaTimeout() + m_engine->pingInterval(); m_engine->createSessionId(m_localSid); m_sid = m_localSid; - Debug(m_engine,DebugAll,"Call(%s). Outgoing msg=%s [%p]",m_sid.c_str(),msg,this); - if (msg) - sendMessage(msg); + Debug(m_engine,DebugAll,"Call(%s). Outgoing from=%s to=%s [%p]", + m_sid.c_str(),m_local.c_str(),m_remote.c_str(),this); } // Create an incoming session -JGSession::JGSession(Version ver, JGEngine* engine, JBEvent* event, const String& id) +JGSession::JGSession(Version ver, JGEngine* engine, const JabberID& caller, + const JabberID& called, XmlElement* xml, const String& id) : Mutex(true,"JGSession"), m_version(ver), m_state(Idle), m_timeToPing(0), m_engine(engine), - m_stream(0), m_outgoing(false), m_sid(id), + m_local(caller), + m_remote(called), m_lastEvent(0), m_recvTerminate(false), m_private(0), m_stanzaId(1) { - if (event->stream() && event->stream()->ref()) - m_stream = event->stream(); - if (m_engine && m_engine->pingInterval()) + if (m_engine->pingInterval()) m_timeToPing = Time::msecNow() + m_engine->pingInterval(); - m_events.append(event); + m_queue.addChild(xml); m_engine->createSessionId(m_localSid); - Debug(m_engine,DebugAll,"Call(%s). Incoming [%p]",m_sid.c_str(),this); + Debug(m_engine,DebugAll,"Call(%s). Incoming from=%s to=%s [%p]", + m_sid.c_str(),m_remote.c_str(),m_local.c_str(),this); } // Destructor: hangup, cleanup, remove from engine's list @@ -816,72 +912,75 @@ JGSession::~JGSession() XDebug(m_engine,DebugAll,"JGSession::~JGSession() [%p]",this); } -// Ask this session to accept an event -bool JGSession::acceptEvent(JBEvent* event, const String& sid) +// Ask this session to accept an incoming xml element +bool JGSession::acceptIq(XMPPUtils::IqType type, const JabberID& from, const JabberID& to, + const String& id, XmlElement* xml) { - if (!event) - return false; - - // Requests must match the session id - // Responses' id must start with session's local id (this is the way we generate the stanza id) - if (sid) { - if (sid != m_sid) - return false; - } - else if (!event->id().startsWith(m_localSid)) + if (!(xml && id)) return false; // Check to/from - if (m_localJID != event->to() || m_remoteJID != event->from()) + if (m_local != to || m_remote != from) return false; - - // Ok: keep a referenced event - if (event->ref()) - enqueue(event); + // Requests must match the session id + // Responses' id must start with session's local id (this is the way we generate the stanza id) + switch (type) { + case XMPPUtils::IqSet: + if (id != m_sid) + return false; + break; + case XMPPUtils::IqResult: + case XMPPUtils::IqError: + if (!id.startsWith(m_localSid)) + return false; + // TODO: check sent stanzas queue to match the id + break; + default: + return false; + } + // Ok + Lock lock(this); + m_queue.addChild(xml); + DDebug(m_engine,DebugAll,"Call(%s). Accepted xml (%p,%s) [%p]", + m_sid.c_str(),xml,xml->tag(),this); return true; } -// Confirm a received element. If the error is NoError a result stanza will be sent -// Otherwise, an error stanza will be created and sent -bool JGSession::confirm(XMLElement* xml, XMPPError::Type error, - const char* text, XMPPError::ErrorType type) +// Confirm (send result) a received element +bool JGSession::confirmResult(XmlElement* xml) { if (!xml) return false; - XMLElement* iq = 0; - if (error == XMPPError::NoError) { - String id = xml->getAttribute("id"); - iq = XMPPUtils::createIq(XMPPUtils::IqResult,m_localJID,m_remoteJID,id); - // The receiver will detect which stanza is confirmed by id - // If missing, make a copy of the received element and attach it to the error - if (!id) { - XMLElement* copy = new XMLElement(*xml); - iq->addChild(copy); - } + const char* id = xml->attribute("id"); + XmlElement* iq = XMPPUtils::createIqResult(m_local,m_remote,id); + // The receiver will detect which stanza is confirmed by id + // If missing, make a copy of the received element and attach it to the error + if (TelEngine::null(id)) { + XmlElement* copy = new XmlElement(*xml); + iq->addChild(copy); } - else - iq = XMPPUtils::createError(xml,type,error,text); + return sendStanza(iq,0,false); +} + +// Confirm (send error) a received element +bool JGSession::confirmError(XmlElement*& xml, XMPPError::Type error, + const char* text, XMPPError::ErrorType type) +{ + XmlElement* iq = XMPPUtils::createIqError(m_local,m_remote,xml,type,error,text); return sendStanza(iq,0,false); } // Close a Pending or Active session -bool JGSession::hangup(int reason, const char* msg) +bool JGSession::hangup(XmlElement* reason) { Lock lock(this); - if (state() != Pending && state() != Active) + if (state() != Pending && state() != Active) { + TelEngine::destruct(reason); return false; - DDebug(m_engine,DebugAll,"Call(%s). Hangup('%s') [%p]",m_sid.c_str(),msg,this); + } + DDebug(m_engine,DebugAll,"Call(%s). Hangup(%p) [%p]",m_sid.c_str(),reason,this); // Clear sent stanzas list. We will wait for this element to be confirmed m_sentStanza.clear(); - const char* tmp = lookupReason(reason); - XMLElement* res = 0; - if (tmp || msg) { - res = new XMLElement(XMLElement::Reason); - if (tmp) - res->addChild(new XMLElement(tmp)); - if (msg) - res->addChild(new XMLElement(XMLElement::Text,0,msg)); - } - XMLElement* xml = createJingle(ActTerminate,res); + XmlElement* xml = createJingle(ActTerminate,reason); bool ok = sendStanza(xml); changeState(Ending); return ok; @@ -892,14 +991,14 @@ void JGSession::buildSocksDstAddr(String& buf) { SHA1 sha(m_sid); if (outgoing()) - sha << m_localJID << m_remoteJID; + sha << m_local << m_remote; else - sha << m_remoteJID << m_localJID; + sha << m_remote << m_local; buf = sha.hexDigest(); } // Send a session info element to the remote peer -bool JGSession::sendInfo(XMLElement* xml, String* stanzaId) +bool JGSession::sendInfo(XmlElement* xml, String* stanzaId) { if (!xml) return false; @@ -918,8 +1017,8 @@ bool JGSession::sendDtmf(const char* dtmf, unsigned int msDuration, String* stan if (!(dtmf && *dtmf)) return false; - XMLElement* iq = createJingle(version() != Version0 ? ActInfo : ActContentInfo); - XMLElement* sess = iq->findFirstChild(); + XmlElement* iq = createJingle(version() != Version0 ? ActInfo : ActContentInfo); + XmlElement* sess = iq->findFirstChild(); if (!sess) { TelEngine::destruct(iq); return false; @@ -929,36 +1028,20 @@ bool JGSession::sendDtmf(const char* dtmf, unsigned int msDuration, String* stan s[0] = *dtmf++; sess->addChild(createDtmf(s,msDuration)); } - TelEngine::destruct(sess); return sendStanza(iq,stanzaId); } // Check if the remote party supports a given feature bool JGSession::hasFeature(XMPPNamespace::Type feature) { - if (!m_stream) - return false; - JBClientStream* cStream = static_cast(m_stream->getObject("JBClientStream")); - if (cStream) { - XMPPUser* user = cStream->getRemote(remote()); - if (!user) - return false; - bool ok = false; - user->lock(); - JIDResource* res = user->remoteRes().get(remote().resource()); - ok = res && res->features().get(feature); - user->unlock(); - TelEngine::destruct(user); - return ok; - } return false; } // Build a transfer element -XMLElement* JGSession::buildTransfer(const String& transferTo, +XmlElement* JGSession::buildTransfer(const String& transferTo, const String& transferFrom, const String& sid) { - XMLElement* transfer = XMPPUtils::createElement(XMLElement::Transfer, + XmlElement* transfer = XMPPUtils::createElement(XmlTag::Transfer, XMPPNamespace::JingleTransfer); transfer->setAttributeValid("from",transferFrom); transfer->setAttributeValid("to",transferTo); @@ -974,108 +1057,104 @@ JGEvent* JGSession::getEvent(u_int64_t time) return 0; if (state() == Destroy) return 0; - // Deque and process event(s) - // Loop until a jingle event is generated or no more events in queue - JBEvent* jbev = 0; + // Deque and process xml + // Loop until a jingle event is generated or no more xml in queue + XmlElement* xml = 0; while (true) { - TelEngine::destruct(jbev); - jbev = static_cast(m_events.remove(false)); - if (!jbev) + TelEngine::destruct(xml); + xml = static_cast(m_queue.pop()); + if (!xml) break; - DDebug(m_engine,DebugAll, - "Call(%s). Dequeued Jabber event (%p,%s) in state %s [%p]", - m_sid.c_str(),jbev,jbev->name(),lookupState(state()),this); + DDebug(m_engine,DebugAll,"Call(%s). Dequeued xml (%p,%s) ns=%s in state %s [%p]", + m_sid.c_str(),xml,xml->tag(),TelEngine::c_safe(xml->xmlns()), + lookupState(state()),this); // Update ping interval - if (m_engine && m_engine->pingInterval()) + if (m_engine->pingInterval()) m_timeToPing = time + m_engine->pingInterval(); else m_timeToPing = 0; - // Process Jingle 'set' stanzas - if (jbev->type() == JBEvent::IqJingleSet) { - // Filter some conditions in which we can't accept any jingle stanza - // Outgoing idle sessions are waiting for the user to initiate them - if (state() == Idle && outgoing()) { - confirm(jbev->releaseXML(),XMPPError::SRequest); - continue; - } - - m_lastEvent = decodeJingle(jbev); - - if (!m_lastEvent) { - // Destroy incoming session if session initiate stanza contains errors + XMPPUtils::IqType t = XMPPUtils::iqType(xml->attribute("type")); + // Process Jingle 'set' stanzas and file transfer + if (t == XMPPUtils::IqSet || t == XMPPUtils::IqGet) { + XmlElement* child = xml->findFirstChild(); + if (!child || t == XMPPUtils::IqGet) { + confirmError(xml,XMPPError::BadRequest); if (!outgoing() && state() == Idle) { - m_lastEvent = new JGEvent(JGEvent::Destroy,this,0,"failure"); - // TODO: hangup + m_lastEvent = new JGEvent(JGEvent::Destroy,this); break; } + } + int ns = XMPPUtils::xmlns(*child); + // Jingle + if (ns == XMPPNamespace::Jingle || ns == XMPPNamespace::JingleSession) { + // Filter some conditions in which we can't accept any jingle stanza + // Outgoing idle sessions are waiting for the user to initiate them + if (state() == Idle && outgoing()) { + confirmError(xml,XMPPError::Request); + continue; + } + JGEvent* event = decodeJingle(xml,child); + if (!event) { + // Destroy incoming session if session initiate stanza contains errors + if (!outgoing() && state() == Idle) { + m_lastEvent = new JGEvent(JGEvent::Destroy,this); + // TODO: hangup + break; + } + continue; + } + if (event->action() != ActInfo) { + m_lastEvent = processJingleSetEvent(event); + if (m_lastEvent) + break; + } + else { + // ActInfo with empty session info: PING + XDebug(m_engine,DebugAll,"Call(%s). Received empty '%s' (ping) [%p]", + m_sid.c_str(),event->actionName(),this); + event->confirmElement(); + delete event; + } continue; } - - // ActInfo: empty session info - if (m_lastEvent->action() == ActInfo) { - XDebug(m_engine,DebugAll,"Call(%s). Received empty '%s' (ping) [%p]", - m_sid.c_str(),m_lastEvent->actionName(),this); - m_lastEvent->confirmElement(); - delete m_lastEvent; - m_lastEvent = 0; - continue; + // File transfer iq + if (ns == XMPPNamespace::ByteStreams) { + m_lastEvent = processFileTransfer(t == XMPPUtils::IqSet,xml,child); + if (m_lastEvent) + break; } - - processJingleSetLastEvent(*jbev); - if (!m_lastEvent) - continue; - break; + else + DDebug(m_engine,DebugStub,"Call(%s). Unhandled ns=%s [%p]", + m_sid.c_str(),TelEngine::c_safe(xml->xmlns()),this); + confirmError(xml,XMPPError::ServiceUnavailable); + if (!outgoing() && state() == Idle) { + m_lastEvent = new JGEvent(JGEvent::Destroy,this); + break; + } + continue; } - if (jbev->type() == JBEvent::Iq) { - processJabberIqEvent(*jbev); + // Process Jingle 'set' stanzas and file transfer + if (t == XMPPUtils::IqResult || t == XMPPUtils::IqError) { + m_lastEvent = processJabberIqResponse(t == XMPPUtils::IqResult,xml); if (m_lastEvent) break; continue; } - // Check for responses or failures - if (jbev->type() == JBEvent::IqJingleRes || - jbev->type() == JBEvent::IqJingleErr || - jbev->type() == JBEvent::IqResult || - jbev->type() == JBEvent::IqError || - jbev->type() == JBEvent::WriteFail) { - if (!processJabberIqResponse(*jbev) || m_lastEvent) - break; - continue; - } - - // Silently ignore temporary stream down - if (jbev->type() == JBEvent::Terminated) { - DDebug(m_engine,DebugInfo, - "Call(%s). Stream disconnected in state %s [%p]", - m_sid.c_str(),lookupState(state()),this); - continue; - } - - // Terminate on stream destroy - if (jbev->type() == JBEvent::Destroy) { - Debug(m_engine,DebugInfo, - "Call(%s). Stream destroyed in state %s [%p]", - m_sid.c_str(),lookupState(state()),this); - m_lastEvent = new JGEvent(JGEvent::Terminated,this,0,"noconn"); - break; - } - - Debug(m_engine,DebugStub,"Call(%s). Unhandled event type %u '%s' [%p]", - m_sid.c_str(),jbev->type(),jbev->name(),this); + confirmError(xml,XMPPError::ServiceUnavailable); continue; } - TelEngine::destruct(jbev); + TelEngine::destruct(xml); // No event: check first sent stanza's timeout if (!m_lastEvent) { ObjList* o = m_sentStanza.skipNull(); JGSentStanza* tmp = o ? static_cast(o->get()) : 0; - while (tmp && tmp->timeout(time)) { + if (tmp && tmp->timeout(time)) { Debug(m_engine,DebugNote,"Call(%s). Sent stanza ('%s') timed out [%p]", m_sid.c_str(),tmp->c_str(),this); // Don't terminate if the sender requested to be notified @@ -1084,8 +1163,7 @@ JGEvent* JGSession::getEvent(u_int64_t time) m_lastEvent->m_id = *tmp; o->remove(); if (m_lastEvent->final()) - hangup(false,"Timeout"); - break; + hangup(createReason(ReasonTimeout,"Stanza timeout")); } } @@ -1111,60 +1189,17 @@ JGEvent* JGSession::getEvent(u_int64_t time) // Release this session and its memory void JGSession::destroyed() { + hangup(); // Remove from engine if (m_engine) { Lock lock(m_engine); m_engine->m_sessions.remove(this,false); } - lock(); - // Cleanup. Respond to events in queue - if (m_stream) { - hangup(ReasonUnknown); - for (ObjList* o = m_events.skipNull(); o; o = o->skipNext()) { - JBEvent* jbev = static_cast(o->get()); - // Skip events generated by the stream - if (jbev->type() == JBEvent::WriteFail || - jbev->type() == JBEvent::Terminated || - jbev->type() == JBEvent::Destroy) - continue; - // Respond to non error/result IQs - XMLElement* xml = jbev->element(); - if (!(xml && xml->type() == XMLElement::Iq)) - continue; - XMPPUtils::IqType t = XMPPUtils::iqType(xml->getAttribute("type")); - if (t == XMPPUtils::IqError || t == XMPPUtils::IqResult) - continue; - // Respond - if (m_recvTerminate) - confirm(jbev->releaseXML(),XMPPError::SRequest, - "Session terminated",XMPPError::TypeCancel); - else { - XMLElement* jingle = checkJingle(jbev->child()); - m_recvTerminate = jingle && ActTerminate == getAction(jingle); - confirm(jbev->element()); - } - } - TelEngine::destruct(m_stream); - } - m_events.clear(); - unlock(); DDebug(m_engine,DebugInfo,"Call(%s). Destroyed [%p]",m_sid.c_str(),this); } -// Enqueue a Jabber engine event -void JGSession::enqueue(JBEvent* event) -{ - Lock lock(this); - if (event->type() == JBEvent::Terminated || event->type() == JBEvent::Destroy) - m_events.insert(event); - else - m_events.append(event); - DDebug(m_engine,DebugAll,"Call(%s). Accepted event (%p,%s) [%p]", - m_sid.c_str(),event,event->name(),this); -} - // Send a stanza to the remote peer -bool JGSession::sendStanza(XMLElement* stanza, String* stanzaId, bool confirmation, +bool JGSession::sendStanza(XmlElement* stanza, String* stanzaId, bool confirmation, bool ping) { if (!stanza) @@ -1172,20 +1207,20 @@ bool JGSession::sendStanza(XMLElement* stanza, String* stanzaId, bool confirmati Lock lock(this); // confirmation=true: this is not a response, don't allow if terminated bool terminated = (state() == Ending || state() == Destroy); - if (!m_stream || (terminated && confirmation)) { + if (terminated && confirmation) { #ifdef DEBUG Debug(m_engine,DebugNote, "Call(%s). Can't send stanza (%p,'%s') in state %s [%p]", - m_sid.c_str(),stanza,stanza->name(),lookupState(m_state),this); + m_sid.c_str(),stanza,stanza->tag(),lookupState(m_state),this); #endif TelEngine::destruct(stanza); return false; } DDebug(m_engine,DebugAll,"Call(%s). Sending stanza (%p,'%s') id=%s [%p]", - m_sid.c_str(),stanza,stanza->name(),String::boolText(stanzaId != 0),this); + m_sid.c_str(),stanza,stanza->tag(),String::boolText(stanzaId != 0),this); const char* senderId = m_localSid; // Check if the stanza should be added to the list of stanzas requiring confirmation - if (confirmation && stanza->type() == XMLElement::Iq) { + if (confirmation && XMPPUtils::isUnprefTag(*stanza,XmlTag::Iq)) { String id = m_localSid; id << "_" << (unsigned int)m_stanzaId++; JGSentStanza* sent = new JGSentStanza(id, @@ -1196,11 +1231,7 @@ bool JGSession::sendStanza(XMLElement* stanza, String* stanzaId, bool confirmati *stanzaId = *sent; m_sentStanza.append(sent); } - // Send. If it fails leave it in the sent items to timeout - JBStream::Error res = m_stream->sendStanza(stanza,senderId); - if (res == JBStream::ErrorNoSocket || res == JBStream::ErrorContext) - return false; - return true; + return m_engine->sendStanza(this,stanza); } // Send a ping (empty session info) stanza to the remote peer if it's time to do it @@ -1217,37 +1248,36 @@ bool JGSession::sendPing(u_int64_t msecNow) return sendStanza(createJingle(ActInfo),0,true,true); } -// Method called in getEvent() to process a last event set from a jingle set jabber event -void JGSession::processJingleSetLastEvent(JBEvent& ev) +// Method called in getEvent() to process a last event decoded from a +// received jingle element +JGEvent* JGSession::processJingleSetEvent(JGEvent*& ev) { - if (!m_lastEvent) - return; + if (!ev) + return 0; DDebug(m_engine,DebugInfo,"Call(%s). Processing action (%u,'%s') state=%s [%p]", - m_sid.c_str(),m_lastEvent->action(),m_lastEvent->actionName(), - lookupState(state()),this); + m_sid.c_str(),ev->action(),ev->actionName(),lookupState(state()),this); // Check for termination events - if (m_lastEvent->final()) - return; + if (ev->final()) + return ev; bool error = false; bool fatal = false; switch (state()) { case Active: - error = m_lastEvent->action() == ActAccept || - m_lastEvent->action() == ActInitiate || - m_lastEvent->action() == ActRinging; + error = ev->action() == ActAccept || ev->action() == ActInitiate || + ev->action() == ActRinging; break; case Pending: // Accept session-accept, transport, content and ringing stanzas - switch (m_lastEvent->action()) { + switch (ev->action()) { case ActAccept: if (outgoing()) { // XEP-0166 7.2.6: responder may be overridden - if (m_lastEvent->jingle()) { - JabberID rsp = m_lastEvent->jingle()->getAttribute("responder"); - if (!rsp.null() && m_remoteJID != rsp) { - m_remoteJID.set(rsp); + if (ev->jingle()) { + JabberID rsp(ev->jingle()->attribute("responder")); + if (!rsp.null() && m_remote != rsp) { + m_remote.set(rsp); Debug(m_engine,DebugInfo, "Call(%s). Remote jid changed to '%s' [%p]", m_sid.c_str(),rsp.c_str(),this); @@ -1268,9 +1298,11 @@ void JGSession::processJingleSetLastEvent(JBEvent& ev) case ActContentReject: case ActContentRemove: case ActInfo: + case ActDescriptionInfo: case ActRinging: case ActTrying: case ActReceived: + case ActCandidates: break; default: error = true; @@ -1278,9 +1310,9 @@ void JGSession::processJingleSetLastEvent(JBEvent& ev) break; case Idle: // Update data. Terminate if not a session initiating event - if (m_lastEvent->action() == ActInitiate) { - m_localJID.set(ev.to()); - m_remoteJID.set(ev.from()); + if (ev->action() == ActInitiate) { +// m_local.set(ev.to()); +// m_remote.set(ev.from()); changeState(Pending); } else @@ -1291,7 +1323,7 @@ void JGSession::processJingleSetLastEvent(JBEvent& ev) } if (!error) { // Don't confirm actions that need session user's interaction - switch (m_lastEvent->action()) { + switch (ev->action()) { case ActInitiate: case ActTransportInfo: case ActTransportAccept: @@ -1309,106 +1341,103 @@ void JGSession::processJingleSetLastEvent(JBEvent& ev) case ActMute: case ActTrying: case ActReceived: + case ActDescriptionInfo: + case ActCandidates: break; default: - m_lastEvent->confirmElement(); + ev->confirmElement(); } - return; + return ev; } - m_lastEvent->confirmElement(XMPPError::SRequest); - delete m_lastEvent; - m_lastEvent = 0; + ev->confirmElement(XMPPError::Request); + delete ev; + ev = 0; if (fatal) - m_lastEvent = new JGEvent(JGEvent::Destroy,this); + ev = new JGEvent(JGEvent::Destroy,this); + return ev; } // Method called in getEvent() to process a jabber event carrying a response -bool JGSession::processJabberIqResponse(JBEvent& ev) +JGEvent* JGSession::processJabberIqResponse(bool result, XmlElement*& xml) { + if (!xml) + return 0; JGSentStanza* sent = 0; + String id(xml->getAttribute("id")); + if (TelEngine::null(id)) { + TelEngine::destruct(xml); + return 0; + } // Find a sent stanza to match the event's id for (ObjList* o = m_sentStanza.skipNull(); o; o = o->skipNext()) { sent = static_cast(o->get()); - if (ev.id() == *sent) + if (*sent == id) break; sent = 0; } - if (!sent) - return true; - bool iqErr = (ev.type() == JBEvent::IqJingleErr || ev.type() == JBEvent::IqError); + if (!sent) { + TelEngine::destruct(xml); + return 0; + } // Check termination conditions // Always terminate when receiving responses in Ending state bool terminateEnding = (state() == Ending); // Terminate pending outgoing if no notification required // (Initial session request is sent without notification required) bool terminatePending = false; - if (state() == Pending && outgoing() && (iqErr || ev.type() == JBEvent::WriteFail)) + if (state() == Pending && outgoing() && !result) terminatePending = !sent->notify(); - // Write fail: Terminate if failed stanza is a Jingle one and the sender - // didn't requested notification - bool terminateFail = false; - if (!(terminateEnding || terminatePending) && ev.type() == JBEvent::WriteFail) - terminateFail = !sent->notify(); // Generate event + JGEvent* ev = 0; + String text; + if (!result) { + String tmp; + XMPPUtils::decodeError(xml,tmp,text); + if (!text) + text = tmp; + } if (terminateEnding) - m_lastEvent = new JGEvent(JGEvent::Destroy,this); - else if (terminatePending || terminateFail) - m_lastEvent = new JGEvent(JGEvent::Terminated,this, - ev.type() != JBEvent::WriteFail ? ev.releaseXML() : 0, - ev.text() ? ev.text().c_str() : "failure"); + ev = new JGEvent(JGEvent::Destroy,this,xml,text); + else if (terminatePending) + ev = new JGEvent(JGEvent::Terminated,this,xml,text); else if (sent->notify()) - switch (ev.type()) { - case JBEvent::IqJingleRes: - case JBEvent::IqResult: - m_lastEvent = new JGEvent(JGEvent::ResultOk,this,ev.releaseXML()); - break; - case JBEvent::IqJingleErr: - case JBEvent::IqError: - m_lastEvent = new JGEvent(JGEvent::ResultError,this,ev.releaseXML(),ev.text()); - break; - case JBEvent::WriteFail: - m_lastEvent = new JGEvent(JGEvent::ResultWriteFail,this,ev.releaseXML(),ev.text()); - break; - default: - DDebug(m_engine,DebugStub,"Call(%s). Unhandled response event (%p,%u,%s) [%p]", - m_sid.c_str(),&ev,ev.type(),ev.name(),this); - } + if (result) + ev = new JGEvent(JGEvent::ResultOk,this,xml); + else + ev = new JGEvent(JGEvent::ResultError,this,xml,text); else { // Terminate on ping error - if (sent->ping()) { - terminateFail = iqErr || ev.type() == JBEvent::WriteFail; - if (terminateFail) - m_lastEvent = new JGEvent(JGEvent::Terminated,this, - ev.type() != JBEvent::WriteFail ? ev.releaseXML() : 0, - ev.text() ? ev.text().c_str() : "failure"); - } + if (sent->ping() && !result) + ev = new JGEvent(JGEvent::Terminated,this,xml,text); } - if (m_lastEvent && !m_lastEvent->m_id) - m_lastEvent->m_id = *sent; + if (ev) + xml = 0; + else + TelEngine::destruct(xml); String error; #ifdef DEBUG - if (ev.type() == JBEvent::IqJingleErr && ev.text()) - error << " (error='" << ev.text() << "')"; + if (text) + error << " '" << text << "')"; #endif - bool terminate = (m_lastEvent && m_lastEvent->final()); - Debug(m_engine,(terminatePending || terminateFail) ? DebugNote : DebugAll, - "Call(%s). Sent %selement with id=%s confirmed by event=%s%s%s [%p]", - m_sid.c_str(),sent->ping() ? "ping " : "",ev.id().c_str(), - ev.name(),error.safe(),terminate ? ". Terminating": "",this); + bool terminate = (ev && ev->final()); + Debug(m_engine,terminatePending ? DebugNote : DebugAll, + "Call(%s). Sent %selement with id=%s confirmed by %s%s%s [%p]", + m_sid.c_str(),sent->ping() ? "ping " : "",sent->c_str(), + result ? "result" : "error",error.safe(),terminate ? ". Terminating": "",this); m_sentStanza.remove(sent,true); // Gracefully terminate - if (terminate && state() != Ending) { - hangup(ReasonUnknown); - return false; - } - return true; + if (terminate && state() != Ending) + hangup(); + return ev; } -// Method called in getEvent() to process a generic jabber iq event -void JGSession::processJabberIqEvent(JBEvent& ev) +// Decode a file transfer element +JGEvent* JGSession::processFileTransfer(bool set, XmlElement*& xml, XmlElement* child) { - confirm(ev.releaseXML(),XMPPError::SFeatureNotImpl); + if (xml) + confirmError(xml,XMPPError::FeatureNotImpl); + return 0; } // Event termination notification @@ -1466,20 +1495,21 @@ JGSession::Action JGSession::lookupAction(const char* str, Version ver) } -/** +/* * JGSession0 */ - // Create an outgoing session -JGSession0::JGSession0(JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, const char* msg) - : JGSession(Version0,engine,stream,callerJID,calledJID,msg) +JGSession0::JGSession0(JGEngine* engine, const JabberID& caller, const JabberID& called) + : JGSession(Version0,engine,caller,called), + m_candidatesAction(ActCount) { } // Create an incoming session -JGSession0::JGSession0(JGEngine* engine, JBEvent* event, const String& id) - : JGSession(Version0,engine,event,id) +JGSession0::JGSession0(JGEngine* engine, const JabberID& caller, const JabberID& called, + XmlElement* xml, const String& id) + : JGSession(Version0,engine,caller,called,xml,id), + m_candidatesAction(ActCount) { m_sessContentName = m_localSid + "_content"; } @@ -1489,22 +1519,13 @@ JGSession0::~JGSession0() { } -// Check if a given XML element is valid jingle one -XMLElement* JGSession0::checkJingle(XMLElement* xml) -{ - if (xml && xml->type() == XMLElement::Session && - XMPPUtils::hasXmlns(*xml,XMPPNamespace::JingleSession)) - return xml; - return 0; -} - // Accept a Pending incoming session bool JGSession0::accept(const ObjList& contents, String* stanzaId) { Lock lock(this); if (outgoing() || state() != Pending) return false; - XMLElement* xml = createJingle(ActAccept); + XmlElement* xml = createJingle(ActAccept); addJingleContents0(m_sessContentName,xml,contents,true,true,true); if (!sendStanza(xml,stanzaId)) return false; @@ -1526,6 +1547,9 @@ bool JGSession0::sendContent(Action action, const ObjList& contents, String* sta addDesc = false; break; case ActTransportAccept: + // Old candidates: don't send it + if (m_candidatesAction != ActTransportInfo) + return true; minimal = true; addDesc = false; addTrans = true; @@ -1539,19 +1563,33 @@ bool JGSession0::sendContent(Action action, const ObjList& contents, String* sta tmp = "Content" + String(Time::secNow()); stanzaId = &tmp; } - XMLElement* xml = createJingle(action); - addJingleContents0(m_sessContentName,xml,contents,minimal,addDesc,addTrans); - return sendStanza(xml,stanzaId); + if (action != ActTransportInfo || m_candidatesAction != ActCount) { + Action a = (action == ActTransportInfo) ? m_candidatesAction : action; + XmlElement* xml = createJingle(a); + addJingleContents0(m_sessContentName,xml,contents,minimal,addDesc,addTrans, + m_candidatesAction); + return sendStanza(xml,stanzaId); + } + // Send both transports + XmlElement* xml = createJingle(ActTransportInfo); + addJingleContents0(m_sessContentName,xml,contents,minimal,addDesc,addTrans, + ActTransportInfo); + bool ok = sendStanza(xml,stanzaId); + tmp << stanzaId << "_1"; + xml = createJingle(ActCandidates); + addJingleContents0(m_sessContentName,xml,contents,minimal,addDesc,addTrans, + ActCandidates); + return sendStanza(xml,&tmp) || ok; } // Build and send the initial message on an outgoing session -bool JGSession0::initiate(const ObjList& contents, XMLElement* extra, const char* subject) +bool JGSession0::initiate(const ObjList& contents, XmlElement* extra, const char* subject) { - XMLElement* xml = createJingle(ActInitiate); + XmlElement* xml = createJingle(ActInitiate); addJingleContents0(m_sessContentName,xml,contents,true,true,true); addJingleChild0(xml,extra); if (!null(subject)) - addJingleChild0(xml,new XMLElement(XMLElement::Subject,0,subject)); + addJingleChild0(xml,XMPPUtils::createSubject(subject)); if (sendStanza(xml)) { changeState(Pending); return true; @@ -1561,43 +1599,34 @@ bool JGSession0::initiate(const ObjList& contents, XMLElement* extra, const char } // Decode a valid jingle set event. Set the event's data on success -JGEvent* JGSession0::decodeJingle(JBEvent* jbev) +JGEvent* JGSession0::decodeJingle(XmlElement*& xml, XmlElement* child) { - XMLElement* jingle = jbev->child(); - if (!jingle) { - confirm(jbev->releaseXML(),XMPPError::SBadRequest); + if (!xml) + return 0; + if (!child) { + confirmError(xml,XMPPError::BadRequest); return 0; } - - Action act = getAction(jingle); + Action act = getAction(child); if (act == ActCount) { - confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable, - "Unknown session action"); + confirmError(xml,XMPPError::ServiceUnavailable,"Unknown session action"); return 0; } - // *** ActTerminate - if (act == ActTerminate) { + // *** ActTerminate, ActReject + if (act == ActTerminate || act == ActReject) { // Confirm here: this is a final event, // stanza won't be confirmed in getEvent() m_recvTerminate = true; const char* reason = 0; const char* text = 0; - XMLElement* res = jingle->findFirstChild(XMLElement::Reason); - if (res) { - XMLElement* tmp = res->findFirstChild(); - if (tmp && tmp->type() != XMLElement::Text) - reason = tmp->name(); - TelEngine::destruct(tmp); - tmp = res->findFirstChild(XMLElement::Text); - if (tmp) - text = tmp->getText(); - TelEngine::destruct(tmp); - TelEngine::destruct(res); - } - JGEvent* ev = new JGEvent(JGEvent::Terminated,this,jbev->releaseXML(),reason,text); + decodeJingleReason(*xml,reason,text); + JGEvent* ev = new JGEvent(JGEvent::Terminated,this,xml,reason,text); + if (!ev->m_reason && act == ActReject) + ev->m_reason = lookupReason(ReasonDecline); ev->setAction(act); ev->confirmElement(); + xml = 0; return ev; } @@ -1605,60 +1634,71 @@ JGEvent* JGSession0::decodeJingle(JBEvent* jbev) if (act == ActContentInfo) { // Check dtmf // Expect more then 1 'dtmf' child - XMLElement* tmp = jingle->findFirstChild(XMLElement::Dtmf); + XmlElement* tmp = XMPPUtils::findFirstChild(*child,XmlTag::Dtmf); String text; - for (; tmp; tmp = jingle->findNextChild(tmp,XMLElement::Dtmf)) { - String reason = tmp->getAttribute("action"); + for (; tmp; tmp = XMPPUtils::findNextChild(*child,tmp,XmlTag::Dtmf)) { + String reason = tmp->attribute("action"); if (reason == "button-up") - text << tmp->getAttribute("code"); + text << tmp->attribute("code"); } - if (text) - return new JGEvent(ActDtmf,this,jbev->releaseXML(),0,text); - confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable); - return 0; + JGEvent* ev = 0; + if (text) { + ev = new JGEvent(ActDtmf,this,xml,0,text); + xml = 0; + } + else + unhandledAction(this,xml,act); + return ev; } // *** ActInfo if (act == ActInfo) { - // Check info element // Return ActInfo event to signal ping (XEP-0166 6.8) - XMLElement* child = jingle->findFirstChild(); - if (!child) - return new JGEvent(ActInfo,this,jbev->releaseXML()); - - JGEvent* event = 0; - Action a = ActCount; - XMPPNamespace::Type ns = XMPPNamespace::Count; - // Check namespace and build event - switch (child->type()) { - case XMLElement::Ringing: - a = ActRinging; - ns = XMPPNamespace::JingleRtpInfoOld; - break; - case XMLElement::Mute: - a = ActMute; - ns = XMPPNamespace::JingleRtpInfoOld; - break; - default: ; + JGEvent* ev = 0; + XmlElement* ch = child->findFirstChild(); + if (ch) { + int t = XmlTag::Count,n; + XMPPUtils::getTag(*child,t,n); + switch (t) { + case XmlTag::Ringing: + if (n == XMPPNamespace::JingleRtpInfoOld) + ev = new JGEvent(ActRinging,this,xml); + break; + case XmlTag::Mute: + if (n == XMPPNamespace::JingleRtpInfoOld) + ev = new JGEvent(ActMute,this,xml); + break; + default: ; + } } - if (a != ActCount && XMPPUtils::hasXmlns(*child,ns)) - event = new JGEvent(a,this,jbev->releaseXML()); else - confirm(jbev->releaseXML(),XMPPError::SFeatureNotImpl); - TelEngine::destruct(child); - return event; + ev = new JGEvent(ActInfo,this,xml); + if (ev) + xml = 0; + else + unhandledAction(this,xml,act,ch); + return ev; } if (act == ActTransportAccept) { - confirm(jbev->element()); + confirmResult(xml); + TelEngine::destruct(xml); return 0; } + // Update candidates action + if (m_candidatesAction == ActCount && + (act == ActCandidates || act == ActTransportInfo)) { + m_candidatesAction = act; + Debug(m_engine,DebugAll,"Call(%s). Candidates action set to %s [%p]", + m_sid.c_str(),lookupAction(m_candidatesAction,version()),this); + } + if (act == ActCandidates) + act = ActTransportInfo; + // Get transport // Get media description // Create event, update transport and media - XMLElement* media = 0; - XMLElement* trans = jingle; JGSessionContent* c = 0; JGEvent* event = 0; while (true) { @@ -1667,65 +1707,80 @@ JGEvent* JGSession0::decodeJingle(JBEvent* jbev) c->m_rtpRemoteCandidates.m_type = JGRtpCandidates::RtpIceUdp; // Build media if (act == ActInitiate || act == ActAccept) { - media = jingle->findFirstChild(XMLElement::Description); - if (media && XMPPUtils::hasXmlns(*media,XMPPNamespace::JingleAudio)) { - c->m_rtpMedia.fromXML(media); + XmlElement* media = XMPPUtils::findFirstChild(*child,XmlTag::Description, + XMPPNamespace::JingleAudio); + if (media) { + c->m_rtpMedia.fromXml(media); c->m_rtpMedia.m_media = JGRtpMediaList::Audio; } - else + else { + Debug(m_engine,DebugInfo,"Call(%s). No media description for action=%s [%p]", + m_sid.c_str(),lookupAction(act,version()),this); break; + } } // Build transport - trans = trans->findFirstChild(XMLElement::Transport); - if (trans && !XMPPUtils::hasXmlns(*trans,XMPPNamespace::JingleTransport)) { - if (trans != jingle) - TelEngine::destruct(trans); + XmlElement* trans = 0; + if (m_candidatesAction != ActCandidates) + trans = XMPPUtils::findFirstChild(*child,XmlTag::Transport, + XMPPNamespace::JingleTransport); + else + trans = child; + if (act == ActInitiate && m_candidatesAction == ActCount) { + if (trans && trans != child) + m_candidatesAction = ActTransportInfo; else - trans = 0; + m_candidatesAction = ActCandidates; + Debug(m_engine,DebugAll,"Call(%s). Candidates action set to %s [%p]", + m_sid.c_str(),lookupAction(m_candidatesAction,version()),this); + } + XmlElement* t = 0; + if (trans) { + String* ns = trans->xmlns(); + t = trans->findFirstChild(&XMPPUtils::s_tag[XmlTag::Candidate],ns); } - XMLElement* t = trans ? trans->findFirstChild(XMLElement::Candidate) : 0; if (t) { JGRtpCandidate* cd = new JGRtpCandidate(m_localSid + "_transport"); cd->m_component = "1"; - cd->m_generation = t->getAttribute("generation"); - cd->m_address = t->getAttribute("address"); - cd->m_port = t->getAttribute("port"); - cd->m_protocol = t->getAttribute("protocol");; - cd->m_generation = t->getAttribute("generation"); - cd->m_type = t->getAttribute("type"); - c->m_rtpRemoteCandidates.m_ufrag = t->getAttribute("username"); - c->m_rtpRemoteCandidates.m_password = t->getAttribute("password"); + cd->m_generation = t->attribute("generation"); + cd->m_address = t->attribute("address"); + cd->m_port = t->attribute("port"); + cd->m_protocol = t->attribute("protocol");; + cd->m_generation = t->attribute("generation"); + cd->m_type = t->attribute("type"); + c->m_rtpRemoteCandidates.m_ufrag = t->attribute("username"); + c->m_rtpRemoteCandidates.m_password = t->attribute("password"); c->m_rtpRemoteCandidates.append(cd); - TelEngine::destruct(t); } - else if (act == ActTransportInfo) + else if (act == ActTransportInfo) { + Debug(m_engine,DebugInfo,"Call(%s). No transport candidates for action=%s [%p]", + m_sid.c_str(),lookupAction(act,version()),this); break; + } // Don't set the event's element yet: this would invalidate the 'jingle' variable - event = new JGEvent(act,this,jbev->releaseXML()); + event = new JGEvent(act,this,xml); event->m_contents.append(c); + xml = 0; break; } - if (trans != jingle) - TelEngine::destruct(trans); - TelEngine::destruct(media); - if (!event) { - TelEngine::destruct(c); - confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable); - } - return event; + if (event) + return event; + TelEngine::destruct(c); + confirmError(xml,XMPPError::ServiceUnavailable); + return 0; } // Create an 'iq' stanza with a 'jingle' child -XMLElement* JGSession0::createJingle(Action action, XMLElement* element1, - XMLElement* element2, XMLElement* element3) +XmlElement* JGSession0::createJingle(Action action, XmlElement* element1, + XmlElement* element2, XmlElement* element3) { - XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,m_localJID,m_remoteJID,0); - XMLElement* jingle = XMPPUtils::createElement(XMLElement::Session, + XmlElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,m_local,m_remote,0); + XmlElement* jingle = XMPPUtils::createElement(XmlTag::Session, XMPPNamespace::JingleSession); if (action < ActCount) jingle->setAttribute("type",lookupAction(action,version())); - jingle->setAttribute("initiator",outgoing() ? m_localJID : m_remoteJID); - jingle->setAttribute("responder",outgoing() ? m_remoteJID : m_localJID); + jingle->setAttribute("initiator",outgoing() ? m_local : m_remote); + jingle->setAttribute("responder",outgoing() ? m_remote : m_local); jingle->setAttribute("id",m_sid); jingle->addChild(element1); jingle->addChild(element2); @@ -1735,9 +1790,9 @@ XMLElement* JGSession0::createJingle(Action action, XMLElement* element1, } // Create a dtmf XML element -XMLElement* JGSession0::createDtmf(const char* dtmf, unsigned int msDuration) +XmlElement* JGSession0::createDtmf(const char* dtmf, unsigned int msDuration) { - XMLElement* xml = XMPPUtils::createElement(XMLElement::Dtmf,XMPPNamespace::DtmfOld); + XmlElement* xml = XMPPUtils::createElement(XmlTag::Dtmf,XMPPNamespace::DtmfOld); xml->setAttribute("action","button-up"); xml->setAttribute("code",dtmf); return xml; @@ -1749,15 +1804,15 @@ XMLElement* JGSession0::createDtmf(const char* dtmf, unsigned int msDuration) */ // Create an outgoing session -JGSession1::JGSession1(JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, const char* msg) - : JGSession(Version1,engine,stream,callerJID,calledJID,msg) +JGSession1::JGSession1(JGEngine* engine, const JabberID& caller, const JabberID& called) + : JGSession(Version1,engine,caller,called) { } // Create an incoming session -JGSession1::JGSession1(JGEngine* engine, JBEvent* event, const String& id) - : JGSession(Version1,engine,event,id) +JGSession1::JGSession1(JGEngine* engine, const JabberID& caller, const JabberID& called, + XmlElement* xml, const String& id) + : JGSession(Version1,engine,caller,called,xml,id) { } @@ -1767,13 +1822,13 @@ JGSession1::~JGSession1() } // Build and send the initial message on an outgoing session -bool JGSession1::initiate(const ObjList& contents, XMLElement* extra, const char* subject) +bool JGSession1::initiate(const ObjList& contents, XmlElement* extra, const char* subject) { - XMLElement* xml = createJingle(ActInitiate); + XmlElement* xml = createJingle(ActInitiate); addJingleContents(xml,contents,false,true,true,true); addJingleChild(xml,extra); if (!null(subject)) - addJingleChild(xml,new XMLElement(XMLElement::Subject,0,subject)); + addJingleChild(xml,XMPPUtils::createSubject(subject)); if (sendStanza(xml)) { changeState(Pending); return true; @@ -1782,22 +1837,13 @@ bool JGSession1::initiate(const ObjList& contents, XMLElement* extra, const char return false; } -// Check if a given XML element is valid jingle one -XMLElement* JGSession1::checkJingle(XMLElement* xml) -{ - if (xml && xml->type() == XMLElement::Jingle && - XMPPUtils::hasXmlns(*xml,XMPPNamespace::Jingle)) - return xml; - return 0; -} - // Accept a Pending incoming session bool JGSession1::accept(const ObjList& contents, String* stanzaId) { Lock lock(this); if (outgoing() || state() != Pending) return false; - XMLElement* xml = createJingle(ActAccept); + XmlElement* xml = createJingle(ActAccept); addJingleContents(xml,contents,false,true,true,true,true); if (!sendStanza(xml,stanzaId)) return false; @@ -1805,15 +1851,47 @@ bool JGSession1::accept(const ObjList& contents, String* stanzaId) return true; } -// Create a 'hold' child to be added to a session-info element -XMLElement* JGSession1::createHoldXml() +// Create a RTP info child to be added to a session-info element +XmlElement* JGSession1::createRtpInfoXml(RtpInfo info) { - return XMPPUtils::createElement(XMLElement::Hold,XMPPNamespace::JingleAppsRtpInfo); + const char* tag = lookup(info,s_rtpInfo); + if (!TelEngine::null(tag)) + return XMPPUtils::createElement(tag,XMPPNamespace::JingleAppsRtpInfo); + return 0; } -XMLElement* JGSession1::createActiveXml() +// Create a termination reason element +XmlElement* JGSession1::createReason(int reason, const char* text, XmlElement* child) { - return XMPPUtils::createElement(XMLElement::Active,XMPPNamespace::JingleAppsRtpInfo); + const char* res = lookup(reason,s_reasons); + if (TelEngine::null(res)) { + TelEngine::destruct(child); + return 0; + } + XmlElement* r = XMPPUtils::createElement(XmlTag::Reason); + r->addChild(new XmlElement(res)); + if (!TelEngine::null(text)) + r->addChild(XMPPUtils::createElement(XmlTag::Text,text)); + if (child) + r->addChild(child); + return r; +} + +// Create a transfer reason element +XmlElement* JGSession1::createTransferReason(int reason) +{ + const char* res = lookup(reason,s_reasons); + if (!TelEngine::null(res)) + return XMPPUtils::createElement(res,XMPPNamespace::JingleTransfer); + return 0; +} + +XmlElement* JGSession1::createRtpSessionReason(int reason) +{ + const char* res = lookup(reason,s_reasons); + if (!TelEngine::null(res)) + return XMPPUtils::createElement(res,XMPPNamespace::JingleAppsRtpError); + return 0; } // Send a stanza with session content(s) @@ -1861,7 +1939,7 @@ bool JGSession1::sendContent(Action action, const ObjList& contents, String* sta tmp = "Content" + String(Time::secNow()); stanzaId = &tmp; } - XMLElement* xml = createJingle(action); + XmlElement* xml = createJingle(action); addJingleContents(xml,contents,minimal,addDesc,addTrans,addCandidates,addIceAuth); return sendStanza(xml,stanzaId); } @@ -1872,7 +1950,7 @@ bool JGSession1::sendStreamHosts(const ObjList& hosts, String* stanzaId) Lock lock(this); if (state() != Pending) return false; - XMLElement* xml = XMPPUtils::createIq(XMPPUtils::IqSet,m_localJID,m_remoteJID,0); + XmlElement* xml = XMPPUtils::createIq(XMPPUtils::IqSet,m_local,m_remote,0); xml->addChild(JGStreamHost::buildHosts(hosts,m_sid)); return sendStanza(xml,stanzaId); } @@ -1884,8 +1962,8 @@ bool JGSession1::sendStreamHostUsed(const char* jid, const char* stanzaId) if (state() != Pending) return false; bool ok = !null(jid); - XMLElement* xml = XMPPUtils::createIq(ok ? XMPPUtils::IqResult : XMPPUtils::IqError, - m_localJID,m_remoteJID,stanzaId); + XmlElement* xml = XMPPUtils::createIq(ok ? XMPPUtils::IqResult : XMPPUtils::IqError, + m_local,m_remote,stanzaId); if (ok) xml->addChild(JGStreamHost::buildRsp(jid)); else @@ -1895,45 +1973,31 @@ bool JGSession1::sendStreamHostUsed(const char* jid, const char* stanzaId) } // Decode a jingle stanza -JGEvent* JGSession1::decodeJingle(JBEvent* jbev) +JGEvent* JGSession1::decodeJingle(XmlElement*& xml, XmlElement* child) { - XMLElement* jingle = jbev->child(); - if (!jingle) { - confirm(jbev->releaseXML(),XMPPError::SBadRequest); + if (!child) { + confirmError(xml,XMPPError::BadRequest); return 0; } - Action act = getAction(jingle); + Action act = getAction(child); if (act == ActCount) { - confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable, - "Unknown session action"); + confirmError(xml,XMPPError::ServiceUnavailable,"Unknown session action"); return 0; } // *** ActTerminate if (act == ActTerminate) { - // Confirm here: this is a final event, + // Confirm here: this is a final event, // stanza won't be confirmed in getEvent() m_recvTerminate = true; const char* reason = 0; const char* text = 0; - XMLElement* res = jingle->findFirstChild(XMLElement::Reason); - if (res) { - XMLElement* tmp = res->findFirstChild(); - if (tmp && tmp->type() != XMLElement::Text) - reason = tmp->name(); - TelEngine::destruct(tmp); - tmp = res->findFirstChild(XMLElement::Text); - if (tmp) - text = tmp->getText(); - TelEngine::destruct(tmp); - TelEngine::destruct(res); - } - if (!reason) - reason = act==ActTerminate ? "hangup" : "rejected"; - JGEvent* ev = new JGEvent(JGEvent::Terminated,this,jbev->releaseXML(),reason,text); + decodeJingleReason(*xml,reason,text); + JGEvent* ev = new JGEvent(JGEvent::Terminated,this,xml,reason,text); ev->setAction(act); ev->confirmElement(); + xml = 0; return ev; } @@ -1941,67 +2005,73 @@ JGEvent* JGSession1::decodeJingle(JBEvent* jbev) if (act == ActInfo) { // Check info element // Return ActInfo event to signal ping (XEP-0166 6.8) - XMLElement* child = jingle->findFirstChild(); - if (!child) - return new JGEvent(ActInfo,this,jbev->releaseXML()); - - JGEvent* event = 0; - Action a = ActCount; - XMPPNamespace::Type ns = XMPPNamespace::Count; + XmlElement* ch = child->findFirstChild(); + if (!ch) { + JGEvent* ev = new JGEvent(ActInfo,this,xml); + xml = 0; + return ev; + } + JGEvent* ev = 0; // Check namespace and build event - switch (child->type()) { - case XMLElement::Dtmf: - a = ActDtmf; - ns = XMPPNamespace::Dtmf; + switch (XMPPUtils::tag(*ch)) { + case XmlTag::Dtmf: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleDtmf)) { + String text; + const char* reason = 0; + // Expect more then 1 'dtmf' child + for (; ch; ch = XMPPUtils::findNextChild(*child,ch,XmlTag::Dtmf)) { + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleDtmf)) + text << ch->attribute("code"); + else + break; + } + if (ch) + reason = "Bad dtmf namespace"; + else if (!text) + reason = "Empty dtmf(s)"; + if (reason) { + confirmError(xml,XMPPError::BadRequest,reason); + xml = 0; + return 0; + } + ev = new JGEvent(ActDtmf,this,xml,0,text); + } break; - case XMLElement::Transfer: - a = ActTransfer; - ns = XMPPNamespace::JingleTransfer; + case XmlTag::Transfer: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleTransfer)) + ev = new JGEvent(ActTransfer,this,xml); break; - case XMLElement::Hold: - a = ActHold; - ns = XMPPNamespace::JingleAppsRtpInfo; + case XmlTag::Hold: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleAppsRtpInfo)) + ev = new JGEvent(ActHold,this,xml); break; - case XMLElement::Active: - a = ActActive; - ns = XMPPNamespace::JingleAppsRtpInfo; + case XmlTag::Active: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleAppsRtpInfo)) + ev = new JGEvent(ActActive,this,xml); break; - case XMLElement::Ringing: - a = ActRinging; - ns = XMPPNamespace::JingleAppsRtpInfo; + case XmlTag::Ringing: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleAppsRtpInfo)) + ev = new JGEvent(ActRinging,this,xml); break; - case XMLElement::Trying: - a = ActTrying; - ns = XMPPNamespace::JingleTransportRawUdpInfo; + case XmlTag::Trying: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleTransportRawUdpInfo)) + ev = new JGEvent(ActTrying,this,xml); break; - case XMLElement::Received: - a = ActReceived; - ns = XMPPNamespace::JingleTransportRawUdpInfo; + case XmlTag::Received: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleTransportRawUdpInfo)) + ev = new JGEvent(ActReceived,this,xml); break; - case XMLElement::Mute: - a = ActMute; - ns = XMPPNamespace::JingleAppsRtpInfo; + case XmlTag::Mute: + if (XMPPUtils::hasXmlns(*ch,XMPPNamespace::JingleAppsRtpInfo)) + ev = new JGEvent(ActMute,this,xml); break; default: ; } - if (a != ActCount && XMPPUtils::hasXmlns(*child,ns)) { - String text; - // Add Dtmf - if (a == ActDtmf) { - // Expect more then 1 'dtmf' child - for (; child; child = jingle->findNextChild(child,XMLElement::Dtmf)) - text << child->getAttribute("code"); - if (!text) { - confirm(jbev->releaseXML(),XMPPError::SBadRequest,"Empty dtmf(s)"); - return 0; - } - } - event = new JGEvent(a,this,jbev->releaseXML(),"",text); - } + if (ev) + xml = 0; else - confirm(jbev->releaseXML(),XMPPError::SFeatureNotImpl); - TelEngine::destruct(child); - return event; + confirmError(xml,XMPPError::FeatureNotImpl); + return ev; } // *** Elements carrying contents @@ -2017,23 +2087,18 @@ JGEvent* JGSession1::decodeJingle(JBEvent* jbev) case ActContentRemove: case ActInitiate: case ActAccept: + case ActDescriptionInfo: break; default: - confirm(jbev->releaseXML(),XMPPError::SServiceUnavailable); + confirmError(xml,XMPPError::ServiceUnavailable); return 0; } - JGEvent* event = new JGEvent(act,this,jbev->releaseXML()); - jingle = event->jingle(); - if (!jingle) { - event->confirmElement(XMPPError::SInternal); - delete event; - return 0; - } + JGEvent* event = new JGEvent(act,this,xml); XMPPError::Type err = XMPPError::NoError; String text; - XMLElement* c = jingle->findFirstChild(XMLElement::Content); - for (; c; c = jingle->findNextChild(c,XMLElement::Content)) { + XmlElement* c = XMPPUtils::findFirstChild(*child,XmlTag::Content); + for (; c; c = XMPPUtils::findNextChild(*child,c,XmlTag::Content)) { JGSessionContent* content = JGSessionContent::fromXml(c,err,text); if (content) { DDebug(m_engine,DebugAll, @@ -2045,29 +2110,31 @@ JGEvent* JGSession1::decodeJingle(JBEvent* jbev) if (err == XMPPError::NoError) { DDebug(m_engine,DebugAll, "Call(%s). Ignoring content='%s' in '%s' stanza [%p]", - m_sid.c_str(),c->getAttribute("name"),event->actionName(),this); + m_sid.c_str(),c->attribute("name"),event->actionName(),this); continue; } - // Error - TelEngine::destruct(c); - event->confirmElement(err,text); - delete event; - return 0; + break; } - return event; + xml = 0; + if (!c) + return event; + TelEngine::destruct(c); + event->confirmElement(err,text); + delete event; + return 0; } // Create an 'iq' stanza with a 'jingle' child -XMLElement* JGSession1::createJingle(Action action, XMLElement* element1, - XMLElement* element2, XMLElement* element3) +XmlElement* JGSession1::createJingle(Action action, XmlElement* element1, + XmlElement* element2, XmlElement* element3) { - XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,m_localJID,m_remoteJID,0); - XMLElement* jingle = XMPPUtils::createElement(XMLElement::Jingle, + XmlElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,m_local,m_remote,0); + XmlElement* jingle = XMPPUtils::createElement(XmlTag::Jingle, XMPPNamespace::Jingle); if (action < ActCount) jingle->setAttribute("type",lookupAction(action,version())); - jingle->setAttribute("initiator",outgoing() ? m_localJID : m_remoteJID); - jingle->setAttribute("responder",outgoing() ? m_remoteJID : m_localJID); + jingle->setAttribute("initiator",outgoing() ? m_local : m_remote); + jingle->setAttribute("responder",outgoing() ? m_remote : m_local); jingle->setAttribute("sid",m_sid); jingle->addChild(element1); jingle->addChild(element2); @@ -2077,34 +2144,33 @@ XMLElement* JGSession1::createJingle(Action action, XMLElement* element1, } // Create a dtmf XML element -XMLElement* JGSession1::createDtmf(const char* dtmf, unsigned int msDuration) +XmlElement* JGSession1::createDtmf(const char* dtmf, unsigned int msDuration) { - XMLElement* xml = XMPPUtils::createElement(XMLElement::Dtmf,XMPPNamespace::Dtmf); + XmlElement* xml = XMPPUtils::createElement(XmlTag::Dtmf,XMPPNamespace::JingleDtmf); xml->setAttribute("code",dtmf); if (msDuration > 0) xml->setAttribute("duration",String(msDuration)); return xml; } -// Method called in getEvent() to process a generic jabber iq event -void JGSession1::processJabberIqEvent(JBEvent& ev) +// Decode a file transfer element +JGEvent* JGSession1::processFileTransfer(bool set, XmlElement*& xml, XmlElement* child) { - // File transfer - XMLElement* child = ev.child(); - if (child && child->type() == XMLElement::Query && - XMPPUtils::hasXmlns(*child,XMPPNamespace::ByteStreams)) { - m_lastEvent = new JGEvent(ActStreamHost,this,ev.releaseXML()); - child = m_lastEvent->element()->findFirstChild(XMLElement::Query); - XMLElement* sh = child->findFirstChild(XMLElement::StreamHost); - for (; sh; sh = child->findNextChild(sh,XMLElement::StreamHost)) { + JGEvent* ev = 0; + if (xml && child && XMPPUtils::isTag(*child,XmlTag::Query,XMPPNamespace::ByteStreams)) { + ev = new JGEvent(ActStreamHost,this,xml); + XmlElement* sh = XMPPUtils::findFirstChild(*child,XmlTag::StreamHost,XMPPNamespace::ByteStreams); + for (; sh; sh = XMPPUtils::findNextChild(*child,sh,XmlTag::StreamHost,XMPPNamespace::ByteStreams)) { JGStreamHost* s = JGStreamHost::fromXml(sh); if (s) - m_lastEvent->m_streamHosts.append(s); + ev->m_streamHosts.append(s); } - TelEngine::destruct(child); } - else - confirm(ev.releaseXML(),XMPPError::SFeatureNotImpl); + else { + confirmError(xml,XMPPError::FeatureNotImpl); + TelEngine::destruct(xml); + } + return ev; } /* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjabber/xmpputils.cpp b/libs/yjabber/xmpputils.cpp new file mode 100644 index 00000000..2bdbfff2 --- /dev/null +++ b/libs/yjabber/xmpputils.cpp @@ -0,0 +1,1564 @@ +/** + * utils.cpp + * Yet Another Jabber Component Protocol Stack + * This file is part of the YATE Project http://YATE.null.ro + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +using namespace TelEngine; + +#ifdef _WINDOWS +#include +#else +#include +#endif +// Insert a SrvRecord into a list in the proper location +void SrvRecord::insert(ObjList& list, SrvRecord* rec) +{ + XDebug(DebugAll,"SrvRecord::insert(%s port=%d prio=%d weight=%d) [%p]", + rec->c_str(),rec->m_port,rec->m_priority,rec->m_weight,rec); + // NOTE: SRV records with the same priority can be returned by the query + // Their relative order is given by the weight value + // Lower priority number means a higher priority + // Higher weight number means a higher priority + // Append items with equal priority and weight in the order they arrive + for (ObjList* o = list.skipNull(); o; o = o->skipNext()) { + SrvRecord* crt = static_cast(o->get()); + // Skip lower priority values + if (rec->m_priority > crt->m_priority) + continue; + if (rec->m_priority < crt->m_priority) + o->insert(rec); + else { + // Equal priority: skip until less weight or different priority + for (; o; o = o->skipNext()) { + SrvRecord* crt = static_cast(o->get()); + if (crt->m_priority != rec->m_priority || crt->m_weight < rec->m_weight) + break; + } + if (o) + o->insert(rec); + else + list.append(rec); + } + return; + } + list.append(rec); +} + +// Make a SRV query +int Resolver::srvQuery(const char* query, ObjList& result) +{ + int code = 0; + XDebug(DebugAll,"Starting SRV query for '%s'",query); +#ifdef _WINDOWS + DNS_RECORD* srv = 0; + code = (int)::DnsQuery_UTF8(query,DNS_TYPE_SRV,DNS_QUERY_STANDARD,NULL,&srv,NULL); + if (code == ERROR_SUCCESS) { + for (DNS_RECORD* dr = srv; dr; dr = dr->pNext) { + if (dr->wType != DNS_TYPE_SRV || dr->wDataLength != sizeof(DNS_SRV_DATA)) + continue; + SrvRecord::insert(result,new SrvRecord(dr->Data.SRV.pNameTarget,dr->Data.SRV.wPort, + dr->Data.SRV.wPriority,dr->Data.SRV.wWeight)); + } + } + if (srv) + ::DnsRecordListFree(srv,DnsFreeRecordList); +#else + unsigned char buf[512]; + int r = res_query(query,ns_c_in,ns_t_srv,buf,sizeof(buf)); + // TODO: return proper error + code = r >= 0 ? 0 : -1; + if (!code && r > 0 && r <= (int)sizeof(buf)) { + int queryCount = 0; + int answerCount = 0; + unsigned char* p = buf + NS_QFIXEDSZ; + unsigned char* e = buf + r; + NS_GET16(queryCount,p); + NS_GET16(answerCount,p); + p = buf + NS_HFIXEDSZ; + // Skip queries + for (; queryCount > 0; queryCount--) { + int n = dn_skipname(p,e); + if (n < 0) + break; + p += (n + NS_QFIXEDSZ); + } + for (int i = 0; i < answerCount; i++) { + char name[NS_MAXLABEL + 1]; + // Skip this answer's query + int n = dn_expand(buf,e,p,name,sizeof(name)); + if ((n <= 0) || (n > NS_MAXLABEL)) + break; + buf[n] = 0; + p += n; + // Get record type, class, ttl, length + int rrType, rrClass, rrTtl, rrLen; + NS_GET16(rrType,p); + NS_GET16(rrClass,p); + NS_GET32(rrTtl,p); + NS_GET16(rrLen,p); + // Remember current pointer and skip to next answer + unsigned char* l = p; + p += rrLen; + // Skip non SRV answers + if (rrType != (int)ns_t_srv) + continue; + // Now get record priority, weight, port, label + int prio, weight, port; + NS_GET16(prio,l); + NS_GET16(weight,l); + NS_GET16(port,l); + n = dn_expand(buf,e,l,name,sizeof(name)); + if ((n <= 0) || (n > NS_MAXLABEL)) + break; + SrvRecord::insert(result,new SrvRecord(name,port,prio,weight)); + } + } +#endif +#ifdef XDEBUG + if (!code) { + String s; + for (ObjList* o = result.skipNull(); o; o = o->skipNext()) { + SrvRecord* rec = static_cast(o->get()); + s << " " << *rec; + s << " (prio=" << rec->m_priority << " port=" << rec->m_port; + s << " weight=" << rec->m_weight << ")"; + } + Debug(DebugAll,"SRV query for '%s' got %d records%s",query,result.count(),s.safe()); + } + else { + String s; + Thread::errorString(s,code); + Debug(DebugNote,"SRV query for '%s' failed: %d '%s'",query,code,s.c_str()); + } +#endif + return code; +} + +static const JabberID s_emptyJid; +static String s_auth[] = {"password", "auth", ""}; + +const String XMPPNamespace::s_array[Count] = { + "http://etherx.jabber.org/streams", // Stream + "jabber:client", // Client + "jabber:server", // Server + "jabber:server:dialback", // Dialback + "urn:ietf:params:xml:ns:xmpp-streams", // StreamError + "urn:ietf:params:xml:ns:xmpp-stanzas", // StanzaError + "urn:xmpp:ping", // Ping + "http://jabber.org/features/iq-register", // Register + "jabber:iq:register", // IqRegister + "jabber:iq:private", // IqPrivate + "jabber:iq:auth", // IqAuth + "http://jabber.org/features/iq-auth", // IqAuthFeature + "jabber:iq:version", // IqVersion + "urn:xmpp:delay", // Delay + "urn:ietf:params:xml:ns:xmpp-tls", // Tls + "urn:ietf:params:xml:ns:xmpp-sasl", // Sasl + "urn:ietf:params:xml:ns:xmpp-session", // Session + "urn:ietf:params:xml:ns:xmpp-bind", // Bind + "jabber:iq:roster", // Roster + "jabber:iq:roster-dynamic", // DynamicRoster + "http://jabber.org/protocol/disco#info", // DiscoInfo + "http://jabber.org/protocol/disco#items", // DiscoItems + "http://jabber.org/protocol/caps", // EntityCaps + "vcard-temp", // VCard + "http://jabber.org/protocol/si/profile/file-transfer", // SIProfileFileTransfer + "http://jabber.org/protocol/bytestreams", // ByteStreams +#if 0 + "urn:xmpp:jingle:0", // Jingle + "urn:xmpp:jingle:errors:0", // JingleError + "urn:xmpp:jingle:apps:rtp:0", // JingleAppsRtp + "urn:xmpp:jingle:apps:rtp:errors:0", // JingleAppsRtpError + "urn:xmpp:jingle:apps:rtp:info:0", // JingleAppsRtpInfo + "urn:xmpp:jingle:apps:rtp:audio", // JingleAppsRtpAudio + "urn:xmpp:jingle:apps:file-transfer:0", // JingleAppsFileTransfer + "urn:xmpp:jingle:transports:ice-udp:0", // JingleTransportIceUdp + "urn:xmpp:jingle:transports:raw-udp:0", // JingleTransportRawUdp + "urn:xmpp:jingle:transports:raw-udp:info:0", // JingleTransportRawUdpInfo + "urn:xmpp:jingle:transports:bytestreams:0", // JingleTransportByteStreams + "urn:xmpp:jingle:transfer:0", // JingleTransfer + "urn:xmpp:jingle:dtmf:0", // Dtmf +#else + "urn:xmpp:jingle:1", // Jingle + "urn:xmpp:jingle:errors:1", // JingleError + "urn:xmpp:jingle:apps:rtp:1", // JingleAppsRtp + "urn:xmpp:jingle:apps:rtp:errors:1", // JingleAppsRtpError + "urn:xmpp:jingle:apps:rtp:info:1", // JingleAppsRtpInfo + "urn:xmpp:jingle:apps:rtp:audio", // JingleAppsRtpAudio + "urn:xmpp:jingle:apps:file-transfer:1", // JingleAppsFileTransfer + "urn:xmpp:jingle:transports:ice-udp:1", // JingleTransportIceUdp + "urn:xmpp:jingle:transports:raw-udp:1", // JingleTransportRawUdp + "urn:xmpp:jingle:transports:raw-udp:info:1", // JingleTransportRawUdpInfo + "urn:xmpp:jingle:transports:bytestreams:1", // JingleTransportByteStreams + "urn:xmpp:jingle:transfer:0", // JingleTransfer + "urn:xmpp:jingle:dtmf:0", // JingleDtmf +#endif + "http://www.google.com/session", // JingleSession + "http://www.google.com/session/phone", // JingleAudio + "http://www.google.com/transport/p2p", // JingleTransport + "urn:xmpp:jingle:apps:rtp:info", // JingleRtpInfoOld + "http://jabber.org/protocol/jingle/info/dtmf", // DtmfOld + "jabber:x:oob", // XOob + "http://jabber.org/protocol/command", // Command + "msgoffline", // MsgOffline +}; + +const String XMPPError::s_array[Count] = { + "", // NoError + "bad-format", // BadFormat + "bad-namespace-prefix", // BadNamespace + "conflict", // Conflict + "connection-timeout", // ConnTimeout + "host-gone", // HostGone + "host-unknown", // HostUnknown + "improper-addressing", // BadAddressing + "internal-server-error", // Internal + "invalid-from", // InvalidFrom + "invalid-id", // InvalidId + "invalid-namespace", // InvalidNamespace + "invalid-xml", // InvalidXml + "not-authorized", // NotAuth + "policy-violation", // Policy + "remote-connection-failed", // RemoteConn + "resource-constraint", // ResConstraint + "restricted-xml", // RestrictedXml + "see-other-host", // SeeOther + "system-shutdown", // Shutdown + "undefined-condition", // UndefinedCondition + "unsupported-encoding", // UnsupportedEnc + "unsupported-stanza-type", // UnsupportedStanza + "unsupported-version", // UnsupportedVersion + "xml-not-well-formed", // Xml + "aborted", // Aborted + "account-disabled", // AccountDisabled + "credentials-expired", // CredentialsExpired + "encryption-required", // EncryptionRequired + "incorrect-encoding", // IncorrectEnc + "invalid-authzid", // InvalidAuth + "invalid-mechanism", // InvalidMechanism + "malformed-request", // MalformedRequest + "mechanism-too-weak", // MechanismTooWeak + "not-authorized", // NotAuthorized + "temporary-auth-failure", // TempAuthFailure + "transition-needed", // TransitionNeeded + "resource-constraint", // ResourceConstraint + "not-allowed", // NotAllowed + "bad-request", // BadRequest + "feature-not-implemented", // FeatureNotImpl + "forbidden", // Forbidden + "gone", // Gone + "item-not-found", // ItemNotFound + "jid-malformed", // BadJid + "not-acceptable", // NotAcceptable + "payment-required", // Payment + "recipient-unavailable", // Unavailable + "redirect", // Redirect + "registration-required", // Reg + "remote-server-not-found", // NoRemote + "remote-server-timeout", // RemoteTimeout + "service-unavailable", // ServiceUnavailable + "subscription-required", // Subscription + "unexpected-request", // Request + "", // SocketError + "cancel", // TypeCancel + "continue", // TypeContinue + "modify", // TypeModify + "auth", // TypeAuth + "wait", // TypeWait +}; + +const String XmlTag::s_array[Count] = { + "stream", // Stream + "error", // Error + "features", // Features + "register", // Register + "starttls", // Starttls + "auth", // Auth + "challenge", // Challenge + "abort", // Abort + "aborted", // Aborted + "response", // Response + "proceed", // Proceed + "success", // Success + "failure", // Failure + "mechanisms", // Mechanisms + "mechanism", // Mechanism + "session", // Session + "iq", // Iq + "message", // Message + "presence", // Presence + "query", // Query + "vCard", // VCard + "jingle", // Jingle + "description", // Description + "payload-type", // PayloadType + "transport", // Transport + "candidate", // Candidate + "body", // Body + "subject", // Subject + "feature", // Feature + "bind", // Bind + "resource", // Resource + "transfer", // Transfer + "hold", // Hold + "active", // Active + "ringing", // Ringing + "mute", // Mute + "registered", // Registered + "remove", // Remove + "jid", // Jid + "username", // Username + "password", // Password + "digest", // Digest + "required", // Required + "optional", // Optional + "dtmf", // Dtmf + "dtmf-method", // DtmfMethod + "command", // Command + "text", // Text + "item", // Item + "group", // Group + "reason", // Reason + "content", // Content + "trying", // Trying + "received", // Received + "file", // File + "offer", // Offer + "request", // Request + "streamhost", // StreamHost + "streamhost-used", // StreamHostUsed + "ping", // Ping + "encryption", // Encryption + "crypto", // Crypto + "parameter", // Parameter + "identity", // Identity + "priority", // Priority + "c", // EntityCapsTag +}; + +XMPPNamespace XMPPUtils::s_ns; +XMPPError XMPPUtils::s_error; +XmlTag XMPPUtils::s_tag; + +const TokenDict XMPPUtils::s_presence[] = { + {"probe", Probe}, + {"subscribe", Subscribe}, + {"subscribed", Subscribed}, + {"unavailable", Unavailable}, + {"unsubscribe", Unsubscribe}, + {"unsubscribed", Unsubscribed}, + {"error", PresenceError}, + {0,0} +}; + +const TokenDict XMPPUtils::s_msg[] = { + {"chat", Chat}, + {"groupchat", GroupChat}, + {"headline", HeadLine}, + {"normal", Normal}, + {"error", MsgError}, + {0,0} +}; + +const TokenDict XMPPUtils::s_iq[] = { + {"set", IqSet}, + {"get", IqGet}, + {"result", IqResult}, + {"error", IqError}, + {0,0} +}; + +const TokenDict XMPPUtils::s_commandAction[] = { + {"execute", CommExecute}, + {"cancel", CommCancel}, + {"prev", CommPrev}, + {"next", CommNext}, + {"complete", CommComplete}, + {0,0} +}; + +const TokenDict XMPPUtils::s_commandStatus[] = { + {"executing", CommExecuting}, + {"completed", CommCompleted}, + {"cancelled", CommCancelled}, + {0,0} +}; + +const TokenDict XMPPUtils::s_authMeth[] = { + {"DIGEST-SHA1", AuthSHA1}, + {"DIGEST-MD5", AuthMD5}, + {"PLAIN", AuthPlain}, + {"DIALBACK", AuthDialback}, + {0,0} +}; + +const TokenDict XMPPDirVal::s_names[] = { + {"none", None}, + {"to", To}, + {"from", From}, + {"pending_in", PendingIn}, + {"pending_out", PendingOut}, + {0,0}, +}; + + +// Compare 2 Strings. Return -1 if s1s2 or 0 +static inline int cmpBytes(const String& s1, const String& s2) +{ + if (s1 && s2) { + if (s1.length() == s2.length()) + return ::memcmp(s1.c_str(),s2.c_str(),s1.length()); + if (s1.length() < s2.length()) { + int res = ::memcmp(s1.c_str(),s2.c_str(),s1.length()); + if (res) + return res; + return -1; + } + int res = ::memcmp(s1.c_str(),s2.c_str(),s2.length()); + return res ? res : 1; + } + if (s1 || s2) + return s1 ? 1 : -1; + return 0; +} + + +/* + * JabberID + */ +// Assignement operator from JabberID +JabberID& JabberID::operator=(const JabberID& src) +{ + assign(src.c_str()); + m_node = src.node(); + m_domain = src.domain(); + m_resource = src.resource(); + m_bare = src.bare(); + return *this; +} + +void JabberID::set(const char* jid) +{ + this->assign(jid); + parse(); +} + +void JabberID::set(const char* node, const char* domain, const char* resource) +{ + m_node = node; + m_domain = domain; + m_resource = resource; + String::clear(); + if (m_node) + *this << m_node << "@"; + *this << m_domain; + m_bare = *this; + if (m_node && m_resource) + *this << "/" << m_resource; +} + +bool JabberID::valid(const String& value) +{ + if (value.null()) + return true; + return s_regExpValid.matches(value); +} + +Regexp JabberID::s_regExpValid("^\\([[:alnum:]]*\\)"); + +#if 0 +~`!#$%^*_-+=()[]{}|\;?. +#endif + +void JabberID::parse() +{ + String tmp = *this; + int i = tmp.find('@'); + if (i == -1) + m_node = ""; + else { + m_node = tmp.substr(0,i); + tmp = tmp.substr(i+1,tmp.length()-i-1); + } + i = tmp.find('/'); + if (i == -1) { + m_domain = tmp; + m_resource = ""; + } + else { + m_domain = tmp.substr(0,i); + m_resource = tmp.substr(i+1,tmp.length()-i-1); + } + // Set bare JID + m_bare = ""; + if (m_node) + m_bare << m_node << "@"; + m_bare << m_domain; +} + +// Get an empty JabberID +const JabberID& JabberID::empty() +{ + return s_emptyJid; +} + + +/* + * JIDIdentity + */ +void JIDIdentity::fromXml(XmlElement* identity) +{ + if (!identity) + return; + m_category = identity->getAttribute("category"); + m_type = identity->getAttribute("type"); + m_name = identity->getAttribute("name"); +} + +XmlElement* JIDIdentity::createIdentity(const char* category, const char* type, + const char* name) +{ + XmlElement* id = XMPPUtils::createElement(XmlTag::Identity); + id->setAttribute("category",category); + id->setAttribute("type",type); + id->setAttribute("name",name); + return id; +} + + +/* + * JIDIdentityList + */ +// Fill an xml element with identities held by this list +void JIDIdentityList::toXml(XmlElement* parent) const +{ + if (!parent) + return; + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + JIDIdentity* id = static_cast(o->get()); + parent->addChild(id->toXml()); + } +} + +// Add identity children from an xml element +void JIDIdentityList::fromXml(XmlElement* parent) +{ + if (!parent) + return; + XmlElement* id = XMPPUtils::findFirstChild(*parent,XmlTag::Identity); + for (; id; id = XMPPUtils::findNextChild(*parent,id,XmlTag::Identity)) + append(new JIDIdentity(id)); +} + + +/* + * XMPPFeature + */ +// Build an xml element from this feature +XmlElement* XMPPFeature::build(bool addReq) +{ + XmlElement* xml = XMPPUtils::createElement(m_xml); + // Set default namespace + xml->setXmlns(String::empty(),true,*this); + if (addReq) + addReqChild(*xml); + return xml; +} + +// Build a feature element from this one +XmlElement* XMPPFeature::buildFeature() +{ + XmlElement* x = XMPPUtils::createElement(XmlTag::Feature); + x->setAttribute("var",c_str()); + return x; +} + +// Add a required/optional child to an element +void XMPPFeature::addReqChild(XmlElement& xml) +{ +#ifndef RFC3920 + if (m_required) + xml.addChild(XMPPUtils::createElement(XmlTag::Required)); + else + xml.addChild(XMPPUtils::createElement(XmlElement::Optional)); +#endif +} + +// Build a feature from a stream:features child +XMPPFeature* XMPPFeature::fromStreamFeature(XmlElement& xml) +{ + int t = XMPPUtils::tag(xml); + if (t == XmlTag::Count) { + DDebug(DebugStub,"XMPPFeature::fromStreamFeature() unhandled tag '%s'", + xml.tag()); + return 0; + } + XMPPFeature* f = 0; + bool required = XMPPUtils::required(xml); + if (t == XmlTag::Mechanisms && XMPPUtils::hasXmlns(xml,XMPPNamespace::Sasl)) { + int mech = 0; + // Get mechanisms + XmlElement* x = XMPPUtils::findFirstChild(xml,XmlTag::Mechanism); + for (; x; x = XMPPUtils::findNextChild(xml,x,XmlTag::Mechanism)) { + const String& n = x->getText(); + if (!n) + continue; + int m = XMPPUtils::authMeth(n); + if (m) + mech |= m; + else + Debug(DebugStub,"XMPPFeature::fromStreamFeature() Unhandled mechanism '%s'", + n.c_str()); + } + f = new XMPPFeatureSasl(mech,required); + } + else { + String* xmlns = xml.xmlns(); + if (!TelEngine::null(xmlns)) + f = new XMPPFeature(t,xmlns->c_str(),required); + } + return f; +} + +void XMPPFeature::setFeature(int feature) +{ + assign(XMPPUtils::s_ns.at(feature)); +} + + +/* + * XMPPFeatureSasl + */ +// Build an xml element from this feature +XmlElement* XMPPFeatureSasl::build(bool addReq) +{ + if (!m_mechanisms) + return 0; + XmlElement* xml = XMPPFeature::build(false); + for (const TokenDict* t = XMPPUtils::s_authMeth; t->value; t++) + if (mechanism(t->value)) + xml->addChild(XMPPUtils::createElement(XmlTag::Mechanism,t->token)); + if (addReq) + addReqChild(*xml); + return xml; +} + + +/* + * XMPPFeatureList + */ +// Add a list of features to this list. Don't check duplicates +void XMPPFeatureList::add(XMPPFeatureList& list) +{ + ObjList* o = list.skipNull(); + while (o) { + append(o->remove(false)); + o = list.skipNull(); + } +} + +// Re-build this list from stream features +void XMPPFeatureList::fromStreamFeatures(XmlElement& xml) +{ + reset(); + m_identities.fromXml(&xml); + for (XmlElement* x = xml.findFirstChild(); x; x = xml.findNextChild(x)) { + // Process only elements in default namespace + if (!x->isDefaultNs()) + continue; + // Skip identities + if (x->toString() == XMPPUtils::s_tag[XmlTag::Identity]) + continue; + XMPPFeature* f = XMPPFeature::fromStreamFeature(*x); + if (f) + append(f); + } +} + +// Re-build this list from disco info responses +void XMPPFeatureList::fromDiscoInfo(XmlElement& xml) +{ + reset(); + m_identities.fromXml(&xml); + XmlElement* x = XMPPUtils::findFirstChild(xml,XmlTag::Feature); + for (; x; x = XMPPUtils::findNextChild(xml,x,XmlTag::Feature)) { + // Process only elements in default namespace + if (!x->isDefaultNs()) + continue; + const char* var = x->attribute("var"); + if (!TelEngine::null(var)) + append(new XMPPFeature(XmlTag::Feature,var)); + } +} + +// Find a specific feature +XMPPFeature* XMPPFeatureList::get(int feature) +{ + const String& name = XMPPUtils::s_ns.at(feature); + return name ? get(name) : 0; +} + +// Build stream features from this list +XmlElement* XMPPFeatureList::buildStreamFeatures() +{ + XmlElement* xml = XMPPUtils::createElement(XmlTag::Features); + XMPPUtils::setStreamXmlns(*xml); + for (ObjList* o = skipNull(); o; o = o->skipNext()) + xml->addChild((static_cast(o->get()))->build()); + return xml; +} + +// Build an iq query disco info result from this list +XmlElement* XMPPFeatureList::buildDiscoInfo(const char* from, const char* to, + const char* id, const char* node, const char* cap) +{ + XmlElement* res = XMPPUtils::createIqDisco(true,false,from,to,id,node,cap); + XmlElement* query = XMPPUtils::findFirstChild(*res,XmlTag::Query); + if (query) + add(*query); + return res; +} + +// Add this list to an xml element +void XMPPFeatureList::add(XmlElement& xml) +{ + m_identities.toXml(&xml); + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + XMPPFeature* f = static_cast(o->get()); + xml.addChild(f->buildFeature()); + } +} + +// Update the entity capabilities hash +void XMPPFeatureList::updateEntityCaps() +{ + m_entityCapsHash.clear(); + ObjList* o = 0; + // Sort identities by category/type/name + ObjList i; + for (o = m_identities.skipNull(); o; o = o->skipNext()) { + JIDIdentity* id = static_cast(o->get()); + ObjList* oi = i.skipNull(); + for (; oi; oi = oi->skipNext()) { + JIDIdentity* crt = static_cast(oi->get()); + #define CMP_IDENT(a,b) { \ + int res = cmpBytes(a,b); \ + if (res == -1) \ + break; \ + if (res == 1) \ + continue; \ + } + CMP_IDENT(id->m_category,crt->m_category) + // Equal category: check type + CMP_IDENT(id->m_type,crt->m_type) + // Equal category and type: check name + CMP_IDENT(id->m_name,crt->m_name) + // All fields are equal, continue (keep the original order) + #undef CMP_IDENT + } + if (oi) + oi->insert(id)->setDelete(false); + else + i.append(id)->setDelete(false); + } + + // Sort features + ObjList f; + for (o = skipNull(); o; o = o->skipNext()) { + XMPPFeature* feature = static_cast(o->get()); + ObjList* of = f.skipNull(); + for (; of; of = of->skipNext()) { + String* crt = static_cast(of->get()); + if (cmpBytes(*feature,*crt) == -1) + break; + } + if (of) + of->insert(feature)->setDelete(false); + else + f.append(feature)->setDelete(false); + } + + // Build SHA + SHA1 sha; + for (o = i.skipNull(); o; o = o->skipNext()) { + JIDIdentity* id = static_cast(o->get()); + sha << id->m_category << "/" << id->m_type << "//" << id->m_name << "<"; + } + for (o = f.skipNull(); o; o = o->skipNext()) { + XMPPFeature* tmp = static_cast(o->get()); + sha << tmp->c_str() << "<"; + } + Base64 b((void*)sha.rawDigest(),20); + b.encode(m_entityCapsHash); +} + + +/* + * XMPPUtils + */ +// Partially build an XML element from another one. +XmlElement* XMPPUtils::createElement(const XmlElement& src, bool response, bool result) +{ + XmlElement* xml = new XmlElement(src.toString()); + if (response) { + xml->setAttributeValid("from",src.attribute("to")); + xml->setAttributeValid("to",src.attribute("from")); + xml->setAttribute("type",result ? "result" : "error"); + } + else { + xml->setAttributeValid("from",src.attribute("from")); + xml->setAttributeValid("to",src.attribute("to")); + xml->setAttributeValid("type",src.attribute("type")); + } + xml->setAttributeValid("id",src.attribute("id")); + return xml; +} + +XmlElement* XMPPUtils::createIq(IqType type, const char* from, + const char* to, const char* id) +{ + XmlElement* iq = createElement(XmlTag::Iq); + iq->setAttributeValid("type",lookup(type,s_iq,"")); + iq->setAttributeValid("from",from); + iq->setAttributeValid("to",to); + iq->setAttributeValid("id",id); + return iq; +} + +// Create an 'iq' error from a received element. Consume the received element +// Add the given element to the error stanza if the 'id' attribute is missing +XmlElement* XMPPUtils::createIqError(const char* from, const char* to, XmlElement*& xml, + int type, int error, const char* text) +{ + const char* id = xml->attribute("id"); + XmlElement* iq = createIq(XMPPUtils::IqError,from,to,id); + if (TelEngine::null(id)) { + iq->addChild(xml); + xml = 0; + } + TelEngine::destruct(xml); + iq->addChild(createError(type,error,text)); + return iq; +} + +// Create an 'iq' element of type 'get' with a 'vcard' child +XmlElement* XMPPUtils::createVCard(bool get, const char* from, const char* to, const char* id) +{ + XmlElement* xml = createIq(get ? IqGet : IqSet,from,to,id); + xml->addChild(createElement(XmlTag::VCard,XMPPNamespace::VCard)); + return xml; +} + +XmlElement* XMPPUtils::createCommand(CommandAction action, const char* node, + const char* sessionId) +{ + XmlElement* command = createElement(XmlTag::Command,XMPPNamespace::Command); + if (sessionId) + command->setAttribute("sessionid",sessionId); + command->setAttribute("node",node); + command->setAttribute("action",lookup(action,s_commandAction)); + return command; +} + +// Create a disco info/items 'iq' element with a 'query' child +XmlElement* XMPPUtils::createIqDisco(bool info, bool req, const char* from, + const char* to, const char* id, const char* node, const char* cap) +{ + XmlElement* xml = createIq(req ? IqGet : IqResult,from,to,id); + XmlElement* query = createElement(XmlTag::Query, + info ? XMPPNamespace::DiscoInfo : XMPPNamespace::DiscoItems); + if (!TelEngine::null(node)) { + if (TelEngine::null(cap)) + query->setAttribute("node",node); + else + query->setAttribute("node",String(node) + "#" + cap); + } + xml->addChild(query); + return xml; +} + +// Create a version 'iq' result as defined in XEP-0092 +XmlElement* XMPPUtils::createIqVersionRes(const char* from, const char* to, + const char* id, const char* name, const char* version, const char* os) +{ + XmlElement* query = createElement(XmlTag::Query,XMPPNamespace::IqVersion); + query->addChild(createElement("name",name)); + query->addChild(createElement("version",version)); + if (os) + query->addChild(createElement("os",os)); + return createIqResult(from,to,id,query); +} + +XmlElement* XMPPUtils::createError(int type, int condition, const char* text) +{ + XmlElement* err = createElement(XmlTag::Error); + err->setAttribute("type",s_error[type]); + err->addChild(createElement(s_error[condition],XMPPNamespace::StanzaError)); + if (!TelEngine::null(text)) + err->addChild(createElement(XmlTag::Text,XMPPNamespace::StanzaError,text)); + return err; +} + +// Create an error from a received element. Consume the received element +XmlElement* XMPPUtils::createError(XmlElement* xml, int type, int error, + const char* text) +{ + if (!xml) + return 0; + XmlElement* err = createElement(*xml,true,false); + err->addChild(createError(type,error,text)); + TelEngine::destruct(xml); + return err; +} + +// Build a stream error element +XmlElement* XMPPUtils::createStreamError(int error, const char* text) +{ + XmlElement* xml = createElement(XmlTag::Error); + setStreamXmlns(*xml); + XmlElement* err = createElement(s_error[error],XMPPNamespace::StreamError); + xml->addChild(err); + if (!TelEngine::null(text)) + xml->addChild(createElement(XmlTag::Text,XMPPNamespace::StreamError,text)); + return xml; +} + +// Build a register query element +XmlElement* XMPPUtils::createRegisterQuery(IqType type, const char* from, + const char* to, const char* id, + XmlElement* child1, XmlElement* child2, XmlElement* child3) +{ + XmlElement* iq = createIq(type,from,to,id); + XmlElement* q = XMPPUtils::createElement(XmlTag::Query,XMPPNamespace::IqRegister); + if (child1) + q->addChild(child1); + if (child2) + q->addChild(child2); + if (child3) + q->addChild(child3); + iq->addChild(q); + return iq; +} + +// Find an element's first child element in a given namespace +XmlElement* XMPPUtils::findFirstChild(const XmlElement& xml, int t, int ns) +{ + if (t < XmlTag::Count) + if (ns < XMPPNamespace::Count) + return xml.findFirstChild(&s_tag[t],&s_ns[ns]); + else + return xml.findFirstChild(&s_tag[t],0); + else if (ns < XMPPNamespace::Count) + return xml.findFirstChild(0,&s_ns[ns]); + return xml.findFirstChild(); +} + +// Find an element's next child element +XmlElement* XMPPUtils::findNextChild(const XmlElement& xml, XmlElement* start, + int t, int ns) +{ + if (t < XmlTag::Count) + if (ns < XMPPNamespace::Count) + return xml.findNextChild(start,&s_tag[t],&s_ns[ns]); + else + return xml.findNextChild(start,&s_tag[t],0); + else if (ns < XMPPNamespace::Count) + return xml.findNextChild(start,0,&s_ns[ns]); + return xml.findNextChild(start); +} + +// Decode an 'error' XML element +void XMPPUtils::decodeError(XmlElement* xml, String& error, String& text) +{ + if (!xml) + return; + error = ""; + text = ""; + int t; + int ns; + if (!getTag(*xml,t,ns)) + return; + switch (t) { + case XmlTag::Error: + // Stream error + if (ns == XMPPNamespace::Stream) + decodeError(xml,false,error,text); + break; + case XmlTag::Iq: + case XmlTag::Presence: + case XmlTag::Message: + // Stanza in stream namespace + if (ns == XMPPNamespace::Server || ns == XMPPNamespace::Client) + decodeError(xml,true,error,text); + break; + } +} + +// Decode a stream or stanza error condition element +void XMPPUtils::decodeError(XmlElement* xml, bool stanza, String& error, String& text) +{ + if (!xml) + return; + int ns = stanza ? XMPPNamespace::StanzaError : XMPPNamespace::StreamError; + XmlElement* ch = findFirstChild(*xml,XmlTag::Count,ns); + for (; ch; ch = findNextChild(*xml,ch,XmlTag::Count,ns)) { + if (ch->unprefixedTag() != s_tag[XmlTag::Text]) + error = ch->unprefixedTag(); + else + text = ch->getText(); + if (error && text) + break; + } +} + +// Create a 'delay' element as defined in XEP-0203 +XmlElement* XMPPUtils::createDelay(unsigned int timeSec, const char* from, + unsigned int fractions, const char* text) +{ + XmlElement* x = createElement("delay",XMPPNamespace::Delay,text); + x->setAttributeValid("from",from); + String time; + encodeDateTimeSec(time,timeSec,fractions); + x->setAttributeValid("stamp",time); + return x; +} + +// Check if an element has a child with 'priority' tag +int XMPPUtils::priority(XmlElement& xml, int defVal) +{ + XmlElement* p = findFirstChild(xml,XmlTag::Priority); + if (!p) + return defVal; + String prio(p->getText()); + prio.trimBlanks(); + return prio.toInteger(defVal); +} + +inline void addPaddedVal(String& buf, int val, const char* sep) +{ + if (val < 10) + buf << "0"; + buf << val << sep; +} + +// Encode EPOCH time given in seconds to a date/time profile as defined in +// XEP-0082 +void XMPPUtils::encodeDateTimeSec(String& buf, unsigned int timeSec, + unsigned int fractions) +{ + int y; + unsigned int m,d,hh,mm,ss; + if (!Time::toDateTime(timeSec,y,m,d,hh,mm,ss)) + return; + buf << y << "-"; + addPaddedVal(buf,m,"-"); + addPaddedVal(buf,d,"T"); + addPaddedVal(buf,hh,":"); + addPaddedVal(buf,mm,":"); + addPaddedVal(buf,ss,""); + if (fractions) + buf << "." << fractions; + buf << "Z"; +} + +// Decode a date/time profile as defined in XEP-0082 and +// XML Schema Part 2: Datatypes Second Edition to EPOCH time +unsigned int XMPPUtils::decodeDateTimeSec(const String& time, unsigned int* fractions) +{ + // XML Schema Part 2: Datatypes Second Edition + // (see http://www.w3.org/TR/xmlschema-2/#dateTime) + // Section 3.2.7: dateTime + // Format: [-]yyyy[y+]-mm-ddThh:mm:ss[.s+][Z|[+|-]hh:mm] + // NOTE: The document specify that yyyy may be negative and may have more then 4 digits: + // for now we only accept positive years greater then 1970 + + unsigned int ret = (unsigned int)-1; + unsigned int timeFractions = 0; + while (true) { + // Split date/time + int pos = time.find('T'); + if (pos == -1) + return (unsigned int)-1; + // Decode date + if (time.at(0) == '-') + break; + int year = 0; + unsigned int month = 0; + unsigned int day = 0; + String date = time.substr(0,pos); + ObjList* list = date.split('-'); + bool valid = (list->length() == 3 && list->count() == 3); + if (valid) { + year = (*list)[0]->toString().toInteger(-1,10); + month = (unsigned int)(*list)[1]->toString().toInteger(-1,10); + day = (unsigned int)(*list)[2]->toString().toInteger(-1,10); + valid = year >= 1970 && month && month <= 12 && day && day <= 31; + } + TelEngine::destruct(list); + if (valid) + DDebug(DebugAll, + "XMPPUtils::decodeDateTimeSec() decoded year=%d month=%u day=%u from '%s'", + year,month,day,time.c_str()); + else { + DDebug(DebugNote, + "XMPPUtils::decodeDateTimeSec() incorrect date=%s in '%s'", + date.c_str(),time.c_str()); + break; + } + // Decode Time + String t = time.substr(pos + 1,8); + if (t.length() != 8) + break; + unsigned int hh = 0; + unsigned int mm = 0; + unsigned int ss = 0; + int offsetSec = 0; + list = t.split(':'); + valid = (list->length() == 3 && list->count() == 3); + if (valid) { + hh = (unsigned int)(*list)[0]->toString().toInteger(-1,10); + mm = (unsigned int)(*list)[1]->toString().toInteger(-1,10); + ss = (unsigned int)(*list)[2]->toString().toInteger(-1,10); + valid = (hh <= 23 && mm <= 59 && ss <= 59) || + (hh == 24 && mm == 0 && ss == 0); + } + TelEngine::destruct(list); + if (!valid) { + DDebug(DebugNote, + "XMPPUtils::decodeDateTimeSec() incorrect time=%s in '%s'", + t.c_str(),time.c_str()); + break; + } +#ifdef DEBUG + else + Debug(DebugAll, + "XMPPUtils::decodeDateTimeSec() decoded hour=%u minute=%u sec=%u from '%s'", + hh,mm,ss,time.c_str()); +#endif + // Get the rest + unsigned int parsed = date.length() + t.length() + 1; + unsigned int len = time.length() - parsed; + const char* buf = time.c_str() + parsed; + if (len > 1) { + // Get time fractions + if (buf[0] == '.') { + unsigned int i = 1; + // FIXME: Trailing 0s are not allowed in fractions + for (; i < len && buf[i] >= '0' && buf[i] <= '9'; i++) + ; + String fr(buf + 1,i - 1); + if (i > 2) + timeFractions = (unsigned int)fr.toInteger(-1); + else + timeFractions = (unsigned int)-1; + if (timeFractions != (unsigned int)-1) + DDebug(DebugAll, + "XMPPUtils::decodeDateTimeSec() decoded fractions=%u from '%s'", + timeFractions,time.c_str()); + else { + DDebug(DebugNote, + "XMPPUtils::decodeDateTimeSec() incorrect fractions=%s in '%s'", + fr.c_str(),time.c_str()); + break; + } + len -= i; + buf += i; + } + // Get offset + if (len > 1) { + int sign = 1; + if (*buf == '-' || *buf == '+') { + if (*buf == '-') + sign = -1; + buf++; + len--; + } + String offs(buf,5); + // We should have at least 5 bytes: hh:ss + if (len < 5 || buf[2] != ':') { + DDebug(DebugNote, + "XMPPUtils::decodeDateTimeSec() incorrect time offset=%s in '%s'", + offs.c_str(),time.c_str()); + break; + } + unsigned int hhOffs = (unsigned int)offs.substr(0,2).toInteger(-1,10); + unsigned int mmOffs = (unsigned int)offs.substr(3,2).toInteger(-1,10); + // XML Schema Part 2 3.2.7.3: the hour may be 0..13. It can be 14 if minute is 0 + if (mmOffs > 59 || (hhOffs > 13 && !mmOffs)) { + DDebug(DebugNote, + "XMPPUtils::decodeDateTimeSec() incorrect time offset values hour=%u minute=%u in '%s'", + hhOffs,mmOffs,time.c_str()); + break; + } + DDebug(DebugAll, + "XMPPUtils::decodeDateTimeSec() decoded time offset '%c' hour=%u minute=%u from '%s'", + sign > 0 ? '+' : '-',hhOffs,mmOffs,time.c_str()); + offsetSec = sign * (hhOffs * 3600 + mmOffs * 60); + buf += 5; + len -= 5; + } + } + // Check termination markup + if (len && (len != 1 || *buf != 'Z')) { + DDebug(DebugNote, + "XMPPUtils::decodeDateTimeSec() '%s' is incorrectly terminated '%s'", + time.c_str(),buf); + break; + } + ret = Time::toEpoch(year,month,day,hh,mm,ss,offsetSec); +#ifdef DEBUG + if (ret == (unsigned int)-1) + Debug(DebugNote, + "XMPPUtils::decodeDateTimeSec() failed to convert '%s'", + time.c_str()); +#endif + break; + } + + if (ret != (unsigned int)-1) { + if (fractions) + *fractions = timeFractions; + } + return ret; +} + +void XMPPUtils::print(String& xmlStr, XmlChild& xml, bool verbose) +{ + if (verbose) + xmlStr << "\r\n-----"; + const XmlElement* element = xml.xmlElement(); + if (element) { + String indent("\r\n"); + String origindent(" "); + element->toString(xmlStr,false,indent,origindent,false,s_auth); + } + else if (xml.xmlDeclaration()) { + xmlStr << "\r\n"; + xml.xmlDeclaration()->toString(xmlStr,false); + } + else + Debug(DebugStub,"XMPPUtils::print() not implemented for this type!"); + if (verbose) + xmlStr << "\r\n-----"; +} + +// Put an element's name, text and attributes to a list of parameters +void XMPPUtils::toList(XmlElement& xml, NamedList& dest, const char* prefix) +{ + dest.addParam(prefix,xml.tag()); + String pref(String(prefix) + "."); + const String& tmp = xml.getText(); + if (tmp) + dest.addParam(pref,tmp); + unsigned int n = xml.attributes().length(); + for (unsigned int i = 0; i < n; i++) { + NamedString* ns = xml.attributes().getParam(i); + if (ns) + dest.addParam(pref + ns->name(),*ns); + } +} + +bool XMPPUtils::split(NamedList& dest, const char* src, const char sep, + bool nameFirst) +{ + if (!src) + return false; + unsigned int index = 1; + String s = src; + ObjList* obj = s.split(sep,false); + for (ObjList* o = obj->skipNull(); o; o = o->skipNext(), index++) { + String* tmp = static_cast(o->get()); + if (nameFirst) + dest.addParam(*tmp,String(index)); + else + dest.addParam(String(index),*tmp); + } + TelEngine::destruct(obj); + return true; +} + +// Decode a comma separated list of flags and put them into an integr mask +int XMPPUtils::decodeFlags(const String& src, const TokenDict* dict) +{ + if (!dict) + return 0; + int mask = 0; + ObjList* obj = src.split(',',false); + for (ObjList* o = obj->skipNull(); o; o = o->skipNext()) + mask |= lookup(static_cast(o->get())->c_str(),dict); + TelEngine::destruct(obj); + return mask; +} + +// Encode a mask of flags to a comma separated list. +void XMPPUtils::buildFlags(String& dest, int src, const TokenDict* dict) +{ + if (!dict) + return; + for (; dict->token; dict++) + if (0 != (src & dict->value)) + dest.append(dict->token,","); +} + +// Add child elements from a list to a destination element +bool XMPPUtils::addChidren(XmlElement* dest, ObjList& list) +{ + if (!dest) + return false; + ObjList* o = list.skipNull(); + bool added = (0 != o); + for (; o; o = o->skipNext()) { + XmlElement* xml = static_cast(o->get()); + dest->addChild(new XmlElement(*xml)); + } + return added; +} + +// Create a 'c' entity capability element as defined in XEP 0115 +// See XEP 0115 Section 5 +XmlElement* XMPPUtils::createEntityCaps(const String& hash, const char* node) +{ + XmlElement* c = createElement(XmlTag::EntityCapsTag,XMPPNamespace::EntityCaps); + c->setAttributeValid("node",node); + c->setAttribute("hash","sha-1"); + c->setAttribute("ver",hash); + return c; +} + +// Create a 'c' entity capability element as defined by GTalk +XmlElement* XMPPUtils::createEntityCapsGTalkV1() +{ + XmlElement* c = createElement(XmlTag::EntityCapsTag,XMPPNamespace::EntityCaps); + c->setAttribute("node","http://www.google.com/xmpp/client/caps"); + c->setAttribute("ver","1.0"); + c->setAttribute("ext","voice-v1"); + return c; +} + +// Create a presence stanza +XmlElement* XMPPUtils::createPresence(const char* from, + const char* to, Presence type) +{ + XmlElement* presence = createElement(XmlTag::Presence); + presence->setAttributeValid("type",presenceText(type)); + presence->setAttributeValid("from",from); + presence->setAttributeValid("to",to); + return presence; +} + +// Create a message element +XmlElement* XMPPUtils::createMessage(const char* type, const char* from, + const char* to, const char* id, const char* body) +{ + XmlElement* msg = createElement(XmlTag::Message); + msg->setAttributeValid("type",type); + msg->setAttributeValid("from",from); + msg->setAttributeValid("to",to); + msg->setAttributeValid("id",id); + if (body) + msg->addChild(createElement(XmlTag::Body,body)); + return msg; +} + +// Build a dialback 'db:result' xml element used to send the dialback key or +XmlElement* XMPPUtils::createDialbackKey(const char* from, const char* to, + const char* key) +{ + XmlElement* db = createElement("result",key); + setDbXmlns(*db); + db->setAttribute("from",from); + db->setAttribute("to",to); + return db; +} + +// Build a dialback 'db:result' xml element used to send a dialback key response +XmlElement* XMPPUtils::createDialbackResult(const char* from, const char* to, + bool valid) +{ + XmlElement* db = createElement("result"); + setDbXmlns(*db); + db->setAttribute("from",from); + db->setAttribute("to",to); + db->setAttribute("type",valid ? "valid" : "invalid"); + return db; +} + +// Build a dialback 'db:verify' xml element +XmlElement* XMPPUtils::createDialbackVerify(const char* from, const char* to, + const char* id, const char* key) +{ + XmlElement* db = createElement("verify",key); + setDbXmlns(*db); + db->setAttribute("from",from); + db->setAttribute("to",to); + db->setAttribute("id",id); + return db; +} + +// Build a dialback 'db:verify' response xml element +XmlElement* XMPPUtils::createDialbackVerifyRsp(const char* from, const char* to, + const char* id, bool valid) +{ + XmlElement* db = createElement("verify"); + setDbXmlns(*db); + db->setAttribute("from",from); + db->setAttribute("to",to); + db->setAttribute("id",id); + db->setAttribute("type",valid ? "valid" : "invalid"); + return db; +} + +// Retrieve the text of an element's body child +const String& XMPPUtils::body(XmlElement& xml, int ns) +{ + if (ns == XMPPNamespace::Count) + ns = xmlns(xml); + int t,n; + for (XmlElement* b = xml.findFirstChild(); b; b = xml.findNextChild(b)) { + if (getTag(*b,t,n) && t == XmlTag::Body && ns == n) + return b->getText(); + } + return String::empty(); +} + +// Retrieve an xml element from a NamedPointer. Release its ownership +XmlElement* XMPPUtils::getXml(GenObject* gen) +{ + if (!gen) + return 0; + NamedPointer* np = static_cast(gen->getObject("NamedPointer")); + XmlElement* xml = np ? static_cast(np->userObject("XmlElement")) : 0; + if (xml) + np->takeData(); + return xml; +} + +// Retrieve an xml element from a Message parameter +// Try to build (parse) from an extra parameter if not found +XmlElement* XMPPUtils::getXml(NamedList& list, const char* param, const char* extra) +{ + if (!TelEngine::null(param)) { + XmlElement* xml = getXml(list.getParam(param)); + if (xml) { + list.clearParam(param); + return xml; + } + } + if (TelEngine::null(extra)) + return 0; + String* data = list.getParam(extra); + if (!data) + return 0; + XmlElement* xml = getXml(*data); + if (!xml) + DDebug(DebugInfo,"getXml(%s) invalid xml parameter %s='%s", + list.c_str(),extra,data->c_str()); + return xml; +} + +// Retrieve a presence xml element from a list parameter. +// Build a presence stanza from parameters if an element is not found +XmlElement* XMPPUtils::getPresenceXml(NamedList& list, const char* param, + const char* extra, Presence type, bool build) +{ + XmlElement* xml = getXml(list,param,extra); + if (xml || !build) + return xml; + xml = createPresence(0,0,type); +#define SET_TEXT_CHILD(param) { \ + const char* tmp = list.getValue(param); \ + if (tmp) \ + xml->addChild(createElement(param,tmp)); \ + } + SET_TEXT_CHILD("priority") + SET_TEXT_CHILD("show") + SET_TEXT_CHILD("status") +#undef SET_TEXT_CHILD + return xml; +} + +// Retrieve a chat (message) xml element from a list parameter. +// Build a message stanza from parameters if an element is not found +XmlElement* XMPPUtils::getChatXml(NamedList& list, const char* param, + const char* extra, bool build) +{ + XmlElement* xml = getXml(list,param,extra); + if (xml || !build) + return xml; + xml = createMessage(list.getValue("type"),0,0,list.getValue("id"),0); + const char* subject = list.getValue("subject"); + if (!TelEngine::null(subject)) + xml->addChild(createSubject(subject)); + const char* body = list.getValue("body"); + if (!TelEngine::null(body)) + xml->addChild(createBody(body)); + return xml; +} + +// Parse a string to an XmlElement +XmlElement* XMPPUtils::getXml(const String& data) +{ + XmlDomParser dom("XMPPUtils::getXml()",true); + dom.parse(data); + XmlFragment* frag = dom.fragment(); + if (!(frag && frag->getChildren().count() == 1)) + return 0; + XmlChild* child = static_cast(frag->getChildren().skipNull()->get()); + XmlElement* element = child->xmlElement(); + if (element) { + frag->removeChild(child,false); + return element; + } + return 0; +} + + +/* + * XMPPDirVal + */ +// Build a string representation of this object +void XMPPDirVal::toString(String& buf, bool full) const +{ + if (m_value) + if (full) + XMPPUtils::buildFlags(buf,m_value,s_names); + else + XMPPUtils::buildFlags(buf,m_value & ~Pending,s_names); + else + buf << lookup(None,s_names); +} + +// Build a subscription state string representation of this object +void XMPPDirVal::toSubscription(String& buf) const +{ + int val = m_value & ~Pending; + val &= Both; + if (val == Both) + buf << "both"; + else + buf << lookup(val,s_names); +} + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjabber/xmpputils.h b/libs/yjabber/xmpputils.h new file mode 100644 index 00000000..6c8220f4 --- /dev/null +++ b/libs/yjabber/xmpputils.h @@ -0,0 +1,2129 @@ +/** + * xmpputils.h + * Yet Another Jabber Component Protocol Stack + * This file is part of the YATE Project http://YATE.null.ro + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __XMPPUTILS_H +#define __XMPPUTILS_H + +#include +#include + +#ifdef _WINDOWS + +#ifdef LIBYJABBER_EXPORTS +#define YJABBER_API __declspec(dllexport) +#else +#ifndef LIBYJABBER_STATIC +#define YJABBER_API __declspec(dllimport) +#endif +#endif + +#endif /* _WINDOWS */ + +#ifndef YJABBER_API +#define YJABBER_API +#endif + +// Support old RFC 3920 +// If not defined RFC 3920bis changes will be used +#define RFC3920 + +/** + * Holds all Telephony Engine related classes. + */ +namespace TelEngine { + +class StringArray; // A String array +class XMPPNamespace; // XMPP namespaces +class XMPPError; // XMPP errors +class JabberID; // A Jabber ID (JID) +class JIDIdentity; // A JID's identity +class JIDIdentityList; // A list of JID identities +class XMPPFeature; // A feature (stream or JID) +class XMPPFeatureSasl; // A SASL feature +class XMPPFeatureList; // Feature list +class XMPPUtils; // Utilities +class XMPPDirVal; // Direction flags +class XmlElementOut; // An outgoing xml element + +/** + * This class holds a SRV record returned by a query + * The String holds the domain/ip + * @short A SRV record + */ +class YJABBER_API SrvRecord : public String +{ +public: + inline SrvRecord(const char* name, int port, int prio, int weight) + : String(name), m_port(port), m_priority(prio), m_weight(weight) + {} + + /** + * Insert a SrvRecord into a list in the proper location + * @param list Destination list + * @param rec The item to insert + */ + static void insert(ObjList& list, SrvRecord* rec); + + int m_port; + int m_priority; + int m_weight; +}; + +class YJABBER_API Resolver +{ +public: + /** + * Make a SRV query + * @param query The query content + * @param result List of resulting SrvRecord items + * @return 0 on success, error code otherwise + */ + static int srvQuery(const char* query, ObjList& result); +}; + + +/** + * Implements a String array set from an already allocated + * @short A String array + */ +class YJABBER_API StringArray +{ +public: + /** + * Constructor + * @param array The array + * @param len Array length + */ + inline StringArray(const String* array, unsigned int len) + : m_array((String*)array), m_length(len) + {} + + /** + * Return the string at a given index (safe) + * @param index The index in the array + * @return The String at the requested index or an empty one if the index is invalid + */ + inline const String& at(unsigned int index) const + { return index < m_length ? m_array[index] : String::empty(); } + + /** + * Return the string at a given index (unsafe) + * @param index The index in the array + * @return The String at the requested index + */ + inline const String& operator[](unsigned int index) const + { return m_array[index]; } + + /** + * Lookup for an integer associated with a given String + * @param token The String find + * @return Token value or 0 if not found + */ + inline int operator[](const String& token) { + unsigned int i = 0; + for (; i < m_length; i++) + if (m_array[i] == token) + return i; + return m_length; + } + +protected: + String* m_array; + unsigned int m_length; + +private: + StringArray() {} +}; + + +/** + * This class holds the XMPP/Jabber/Jingle namespace enumerations and the associated strings + * @short XMPP namespaces + */ +class YJABBER_API XMPPNamespace : public StringArray +{ +public: + /** + * Namespace type enumeration + */ + enum Type { + Stream = 0, // http://etherx.jabber.org/streams + Client = 1, // jabber:client + Server = 2, // jabber:server + Dialback = 3, // jabber:server:dialback + StreamError = 4, // urn:ietf:params:xml:ns:xmpp-streams + StanzaError = 5, // urn:ietf:params:xml:ns:xmpp-stanzas + Ping = 6, // urn:xmpp:ping + Register = 7, // http://jabber.org/features/iq-register + IqRegister = 8, // jabber:iq:register + IqPrivate = 9, // jabber:iq:private + IqAuth = 10, // jabber:iq:auth + IqAuthFeature = 11, // http://jabber.org/features/iq-auth + IqVersion = 12, // jabber:iq:version + Delay = 13, // urn:xmpp:delay + Tls = 14, // urn:ietf:params:xml:ns:xmpp-tls + Sasl = 15, // urn:ietf:params:xml:ns:xmpp-sasl + Session = 16, // urn:ietf:params:xml:ns:xmpp-session + Bind = 17, // urn:ietf:params:xml:ns:xmpp-bind + Roster = 18, // jabber:iq:roster + DynamicRoster = 19, // jabber:iq:roster-dynamic + DiscoInfo = 20, // http://jabber.org/protocol/disco#info + DiscoItems = 21, // http://jabber.org/protocol/disco#items + EntityCaps = 22, // http://jabber.org/protocol/caps + VCard = 23, // vcard-temp + SIProfileFileTransfer = 24, // http://jabber.org/protocol/si/profile/file-transfer + ByteStreams = 25, // http://jabber.org/protocol/bytestreams + Jingle = 26, // urn:xmpp:jingle:1 + JingleError = 27, // urn:xmpp:jingle:errors:1 + JingleAppsRtp = 28, // urn:xmpp:jingle:apps:rtp:1 + JingleAppsRtpError = 29, // urn:xmpp:jingle:apps:rtp:errors:1 + JingleAppsRtpInfo = 30, // urn:xmpp:jingle:apps:rtp:info:1 + JingleAppsRtpAudio = 31, // urn:xmpp:jingle:apps:rtp:audio + JingleAppsFileTransfer = 32, // urn:xmpp:jingle:apps:file-transfer:1 + JingleTransportIceUdp = 33, // urn:xmpp:jingle:transports:ice-udp:1 + JingleTransportRawUdp = 34, // urn:xmpp:jingle:transports:raw-udp:1 + JingleTransportRawUdpInfo = 35, // urn:xmpp:jingle:transports:raw-udp:info:1 + JingleTransportByteStreams = 36, // urn:xmpp:jingle:transports:bytestreams:1 + JingleTransfer = 37, // urn:xmpp:jingle:transfer:0 + JingleDtmf = 38, // urn:xmpp:jingle:dtmf:0 + JingleSession = 39, // http://www.google.com/session + JingleAudio = 40, // http://www.google.com/session/phone + JingleTransport = 41, // http://www.google.com/transport/p2p + JingleRtpInfoOld = 42, // urn:xmpp:jingle:apps:rtp:info + DtmfOld = 43, // http://jabber.org/protocol/jingle/info/dtmf + XOob = 44, // jabber:x:oob + Command= 45, // http://jabber.org/protocol/command + MsgOffline= 46, // msgoffline + Count = 47, + }; + + /** + * Constructor + */ + inline XMPPNamespace() + : StringArray(s_array,Count) + {} + +private: + static const String s_array[Count]; // Namespace list +}; + + +/** + * This class holds the XMPP error type, error enumerations and associated strings + * @short XMPP errors + */ +class YJABBER_API XMPPError : public StringArray +{ +public: + /** + * Error condition enumeration + */ + enum Type { + NoError = 0, + BadFormat = 1, // bad-format + BadNamespace = 2, // bad-namespace-prefix + Conflict = 3, // conflict + ConnTimeout = 4, // connection-timeout + HostGone = 5, // host-gone + HostUnknown = 6, // host-unknown + BadAddressing = 7, // improper-addressing + Internal = 8, // internal-server-error + InvalidFrom = 9, // invalid-from + InvalidId = 10, // invalid-id + InvalidNamespace = 11, // invalid-namespace + InvalidXml = 12, // invalid-xml + NotAuth = 13, // not-authorized + Policy = 14, // policy-violation + RemoteConn = 15, // remote-connection-failed + ResConstraint = 16, // resource-constraint + RestrictedXml = 17, // restricted-xml + SeeOther = 18, // see-other-host + Shutdown = 19, // system-shutdown + UndefinedCondition = 20, // undefined-condition + UnsupportedEnc = 21, // unsupported-encoding + UnsupportedStanza = 22, // unsupported-stanza-type + UnsupportedVersion = 23, // unsupported-version + Xml = 24, // xml-not-well-formed + Aborted = 25, // aborted + AccountDisabled = 26, // account-disabled + CredentialsExpired = 27, // credentials-expired + EncryptionRequired = 28, // encryption-required + IncorrectEnc = 29, // incorrect-encoding + InvalidAuth = 30, // invalid-authzid + InvalidMechanism = 31, // invalid-mechanism + MalformedRequest = 32, // malformed-request + MechanismTooWeak = 33, // mechanism-too-weak + NotAuthorized = 34, // not-authorized + TempAuthFailure = 35, // temporary-auth-failure + TransitionNeeded = 36, // transition-needed + ResourceConstraint = 37, // resource-constraint + NotAllowed = 38, // not-allowed + BadRequest = 39, // bad-request + FeatureNotImpl = 40, // feature-not-implemented + Forbidden = 41, // forbidden + Gone = 42, // gone + ItemNotFound = 43, // item-not-found + BadJid = 44, // jid-malformed + NotAcceptable = 45, // not-acceptable + Payment = 46, // payment-required + Unavailable = 47, // recipient-unavailable + Redirect = 48, // redirect + Reg = 49, // registration-required + NoRemote = 50, // remote-server-not-found + RemoteTimeout = 51, // remote-server-timeout + ServiceUnavailable = 52, // service-unavailable + Subscription = 53, // subscription-required + Request = 54, // unexpected-request + SocketError = 55, // Don't send any error or stream end tag to remote party + TypeCount = 56 + }; + + /** + * Error type enumeration + */ + enum ErrorType { + TypeCancel = TypeCount, // do not retry (the error is unrecoverable) + TypeContinue = TypeCount + 1, // proceed (the condition was only a warning) + TypeModify = TypeCount + 2, // retry after changing the data sent + TypeAuth = TypeCount + 3, // retry after providing credentials + TypeWait = TypeCount + 4, // retry after waiting (the error is temporary) + Count = TypeCount + 5 + }; + + /** + * Constructor + */ + inline XMPPError() + : StringArray(s_array,Count) + {} + +private: + static const String s_array[Count]; // Error list +}; + +/** + * This class holds a list of XML tags + * @short XML known tags array + */ +class YJABBER_API XmlTag : public StringArray +{ +public: + /** + * Element tag enumeration + */ + enum Type { + Stream = 0, // stream + Error = 1, // error + Features = 2, // features + Register = 3, // register + Starttls = 4, // starttls + Auth = 5, // auth + Challenge = 6, // challenge + Abort = 7, // abort + Aborted = 8, // aborted + Response = 9, // response + Proceed = 10, // proceed + Success = 11, // success + Failure = 12, // failure + Mechanisms = 13, // mechanisms + Mechanism = 14, // mechanism + Session = 15, // session + Iq = 16, // iq + Message = 17, // message + Presence = 18, // presence + Query = 19, // query + VCard = 20, // vCard + Jingle = 21, // jingle + Description = 22, // description + PayloadType = 23, // payload-type + Transport = 24, // transport + Candidate = 25, // candidate + Body = 26, // body + Subject = 27, // subject + Feature = 28, // feature + Bind = 29, // bind + Resource = 30, // resource + Transfer = 31, // transfer + Hold = 32, // hold + Active = 33, // active + Ringing = 34, // ringing + Mute = 35, // mute + Registered = 36, // registered + Remove = 37, // remove + Jid = 38, // jid + Username = 39, // username + Password = 40, // password + Digest = 41, // digest + Required = 42, // required + Optional = 43, // optional + Dtmf = 44, // dtmf + DtmfMethod = 45, // dtmf-method + Command = 46, // command + Text = 47, // text + Item = 48, // item + Group = 49, // group + Reason = 50, // reason + Content = 51, // content + Trying = 52, // trying + Received = 53, // received + File = 54, // file + Offer = 55, // offer + Request = 56, // request + StreamHost = 57, // streamhost + StreamHostUsed = 58, // streamhost-used + Ping = 59, // ping + Encryption = 60, // encryption + Crypto = 61, // crypto + Parameter = 62, // parameter + Identity = 63, // identity + Priority = 64, // priority + EntityCapsTag = 65, // c + Count = 66 + }; + + /** + * Constructor + */ + inline XmlTag() + : StringArray(s_array,Count) + {} + +private: + static const String s_array[Count]; // Tag list +}; + + +/** + * This class holds a Jabber ID + * @short A Jabber ID + */ +class YJABBER_API JabberID : public String +{ + YCLASS(JabberID,String) +public: + /** + * Constructor + */ + inline JabberID() {} + + /** + * Constructor. Constructs a JID from a given string + * @param jid The JID string + */ + inline JabberID(const char* jid) + { set(jid); } + + /** + * Constructor. Constructs a JID from a given string + * @param jid The JID string + */ + inline JabberID(const String& jid) + { set(jid); } + + /** + * Constructor. Constructs a JID from a given string + * @param jid The JID string + */ + inline JabberID(const String* jid) + { set(TelEngine::c_safe(jid)); } + + /** + * Constructor. Constructs a JID from user, domain, resource + * @param node The node + * @param domain The domain + * @param resource The resource + */ + inline JabberID(const char* node, const char* domain, const char* resource = 0) + { set(node,domain,resource); } + + /** + * Copy constructor + * @param src Source Jabber ID + */ + inline JabberID(const JabberID& src) + { *this = src; } + + /** + * Check if this is a valid JID + * @return True if this JID is a valid one + */ + inline bool valid() const + { return null() || m_domain; } + + /** + * Get the node part of the JID + * @return The node part of the JID + */ + inline const String& node() const + { return m_node; } + + /** + * Get the bare JID: "node@domain" + * @return The bare JID + */ + inline const String& bare() const + { return m_bare; } + + /** + * Get the domain part of the JID + * @return The domain part of the JID + */ + inline const String& domain() const + { return m_domain; } + + /** + * Set the domain part of the JID. + * @param d The new domain part of the JID. + */ + inline void domain(const char* d) + { set(m_node.c_str(),d,m_resource.c_str()); } + + /** + * Get the resource part of the JID + * @return The resource part of the JID + */ + inline const String& resource() const + { return m_resource; } + + /** + * Check if this is a full JID + * @return True if this is a full JID + */ + inline bool isFull() const + { return m_node && m_domain && m_resource; } + + /** + * Clear content + */ + inline void clear() { + String::clear(); + m_node.clear(); + m_domain.clear(); + m_resource.clear(); + m_bare.clear(); + } + + /** + * Try to match another JID to this one. If src has a resource compare it too + * (case sensitive). Otherwise compare just the bare JID (case insensitive) + * @param src The JID to match + * @return True if matched + */ + inline bool match(const JabberID& src) const + { return (src.resource().null() || (resource() == src.resource())) && (bare() &= src.bare()); } + + /** + * Assignement operator from JabberID + * @param src The JID to copy from + * @return This object + */ + JabberID& operator=(const JabberID& src); + + /** + * Assignement operator from String + * @param src The string + * @return This object + */ + inline JabberID& operator=(const String& src) + { set(src); return *this; } + + /** + * Assignement operator from String pointer + * @param src The string + * @return This object + */ + inline JabberID& operator=(const String* src) + { set(TelEngine::c_safe(src)); return *this; } + + /** + * Equality operator. Do a case senitive resource comparison and a case + * insensitive bare jid comparison + * @param src The JID to compare with + * @return True if equal + */ + inline bool operator==(const JabberID& src) const + { return (resource() == src.resource()) && (bare() &= src.bare()); } + + /** + * Equality operator. Build a temporary JID and compare with it + * @param src The string to compare with + * @return True if equal + */ + inline bool operator==(const String& src) const + { JabberID tmp(src); return operator==(tmp); } + + /** + * Inequality operator + * @param src The JID to compare with + * @return True if not equal + */ + inline bool operator!=(const JabberID& src) const + { return !operator==(src); } + + /** + * Inequality operator + * @param src The string to compare with + * @return True if not equal + */ + inline bool operator!=(const String& src) const + { return !operator==(src); } + + /** + * Set the resource part of the JID + * @param res The new resource part of the JID + */ + inline void resource(const char* res) + { set(m_node.c_str(),m_domain.c_str(),res); } + + /** + * Set the data + * @param jid The JID string to assign + */ + void set(const char* jid); + + /** + * Set the data + * @param node The node + * @param domain The domain + * @param resource The resource + */ + void set(const char* node, const char* domain, const char* resource = 0); + + /** + * Get an empty JabberID + * @return A global empty JabberID + */ + static const JabberID& empty(); + + /** + * Check if the given string contains valid characters + * @param value The string to check + * @return True if value is valid or 0. False if value is a non empty invalid string + */ + static bool valid(const String& value); + + /** + * Keep the regexp used to check the validity of a string + */ + static Regexp s_regExpValid; + +private: + void parse(); // Parse the string. Set the data + + String m_node; // The node part + String m_domain; // The domain part + String m_resource; // The resource part + String m_bare; // The bare JID: node@domain +}; + + +/** + * This class holds an identity for a JID + * See http://xmpp.org/registrar/disco-categories.html for identity categories + * and associated types + * @short A JID identity + */ +class YJABBER_API JIDIdentity : public GenObject +{ + YCLASS(JIDIdentity,GenObject) +public: + /** + * Constructor. Build a JID identity + * @param c The JID's category + * @param t The JID's type + * @param name Optional identity (JID) name + */ + inline JIDIdentity(const char* c, const char* t, const char* name = 0) + : m_category(c), m_type(t), m_name(name) + {} + + /** + * Constructor. Build a JID identity from xml + * @param identity The identity element + */ + inline JIDIdentity(XmlElement* identity) + { fromXml(identity); } + + /** + * Build an XML element from this identity + * @return XmlElement pointer or 0 if category or type are empty + */ + inline XmlElement* toXml() const { + if (!(m_category && m_type)) + return 0; + return createIdentity(m_category,m_type,m_name); + } + + /** + * Update this identity from an XML element + * @param identity The source element + */ + void fromXml(XmlElement* identity); + + /** + * Create an 'identity' element + * @param category The 'category' attribute + * @param type The 'type' attribute + * @param name The 'name' attribute + * @return A valid XmlElement pointer + */ + static XmlElement* createIdentity(const char* category, + const char* type, const char* name); + + String m_category; + String m_type; + String m_name; +}; + + +/** + * This class holds a list of JID identities + * @short A list of JID identities + */ +class YJABBER_API JIDIdentityList : public ObjList +{ + YCLASS(JIDIdentityList,ObjList) +public: + /** + * Fill an xml element with identities held by this list + * @param parent The parent element to fill + */ + void toXml(XmlElement* parent) const; + + /** + * Add identity children from an xml element + * @param parent The element containing the identity children + */ + void fromXml(XmlElement* parent); +}; + +/** + * This class holds an XMPP feature + * @short A feature + */ +class YJABBER_API XMPPFeature : public String +{ + YCLASS(XMPPFeature,GenObject) +public: + /** + * Constructor + * @param xml XML element tag as enumeration + * @param feature The feature (namespace) index + * @param required True if this feature is required + */ + inline XMPPFeature(int xml, int feature, bool required = false) + : m_xml(xml), m_required(required) + { setFeature(feature); } + + /** + * Constructor + * @param xml XML element tag as enumeration + * @param feature The feature name + * @param required True if this feature is required + */ + inline XMPPFeature(int xml, const char* feature, bool required = false) + : String(feature), m_xml(xml), m_required(required) + {} + + /** + * Constructor. Build from feature index + * @param feature The feature + */ + inline XMPPFeature(int feature) + : m_xml(XmlTag::Count), m_required(false) + { setFeature(feature); } + + /** + * Constructor. Build from feature name + * @param feature The feature + */ + inline XMPPFeature(const char* feature) + : String(feature), m_xml(XmlTag::Count), m_required(false) + {} + + /** + * Destructor + */ + virtual ~XMPPFeature() + {} + + /** + * Check if this feature is a required one + * @return True if this feature is a required one + */ + inline bool required() const + { return m_required; } + + /** + * Build an xml element from this feature + * @param addReq True to add the required/optional child + * @return XmlElement pointer or 0 + */ + virtual XmlElement* build(bool addReq = true); + + /** + * Build a feature element from this one + * @return XmlElement pointer + */ + virtual XmlElement* buildFeature(); + + /** + * Add a required/optional child to an element + * @param xml Destination element + */ + void addReqChild(XmlElement& xml); + + /** + * Build a feature from a stream:features child + * @param xml The feature element to parse + * @return XMPPFeature pointer or 0 if unknown + */ + static XMPPFeature* fromStreamFeature(XmlElement& xml); + +private: + void setFeature(int feature); + + int m_xml; // Element tag as enumeration + bool m_required; // Required flag +}; + + +/** + * This class holds a SASL feature along with authentication mechanisms + * @short A SASL feature + */ +class YJABBER_API XMPPFeatureSasl : public XMPPFeature +{ + YCLASS(XMPPFeatureSasl,XMPPFeature) +public: + /** + * Constructor + * @param mech Authentication mechanism(s) + * @param required Required flag + */ + inline XMPPFeatureSasl(int mech, bool required = false) + : XMPPFeature(XmlTag::Mechanisms,XMPPNamespace::Sasl,required), + m_mechanisms(mech) + {} + + /** + * Get the authentication mechanisms + * @return The authentication mechanisms used by the JID + */ + inline int mechanisms() const + { return m_mechanisms; } + + /** + * Check if a given mechanism is allowed + * @return True if the given mechanism is allowed + */ + inline bool mechanism(int mech) const + { return 0 != (m_mechanisms & mech); } + + /** + * Build an xml element from this feature + * @param addReq True to add the required/optional child + * @return XmlElement pointer or 0 + */ + virtual XmlElement* build(bool addReq = true); + +private: + int m_mechanisms; // Authentication mechanisms +}; + + +/** + * This class holds a list of JID features + * @short JID feature list + */ +class YJABBER_API XMPPFeatureList : public ObjList +{ + YCLASS(XMPPFeatureList,ObjList) +public: + /** + * Add a feature to the list + * @param xml XML element tag as enumeration + * @param feature The feature to add as enumeration + * @param required True if this feature is required + * @return False if the given feature already exists + */ + inline bool add(int xml, int feature, bool required = false) { + if (get(feature)) + return false; + append(new XMPPFeature(xml,feature,required)); + return true; + } + + /** + * Add a feature to the list + * @param feature The feature to add as enumeration + * @return False if the given feature already exists + */ + inline bool add(int feature) { + if (get(feature)) + return false; + append(new XMPPFeature(feature)); + return true; + } + + /** + * Add a feature to the list. Destroy the received parameter if already in the list + * @param feature The feature to add + * @return False if the given feature already exists + */ + inline bool add(XMPPFeature* feature) { + if (!feature || get(*feature)) { + TelEngine::destruct(feature); + return false; + } + append(feature); + return true; + } + + /** + * Clear data + */ + inline void reset() { + clear(); + m_identities.clear(); + m_entityCapsHash.clear(); + } + + /** + * Move a list of features to this list. Don't check duplicates + * @param list The source list + */ + void add(XMPPFeatureList& list); + + /** + * Re-build this list from stream features + * @param xml The features element to parse + */ + void fromStreamFeatures(XmlElement& xml); + + /** + * Re-build this list from disco info responses + * @param xml The element to parse + */ + void fromDiscoInfo(XmlElement& xml); + + /** + * Remove a feature from the list + * @param feature The feature to remove + */ + inline void remove(int feature) + { ObjList::remove(get(feature),true); } + + /** + * Get a feature from the list + * @param feature The feature to get + * @return Pointer to the feature or 0 if it doesn't exists + */ + XMPPFeature* get(int feature); + + /** + * Get a feature from the list + * @param feature The feature name to find + * @return Pointer to the feature or 0 if it doesn't exists + */ + inline XMPPFeature* get(const String& feature) { + ObjList* o = find(feature); + return o ? static_cast(o->get()) : 0; + } + + /** + * Build stream features from this list + * @return XmlElement pointer + */ + XmlElement* buildStreamFeatures(); + + /** + * Build an iq query disco info result from this list + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param node Optional 'node' attribute + * @param cap Optional capability to be set as 'node' suffix + * @return XmlElement pointer + */ + XmlElement* buildDiscoInfo(const char* from, const char* to, const char* id, + const char* node = 0, const char* cap = 0); + + /** + * Add this list to an xml element + * @param xml Destination element + */ + void add(XmlElement& xml); + + /** + * Update the entity capabilities hash + */ + void updateEntityCaps(); + + JIDIdentityList m_identities; + String m_entityCapsHash; // SHA-1 entity caps as defined in XEP 0115 +}; + + +/** + * This class is a general XMPP utilities + * @short General XMPP utilities + */ +class YJABBER_API XMPPUtils +{ +public: + /** + * Presence type enumeration + */ + enum Presence { + Probe, // probe + Subscribe, // subscribe request + Subscribed, // subscribe accepted + Unavailable, // unavailable + Unsubscribe, // unsubscribe request + Unsubscribed, // unsubscribe accepted + PresenceError, // error + PresenceNone + }; + + /** + * Message type enumeration + */ + enum MsgType { + Chat, // chat + GroupChat, // groupchat + HeadLine, // headline + Normal, // normal + MsgError, // error + MsgNone + }; + + /** + * Iq type enumeration + */ + enum IqType { + IqSet, // set + IqGet, // get + IqResult, // result + IqError, // error + IqCount + }; + + /** + * Command action enumeration + */ + enum CommandAction { + CommExecute, + CommCancel, + CommPrev, + CommNext, + CommComplete, + }; + + /** + * Command status enumeration + */ + enum CommandStatus { + CommExecuting, + CommCompleted, + CommCancelled, + }; + + /** + * Authentication methods + */ + enum AuthMethod { + AuthNone = 0x00, // No authentication mechanism + AuthSHA1 = 0x01, // SHA1 digest + AuthMD5 = 0x02, // MD5 digest + AuthPlain = 0x04, // Plain text password + AuthDialback = 0x08, // Dialback authentication + }; + + /** + * Check if an xml element has type 'result' or 'error' + * @param xml The element to check + * @return True if the element is a response one + */ + static inline bool isResponse(const XmlElement& xml) { + String* tmp = xml.getAttribute("type"); + return tmp && (*tmp == "result" || *tmp == "error"); + } + + /** + * Create an XML element + * @param name Element's name + * @param text Optional text for the element + * @return A valid XmlElement pointer + */ + static inline XmlElement* createElement(const char* name, const char* text = 0) { + XmlElement* xml = new XmlElement(String(name),true); + if (!TelEngine::null(text)) + xml->addText(text); + return xml; + } + + /** + * Create an XML element + * @param type Element's type + * @param text Optional text for the element + * @return A valid XmlElement pointer + */ + static inline XmlElement* createElement(int type, const char* text = 0) + { return createElement(s_tag[type],text); } + + /** + * Create an XML element with an 'xmlns' attribute + * @param name Element's name + * @param ns Optional 'xmlns' attribute as enumeration + * @param text Optional text for the element + * @return A valid XmlElement pointer + */ + static inline XmlElement* createElement(const char* name, int ns, + const char* text = 0) { + XmlElement* xml = createElement(name,text); + setXmlns(*xml,String::empty(),true,ns); + return xml; + } + + /** + * Create an XML element with an 'xmlns' attribute + * @param type Element's type + * @param ns 'xmlns' attribute as enumeration + * @param text Optional text for the element + * @return A valid XmlElement pointer + */ + static inline XmlElement* createElement(int type, int ns, const char* text = 0) + { return createElement(s_tag[type],ns,text); } + + /** + * Partially build an XML element from another one. + * Copy tag and 'to', 'from', 'type', 'id' attributes + * @param src Source element + * @param response True to reverse 'to' and 'from' attributes + * @param result True to set type to "result", false to set it to "error". + * Ignored if response is false + */ + static XmlElement* createElement(const XmlElement& src, bool response, bool result); + + /** + * Create an 'iq' element + * @param type Iq type as enumeration + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @return A valid XmlElement pointer + */ + static XmlElement* createIq(IqType type, const char* from = 0, + const char* to = 0, const char* id = 0); + + /** + * Create an 'iq' result element + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param child Optional element child (will be consumed) + * @return A valid XmlElement pointer + */ + static inline XmlElement* createIqResult(const char* from, const char* to, + const char* id, XmlElement* child = 0) { + XmlElement* xml = createIq(IqResult,from,to,id); + if (child) + xml->addChild(child); + return xml; + } + + /** + * Create an 'iq' error from a received element. Consume the received element. + * Add the given element to the error stanza if the 'id' attribute is missing + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param xml Received element + * @param type Error type + * @param error The error + * @param text Optional text to add to the error element + * @return A valid XmlElement pointer or 0 if xml + */ + static XmlElement* createIqError(const char* from, const char* to, XmlElement*& xml, + int type, int error, const char* text = 0); + + /** + * Create an 'iq' element with a 'vcard' child + * @param get True to set the iq's type to 'get', false to set it to 'set' + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @return A valid XmlElement pointer + */ + static XmlElement* createVCard(bool get, const char* from, const char* to, const char* id); + + /** + * Create a 'command' element + * @param action The command action + * @param node The command + * @param sessionId Optional session ID for the command + * @return A valid XmlElement pointer + */ + static XmlElement* createCommand(CommandAction action, const char* node, + const char* sessionId = 0); + + /** + * Create a disco info/items 'iq' element with a 'query' child + * @param info True to create a query info request. False to create a query items request + * @param req True to create a request (type=get), false to create a response (type=result) + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param node Optional 'node' attribute + * @param cap Optional capability to be set as 'node' suffix + * @return A valid XmlElement pointer + */ + static XmlElement* createIqDisco(bool info, bool req, const char* from, const char* to, + const char* id, const char* node = 0, const char* cap = 0); + + /** + * Create a version 'iq' result as defined in XEP-0092 + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param name Program name + * @param version Program version + * @param os Optional operating system + * @return A valid XmlElement pointer + */ + static XmlElement* createIqVersionRes(const char* from, const char* to, + const char* id, const char* name, const char* version, const char* os = 0); + + /** + * Create a 'error' element + * @param type Error type + * @param error The error + * @param text Optional text to add to the error element + * @return A valid XmlElement pointer + */ + static XmlElement* createError(int type, int error, const char* text = 0); + + /** + * Create an error from a received element. Consume the received element. + * Reverse 'to' and 'from' attributes + * @param xml Received element + * @param type Error type + * @param error The error + * @param text Optional text to add to the error element + * @return A valid XmlElement pointer or 0 if xml is 0 + */ + static XmlElement* createError(XmlElement* xml, int type, int error, + const char* text = 0); + + /** + * Create a 'stream:error' element + * @param error The XMPP defined condition + * @param text Optional text to add to the error + * @return A valid XmlElement pointer + */ + static XmlElement* createStreamError(int error, const char* text = 0); + + /** + * Build a register query element + * @param type Iq type as enumeration + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param child1 Optional child of query element + * @param child2 Optional child of query element + * @param child3 Optional child of query element + * @return Valid XmlElement pointer + */ + static XmlElement* createRegisterQuery(IqType type, const char* from, + const char* to, const char* id, + XmlElement* child1 = 0, XmlElement* child2 = 0, XmlElement* child3 = 0); + + /** + * Build an register query element used to create/set username/password + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param username The username + * @param password The password + * @return Valid XmlElement pointer + */ + static inline XmlElement* createRegisterQuery(const char* from, + const char* to, const char* id, + const char* username, const char* password) { + return createRegisterQuery(XMPPUtils::IqSet,from,to,id, + createElement(XmlTag::Username,username), + createElement(XmlTag::Password,password)); + } + + /** + * Create a failure element + * @param ns Element namespace + * @param error Optional error + * @return XmlElement pointer + */ + static inline XmlElement* createFailure(XMPPNamespace::Type ns, + XMPPError::Type error = XMPPError::NoError) { + XmlElement* xml = createElement(XmlTag::Failure,ns); + if (error != XMPPError::NoError) + xml->addChild(new XmlElement(s_error[error])); + return xml; + } + + /** + * Create an 'x' jabber:x:oob url element as described in XEP-0066 + * @param url The URL + * @param desc Optional description + * @return XmlElement pointer + */ + static inline XmlElement* createXOobUrl(const char* url, const char* desc = 0) { + XmlElement* xml = createElement("x",XMPPNamespace::XOob); + xml->addChild(createElement("url",url)); + if (desc) + xml->addChild(createElement("desc",desc)); + return xml; + } + + /** + * Create a 'delay' element as defined in XEP-0203 + * @param timeSec The time to encode (in seconds) + * @param from Optional 'from' attribute + * @param fractions Optional second fractions + * @param text Optional xml element text + * @return XmlElement pointer + */ + static XmlElement* createDelay(unsigned int timeSec, const char* from = 0, + unsigned int fractions = 0, const char* text = 0); + + /** + * Check if an element has a child with 'remove' tag + * @param xml The element to check + * @return True if the element has a child with 'remove' tag + */ + static inline bool remove(XmlElement& xml) + { return 0 != findFirstChild(xml,XmlTag::Remove); } + + /** + * Check if an element has a child with 'required' tag + * @param xml The element to check + * @return True if the element has a child with 'required' tag + */ + static inline bool required(XmlElement& xml) + { return 0 != findFirstChild(xml,XmlTag::Required); } + + /** + * Check if an element has a child with 'priority' tag + * @param xml The element to check + * @param defVal Default value to return if not found or invalid integer + * @return Element priority + */ + static int priority(XmlElement& xml, int defVal = 0); + + /** + * Add a 'priority' child to an element + * @param xml The element to set + * @param prio Priority text + */ + static inline void setPriority(XmlElement& xml, const char* prio) + { xml.addChild(createElement(XmlTag::Priority,prio)); } + + /** + * Get an element's namespace + * @param xml Element + * @return Element namespace as enumeration + */ + static inline int xmlns(XmlElement& xml) { + String* x = xml.xmlns(); + return x ? s_ns[*x] : XMPPNamespace::Count; + } + + /** + * Check if the given element has a given default namespace + * @param xml Element to check + * @param ns Namespace value to check + * @return True if the given element has the requested default namespace + */ + static inline bool hasDefaultXmlns(const XmlElement& xml, int ns) { + String* s = xml.xmlnsAttribute(XmlElement::s_ns); + return s && *s == s_ns[ns]; + } + + /** + * Check if the given element has a given namespace + * @param xml Element to check + * @param ns Namespace value to check + * @return True if the given element is in the requested namespace + */ + static inline bool hasXmlns(const XmlElement& xml, int ns) + { return xml.hasXmlns(s_ns[ns]); } + + /** + * Set an element's namespace + * @param xml Element + * @param name Namespace attribute name + * @param addAttr True to add the namespace attribute value + * @param ns Namespace value as enumeration + * @return True on success + */ + static inline bool setXmlns(XmlElement& xml, const String& name = String::empty(), + bool addAttr = false, int ns = XMPPNamespace::Count) { + if (ns < XMPPNamespace::Count) + return xml.setXmlns(name,addAttr,s_ns[ns]); + return xml.setXmlns(name); + } + + /** + * Set the 'stream' namespace to an element + * @param xml Element + * @return True on success + */ + static inline bool setStreamXmlns(XmlElement& xml) + { return setXmlns(xml,"stream",true,XMPPNamespace::Stream); } + + /** + * Set the 'db' namespace to an element + * @param xml Element + * @return True on success + */ + static inline bool setDbXmlns(XmlElement& xml) + { return setXmlns(xml,"db",true,XMPPNamespace::Dialback); } + + /** + * Find an element's first child element in a given namespace + * @param xml Element + * @param t Optional element tag as enumeration + * @param ns Optional element namespace as enumeration + * @return XmlElement pointer or 0 if not found + */ + static XmlElement* findFirstChild(const XmlElement& xml, int t = XmlTag::Count, + int ns = XMPPNamespace::Count); + + /** + * Find an element's next child element + * @param xml Element + * @param start Starting child + * @param t Optional element tag as enumeration + * @param ns Optional element namespace as enumeration + * @return XmlElement pointer or 0 if not found + */ + static XmlElement* findNextChild(const XmlElement& xml, XmlElement* start, + int t = XmlTag::Count, int ns = XMPPNamespace::Count); + + /** + * Decode a stream error or stanza error + * @param xml The element + * @param error The error condition + * @param text The stanza's error or error text + */ + static void decodeError(XmlElement* xml, String& error, String& text); + + /** + * Decode a stream or stanza error condition element + * @param xml The element + * @param stanza True if the condition must be a stanza error, false to + * decode a stream error + * @param error The error condition + * @param text The stanza's error or error text + */ + static void decodeError(XmlElement* xml, bool stanza, String& error, String& text); + + /** + * Encode EPOCH time given in seconds to a date/time profile as defined in + * XEP-0082 and XML Schema Part 2: Datatypes Second Edition + * @param buf Destination string + * @param timeSec The time to encode (in seconds) + * @param fractions Optional second fractions + */ + static void encodeDateTimeSec(String& buf, unsigned int timeSec, + unsigned int fractions = 0); + + /** + * Decode a date/time profile as defined in XEP-0082 + * and XML Schema Part 2: Datatypes Second Edition to EPOCH time + * @param time The date/time string + * @param fractions Pointer to integer to be filled with second fractions, if present + * @return The decoded time in seconds, -1 on error + */ + static unsigned int decodeDateTimeSec(const String& time, unsigned int* fractions = 0); + + /** + * Print an XmlElement to a string + * @param xmlStr The destination string + * @param xml The xml to print + * @param verbose True to print XML data on multiple lines + */ + static void print(String& xmlStr, XmlChild& xml, bool verbose); + + /** + * Put an element's name, text and attributes to a list of parameters + * @param xml The element + * @param dest Destination list + * @param prefix Prefix to add to parameters + */ + static void toList(XmlElement& xml, NamedList& dest, const char* prefix); + + /** + * Split a string at a delimiter character and fills a named list with its parts + * Skip empty parts + * @param dest The destination NamedList + * @param src Pointer to the string + * @param sep The delimiter + * @param nameFirst True to add the parts as name and index as value. + * False to do the other way + */ + static bool split(NamedList& dest, const char* src, const char sep, + bool nameFirst); + + /** + * Decode a comma separated list of flags and put them into an integer mask + * @param src Source string + * @param dict Dictionary containing flag names and values + * @return The mask of found flags + */ + static int decodeFlags(const String& src, const TokenDict* dict); + + /** + * Encode a mask of flags to a comma separated list of names + * @param dest Destination string + * @param src Source mask + * @param dict Dictionary containing flag names and values + */ + static void buildFlags(String& dest, int src, const TokenDict* dict); + + /** + * Add child elements from a list to a destination element + * @param dest Destination XmlElement + * @param list A list containing XML elements + * @return True if at least one child was added + */ + static bool addChidren(XmlElement* dest, ObjList& list); + + /** + * Create a 'c' entity capability element as defined in XEP 0115 + * @param hash The 'ver' attribute + * @param node The 'node' attribute + * @return XmlElement pointer or 0 on failure + */ + static XmlElement* createEntityCaps(const String& hash, const char* node); + + /** + * Create a 'c' entity capability element as defined by GTalk + * @return A valid XmlElement pointer + */ + static XmlElement* createEntityCapsGTalkV1(); + + /** + * Create an 'presence' element + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param type Presence type as enumeration + * @return A valid XmlElement pointer + */ + static XmlElement* createPresence(const char* from, + const char* to, Presence type = PresenceNone); + + /** + * Create a 'message' element + * @param type Message type string + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param body The message body + * @return A valid XmlElement pointer + */ + static XmlElement* createMessage(const char* type, const char* from, + const char* to, const char* id, const char* body); + + /** + * Create a 'message' element + * @param type Message type as enumeration + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param body The message body + * @return A valid XmlElement pointer + */ + static inline XmlElement* createMessage(MsgType type, const char* from, + const char* to, const char* id, const char* body) + { return createMessage(msgText(type),from,to,id,body); } + + /** + * Build a dialback 'db:result' xml element used to send a dialback key + * @param from The sender + * @param to The recipient + * @param key The dialback key + * @return XmlElement pointer + */ + static XmlElement* createDialbackKey(const char* from, const char* to, + const char* key); + + /** + * Build a dialback 'db:result' xml element used to send a dialback key response + * @param from The sender + * @param to The recipient + * @param valid True if valid, false if invalid + * @return XmlElement pointer + */ + static XmlElement* createDialbackResult(const char* from, const char* to, + bool valid); + + /** + * Build a dialback 'db:verify' xml element + * @param from The sender + * @param to The recipient + * @param id The 'id' attribute (stream id) + * @param key The dialback key + * @return XmlElement pointer + */ + static XmlElement* createDialbackVerify(const char* from, const char* to, + const char* id, const char* key); + + /** + * Build a dialback 'db:verify' response xml element + * @param from The sender + * @param to The recipient + * @param id The 'id' attribute (stream id) + * @param valid True if valid, false if invalid + * @return XmlElement pointer + */ + static XmlElement* createDialbackVerifyRsp(const char* from, const char* to, + const char* id, bool valid); + + /** + * Build a 'subject' xml element + * @param subject Element text + * @return XmlElement pointer + */ + static inline XmlElement* createSubject(const char* subject) + { return createElement(XmlTag::Subject,subject); } + + /** + * Get an element's subject (the text of the first 'subject' child) + * @param xml The element + * @return Element subject or an empty string + */ + static inline const String& subject(XmlElement& xml) { + XmlElement* s = findFirstChild(xml,XmlTag::Subject); + return s ? s->getText() : String::empty(); + } + + /** + * Build a 'body' xml element + * @param body Element text + * @param ns Optional namespace + * @return XmlElement pointer + */ + static inline XmlElement* createBody(const char* body, int ns = XMPPNamespace::Count) + { return createElement(XmlTag::Body,ns,body); } + + /** + * Retrieve the text of an element's body child + * @param xml The element + * @param ns Optional body namespace to match (default: match parent's namespace) + * @return Body or empty string + */ + static const String& body(XmlElement& xml, int ns = XMPPNamespace::Count); + + /** + * Build a name/value parameter xml element + * @param name The 'name' attribute + * @param value The value parameter + * @param tag Optional element tag (defaults to 'parameter') + * @return XmlElement pointer + */ + static inline XmlElement* createParameter(const char* name, const char* value, + const char* tag = "parameter") { + XmlElement* tmp = new XmlElement(tag); + tmp->setAttributeValid("name",name); + tmp->setAttributeValid("value",value); + return tmp; + } + + /** + * Build a name/value parameter xml element + * @param pair The name/value pair + * @param tag Optional element tag (defaults to 'parameter') + * @return XmlElement pointer + */ + static inline XmlElement* createParameter(const NamedString& pair, + const char* tag = "parameter") + { return createParameter(pair.name(),pair,tag); } + + /** + * Get an element's namespace + * @param xml The element + * @return The namespace integer value as XMPPNamespace value + */ + static inline int ns(const XmlElement& xml) { + String* xmlns = xml.xmlns(); + return xmlns ? s_ns[*xmlns] : XMPPNamespace::Count; + } + + /** + * Get an XML tag enumeration value associated with an element's tag + * @param xml The element to check + * @return Xml tag as enumeration + */ + static inline int tag(const XmlElement& xml) + { return s_tag[xml.getTag()]; } + + /** + * Get an XML element's tag and namespace + * @param xml The element to check + * @param tag Element tag as enumeration + * @param ns Element namespace as enumeration + * @return True if data was succesfully retrieved + */ + static inline bool getTag(const XmlElement& xml, int& tag, int& ns) { + const String* t = 0; + const String* n = 0; + if (!xml.getTag(t,n)) + return false; + tag = s_tag[*t]; + ns = n ? s_ns[*n] : XMPPNamespace::Count; + return tag != XmlTag::Count; + } + + /** + * Check if an xml element has a given tag (without prefix) and namespace + * @param xml The element to check + * @param tag Tag to check + * @param ns Namespace to check + * @return True if the element has the requested tag and namespace + */ + static inline bool isTag(const XmlElement& xml, int tag, int ns) { + int t,n; + return getTag(xml,t,n) && tag == t && n == ns; + } + + /** + * Check if an xml element has a given tag (without prefix) + * @param xml The element to check + * @param tag Tag to check + * @return True if the element has the requested tag + */ + static inline bool isUnprefTag(const XmlElement& xml, int tag) + { return xml.unprefixedTag() == s_tag[tag]; } + + /** + * Check if a given element is a stanza one ('iq', 'message' or 'presence') + * @param xml The element to check + * @return True if the element is a stanza + */ + static inline bool isStanza(const XmlElement& xml) { + int t,n; + return getTag(xml,t,n) && + (t == XmlTag::Iq || t == XmlTag::Presence || t == XmlTag::Message); + } + + /** + * Retrieve an xml element from a NamedPointer. + * Release NamedPointer ownership if found + * @param gen The object to be processed + * @return XmlElement pointer or 0 + */ + static XmlElement* getXml(GenObject* gen); + + /** + * Parse a string to an XmlElement + * @param data XML data to parse + * @return XmlElement pointer or 0 if the string is an invalid xml or contains more + * then one element + */ + static XmlElement* getXml(const String& data); + + /** + * Retrieve an xml element from a list parameter. + * Clear the given parameter from list if an XmlElement is found + * Try to build (parse) from an extra parameter if not found + * @param list The list of parameters + * @param param The name of the parameter with the xml element + * @param extra Optional parameter containing xml string data + * @return XmlElement pointer or 0 + */ + static XmlElement* getXml(NamedList& list, const char* param = "xml", + const char* extra = "data"); + + /** + * Retrieve a presence xml element from a list parameter. + * Clear the given parameter from list if an XmlElement is found. + * Try to build (parse) from an extra parameter if not found. + * Build a presence stanza from parameters if an element is not found + * @param list The list of parameters + * @param param The name of the parameter with the xml element + * @param extra Optional parameter containing xml string data + * @param type Presence type to build + * @param build True to build a message stanza if an element is not found + * @return XmlElement pointer or 0 + */ + static XmlElement* getPresenceXml(NamedList& list, const char* param = "xml", + const char* extra = "data", Presence type = PresenceNone, bool build = true); + + /** + * Retrieve a chat (message) xml element from a list parameter. + * Clear the given parameter from list if an XmlElement is found. + * Try to build (parse) from an extra parameter if not found. + * Build a message stanza from parameters if an element is not found + * @param list The list of parameters + * @param param The name of the parameter with the xml element + * @param extra Optional parameter containing xml string data + * @param build True to build a message stanza if an element is not found + * @return XmlElement pointer or 0 + */ + static XmlElement* getChatXml(NamedList& list, const char* param = "xml", + const char* extra = "data", bool build = true); + + /** + * Get the type of a 'presence' stanza as enumeration + * @param text The text to check + * @return Presence type as enumeration + */ + static inline Presence presenceType(const char* text) + { return (Presence)lookup(text,s_presence,PresenceNone); } + + /** + * Get the text from a presence type + * @param presence The presence type + * @return The associated text or 0 + */ + static inline const char* presenceText(Presence presence) + { return lookup(presence,s_presence,0); } + + /** + * Get the type of a 'message' stanza + * @param text The text to check + * @return Message type as enumeration + */ + static inline MsgType msgType(const char* text) + { return (MsgType)lookup(text,s_msg,TelEngine::null(text) ? Normal : MsgNone); } + + /** + * Get the text from a message type + * @param msg The message type + * @return The associated text or 0 + */ + static inline const char* msgText(MsgType msg) + { return lookup(msg,s_msg,0); } + + /** + * Get the type of an 'iq' stanza as enumeration + * @param text The text to check + * @return Iq type as enumeration + */ + static inline IqType iqType(const char* text) + { return (IqType)lookup(text,s_iq,IqCount); } + + /** + * Get the authentication method associated with a given text + * @param text The text to check + * @param defVal Default value to return if not found + * @return Authentication method + */ + static inline int authMeth(const char* text, int defVal = AuthNone) + { return lookup(text,s_authMeth,defVal); } + + /** + * Namespaces + */ + static XMPPNamespace s_ns; + + /** + * Errors + */ + static XMPPError s_error; + + /** + * XML tags + */ + static XmlTag s_tag; + + /** + * Keep the types of 'presence' stanzas + */ + static const TokenDict s_presence[]; + + /** + * Keep the types of 'message' stanzas + */ + static const TokenDict s_msg[]; + + /** + * Keep the types of 'iq' stanzas + */ + static const TokenDict s_iq[]; + + /** + * Keep the command actions + */ + static const TokenDict s_commandAction[]; + + /** + * Keep the command status + */ + static const TokenDict s_commandStatus[]; + + /** + * Authentication methods names + */ + static const TokenDict s_authMeth[]; +}; + +/** + * This class holds a direction flags (such as subscription states) + * @short Direction flags + */ +class YJABBER_API XMPPDirVal +{ +public: + /** + * Direction flags enumeration + */ + enum Direction { + None = 0x00, + To = 0x01, + From = 0x02, + PendingIn = 0x10, + PendingOut = 0x20, + // Masks + Both = 0x03, + Pending = 0x30 + }; + + /** + * Constructor + * @param flags Flag(s) to set + */ + inline XMPPDirVal(int flags = None) + : m_value(flags) + {} + + /** + * Constructor + * @param flags Comma separated list of flags + */ + inline XMPPDirVal(const String& flags) + : m_value(0) + { replace(flags); } + + /** + * Replace all flags + * @param flag The new value of the flags + */ + inline void replace(int flag) + { m_value = flag; } + + /** + * Replace all flags from a list + * @param flags Comma separated list of flags + */ + inline void replace(const String& flags) + { m_value = XMPPUtils::decodeFlags(flags,s_names); } + + /** + * Build a string representation of this object + * @param buf Destination string + * @param full True to add all flags, false to ignore pending flags + */ + void toString(String& buf, bool full) const; + + /** + * Build a subscription state string representation of this object + * @param buf Destination string + */ + void toSubscription(String& buf) const; + + /** + * Set one or more flags + * @param flag Flag(s) to set + */ + inline void set(int flag) + { m_value |= flag; } + + /** + * Reset one or more flags + * @param flag Flag(s) to reset + */ + inline void reset(int flag) + { m_value &= ~flag; } + + /** + * Check if a given bit mask is set + * @param mask Bit mask to check + * @return True if the given bit mask is set + */ + inline bool test(int mask) const + { return (m_value & mask) != 0; } + + /** + * Check if the 'To' flag is set + * @return True if the 'To' flag is set + */ + inline bool to() const + { return test(To); } + + /** + * Check if the 'From' flag is set + * @return True if the 'From' flag is set + */ + inline bool from() const + { return test(From); } + + /** + * Cast operator + */ + inline operator int() + { return m_value; } + + /** + * Keep the flag names + */ + static const TokenDict s_names[]; + +private: + int m_value; // The value +}; + +/** + * This class holds an XML element to be sent through a stream + * @short An outgoing XML element + */ +class YJABBER_API XmlElementOut : public RefObject +{ +public: + /** + * Constructor + * @param element The XML element + * @param senderID Optional sender id + * @param unclose True to not close the tag when building the buffer + */ + inline XmlElementOut(XmlElement* element, const char* senderID = 0, + bool unclose = false) + : m_element(element), m_offset(0), m_id(senderID), m_unclose(unclose), + m_sent(false) + {} + + /** + * Destructor + * Delete m_element if not 0 + */ + virtual ~XmlElementOut() + { TelEngine::destruct(m_element); } + + /** + * Get the underlying element + * @return The underlying element + */ + inline XmlElement* element() const + { return m_element; } + + /** + * Check if this element was (partially) sent + * @return True if an attempt to send this element was already done + */ + inline bool sent() const + { return m_sent; } + + /** + * Get the data buffer + * @return The data buffer + */ + inline const String& buffer() + { return m_buffer; } + + /** + * Get the id member + * @return The id member + */ + inline const String& id() const + { return m_id; } + + /** + * Get the remainig byte count to send + * @return The unsent number of bytes + */ + inline unsigned int dataCount() + { return m_buffer.length() - m_offset; } + + /** + * Get the remainig data to send. Set the buffer if not already set + * @param nCount The number of unsent bytes + * @return Pointer to the remaining data or 0 + */ + inline const char* getData(unsigned int& nCount) { + if (!m_buffer) + prepareToSend(); + nCount = dataCount(); + return m_buffer.c_str() + m_offset; + } + + /** + * Increase the offset with nCount bytes. Set the sent flag + * @param nCount The number of bytes sent + */ + inline void dataSent(unsigned int nCount) { + m_sent = true; + m_offset += nCount; + if (m_offset > m_buffer.length()) + m_offset = m_buffer.length(); + } + + /** + * Release the ownership of m_element + * The caller is responsable of returned pointer + * @return XmlElement pointer or 0 + */ + inline XmlElement* release() { + XmlElement* e = m_element; + m_element = 0; + return e; + } + + /** + * Fill a buffer with the XML element to send + * @param buffer The buffer to fill + */ + inline void toBuffer(String& buffer) { + if (m_element) + m_element->toString(buffer,true,String::empty(),String::empty(),!m_unclose); + } + + /** + * Fill the buffer with the XML element to send + */ + inline void prepareToSend() + { toBuffer(m_buffer); } + +private: + XmlElement* m_element; // The XML element + String m_buffer; // Data to send + unsigned int m_offset; // Offset to send + String m_id; // Sender's id + bool m_unclose; // Close or not the element's tag + bool m_sent; // Sent flag (true if an attempt to send was done) +}; + +}; + +#endif /* __XMPPUTILS_H */ + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjabber/yatejabber.h b/libs/yjabber/yatejabber.h new file mode 100644 index 00000000..89b00874 --- /dev/null +++ b/libs/yjabber/yatejabber.h @@ -0,0 +1,2476 @@ +/** + * yatejabber.h + * Yet Another Jabber Stack + * This file is part of the YATE Project http://YATE.null.ro + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __YATEJABBER_H +#define __YATEJABBER_H + +#include + +/** + * Holds all Telephony Engine related classes. + */ +namespace TelEngine { + +class JBEvent; // A Jabber event +class JBStream; // A Jabber stream +class JBClientStream; // A client to server stream +class JBServerStream; // A server to server stream +class JBRemoteDomainDef; // Options and connect settings for a remote domain +class JBConnect; // A socket connector +class JBEngine; // A Jabber engine +class JBServerEngine; // A Jabber server engine +class JBClientEngine; // A Jabber client engine +class JBStreamSet; // A set of streams to be processed in an uniform way +class JBStreamSetProcessor; // Specialized stream processor +class JBStreamSetReceive; // Specialized stream data receiver +class JBStreamSetList; // A list of stream sets +class JBEntityCaps; // Entity capability +class JBEntityCapsList; // Entity capability list manager + + +/** + * Default port for client to server connections + */ +#define XMPP_C2S_PORT 5222 + +/** + * Default port for server to server connections + */ +#define XMPP_S2S_PORT 5269 + +/** + * Default value for maximum length of an incomplete xml allowed in a stream + * parser's buffer + */ +#define XMPP_MAX_INCOMPLETEXML 8192 + + +/** + * This class handles PLAIN (rfc 4616) and DIGEST (rfc 2831) SASL authentication + * @short SASL authentication mechanism + */ +class YJABBER_API SASL : public GenObject +{ + YCLASS(SASL,GenObject) +public: + /** + * Constructor + * @param plain True to build a plain password auth object + * @param realm Optional server realm + */ + SASL(bool plain, const char* realm = 0); + + /** + * Destructor + */ + ~SASL() + { TelEngine::destruct(m_params); } + + /** + * Set auth params + * @param user Optional username + * @param pwd Optional password + */ + void setAuthParams(const char* user = 0, const char* pwd = 0); + + /** + * Build a client initial auth or challenge response + * @param buf Destination buffer. It will be filled with Base64 encoded result + * @param digestUri Digest MD5 URI + * @return True on success + */ + bool buildAuthRsp(String& buf, const char* digestUri = 0); + + /** + * Build a server reply to challenge response + * @param buf Destination buffer. It will be filled with Base64 encoded result + * @param rsp The response + */ + inline void buildAuthRspReply(String& buf, const String& rsp) { + if (m_plain) + return; + String tmp("rspauth=" + rsp); + Base64 b((void*)tmp.c_str(),tmp.length(),false); + b.encode(buf); + b.clear(false); + } + + /** + * Check if a challenge response reply is valid + * @param reply The reply to check + * @return True if valid + */ + inline bool validAuthReply(const String& reply) { + String tmp; + if (m_params) + buildMD5Digest(tmp,m_params->getValue("password"),false); + return tmp == reply; + } + + /** + * Build an MD5 challenge from this object. + * Generate a new nonce and increase nonce count + * @param buf Destination buffer + * @return True on success + */ + bool buildMD5Challenge(String& buf); + + /** + * Build a Digest MD5 SASL (RFC 2831) to be sent with authentication responses + * @param dest Destination string + * @param password The password to use + * @param challengeRsp True if building a Digest MD5 challenge response, false if + * building a challenge response reply + */ + inline void buildMD5Digest(String& dest, const char* password, + bool challengeRsp = true) { + if (m_params) + buildMD5Digest(dest,*m_params,password,challengeRsp); + } + + /** + * Parse plain password auth data + * @param buf The buffer to parse + * @return True if succesfully parsed + */ + bool parsePlain(const DataBlock& buf); + + /** + * Parse and decode a buffer containing a SASL Digest MD5 challenge. + * @param buf Already checked for valid UTF8 characters input string + * @return True on success + */ + bool parseMD5Challenge(const String& buf); + + /** + * Parse and decode a buffer containing a SASL Digest MD5 response. + * Check realm, nonce and nonce count + * @param buf Already checked for valid UTF8 characters input string + * @return True on success + */ + bool parseMD5ChallengeRsp(const String& buf); + + /** + * Parse and decode a buffer containing SASL plain authentication data + * as defined in RFC 4616 + * @param buf Input buffer + * @param user Destination buffer for username part + * @param pwd Destination buffer for password part + * @param authzid Optional destination buffer for authorization identity part + * @return True on success + */ + static bool parsePlain(const DataBlock& buf, String& user, String& pwd, + String* authzid = 0); + + /** + * Build a Digest MD5 SASL (RFC 2831) to be sent with authentication responses + * @param dest Destination string + * @param params List of parameters + * @param password The password to use + * @param challengeRsp True if building a Digest MD5 challenge response, false if + * building a challenge response reply + */ + static void buildMD5Digest(String& dest, const NamedList& params, + const char* password, bool challengeRsp = true); + + bool m_plain; + NamedList* m_params; + String m_realm; + String m_nonce; + String m_cnonce; + unsigned int m_nonceCount; + +private: + SASL() {} +}; + + +/** + * This class holds a Jabber stream event. Stream events are raised by streams + * and sent by the engine to the proper service + * @short A Jabber stream event + */ +class YJABBER_API JBEvent : public RefObject +{ + YCLASS(JBEvent,RefObject) + friend class JBStream; + friend class JBClientStream; + friend class JBServerStream; +public: + /** + * Event type enumeration + */ + enum Type { + // Stream terminated. Try to connect or wait to be destroyed + Terminated, + // Stream is destroying + Destroy, + // Stream start was received: when processing this event, the upper + // layer must call stream's start() method or terminate the stream + Start, + // Incoming stream need auth: when processing this event, the upper + // layer must call stream's authenticated() method + Auth, + // The event's element is an 'iq' with a child qualified by bind namespace + // This event is generated by an incoming client stream without a bound resource + Bind, + // Stream is running (can send/recv stanzas) + Running, + // The event's element is a 'message' + Message, + // The event's element is a 'presence' + Presence, + // The event's element is an 'iq' + Iq, + // The event's element is a 'db:result' one received by a server-to-server stream + // containing the dialback key to verify + // The event's text is filled with dialback key to verify + DbResult, + // The event's element is a 'db:verify' one received by a server-to-server stream + DbVerify, + // New user register or user password change succeeded + RegisterOk, + // New user register or user password change failed + // The event's element is the response + RegisterFailed, + // Non stanza element received in Running state + Unknown + }; + + /** + * Constructor. Constructs an event from a stream + * @param type Type of this event + * @param stream The stream that generated the event + * @param element Element that generated the event + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @param child Optional type depending element's child + */ + inline JBEvent(Type type, JBStream* stream, XmlElement* element, + const JabberID& from, const JabberID& to, XmlElement* child = 0) + : m_type(type), m_stream(0), m_link(true), m_element(element), + m_child(child) + { init(stream,element,&from,&to); } + + /** + * Constructor. Constructs an event from a stream + * @param type Type of this event + * @param stream The stream that generated the event + * @param element Element that generated the event + * @param child Optional type depending element's child + */ + inline JBEvent(Type type, JBStream* stream, XmlElement* element, + XmlElement* child = 0) + : m_type(type), m_stream(0), m_link(true), m_element(element), + m_child(child) + { init(stream,element); } + + /** + * Destructor. Delete the XML element if valid + */ + virtual ~JBEvent(); + + /** + * Get the event type + * @return The type of this event as enumeration + */ + inline int type() const + { return m_type; } + + /** + * Get the event name + * @return The name of this event + */ + inline const char* name() const + { return lookup(type()); } + + /** + * Get the element's 'type' attribute if any + * @return The element's 'type' attribute + */ + inline const String& stanzaType() const + { return m_stanzaType; } + + /** + * Get the 'from' attribute of a received stanza + * @return The 'from' attribute + */ + inline const JabberID& from() const + { return m_from; } + + /** + * Get the 'to' attribute of a received stanza + * @return The 'to' attribute + */ + inline const JabberID& to() const + { return m_to; } + + /** + * Get the sender's id for Write... events or the 'id' attribute if the + * event carries a received stanza + * @return The event id + */ + inline const String& id() const + { return m_id; } + + /** + * The stanza's text or termination reason for Terminated/Destroy events + * @return The event's text + */ + inline const String& text() const + { return m_text; } + + /** + * Get the stream that generated this event + * @return The stream that generated this event + */ + inline JBStream* stream() const + { return m_stream; } + + /** + * Get a client-to-server stream from the event's stream + * @return JBClientStream pointer or 0 + */ + JBClientStream* clientStream(); + + /** + * Get a server-to-server stream from the event's stream + * @return JBServerStream pointer or 0 + */ + JBServerStream* serverStream(); + + /** + * Get the underlying XmlElement + * @return XmlElement pointer or 0 + */ + inline XmlElement* element() const + { return m_element; } + + /** + * Get the first child of the underlying element if any + * @return XmlElement pointer or 0 + */ + inline XmlElement* child() const + { return m_child; } + + /** + * Delete the underlying XmlElement(s). Release the ownership. + * The caller will own the returned pointer + * @param del True to delete all xml elements owned by this event + * @return XmlElement pointer if not deleted or 0 + */ + XmlElement* releaseXml(bool del = false); + + /** + * Build an 'iq' result stanza from event data + * @param addTags True to add the 'from' and 'to' attributes + * @param child Optional 'iq' child (will be consumed) + * @return True on success + */ + XmlElement* buildIqResult(bool addTags, XmlElement* child = 0); + + /** + * Build and send a stanza 'result' from enclosed 'iq' element + * Release the element on success + * @param child Optional 'iq' child (will be consumed) + * @return True on success + */ + bool sendIqResult(XmlElement* child = 0); + + /** + * Build an 'iq' error stanza from event data + * The event's element will be released and added to the error one + * if the id is empty + * @param addTags True to add the 'from' and 'to' attributes + * @param error Error to be returned to the event's XML sender + * @param reason Optional text to be attached to the error + * @param type Error type + * @return True on success + */ + XmlElement* buildIqError(bool addTags, XMPPError::Type error, const char* reason = 0, + XMPPError::ErrorType type = XMPPError::TypeModify); + + /** + * Build and send a stanza error from enclosed element + * Release the element on success + * @param error Error to be returned to the event's XML sender + * @param reason Optional text to be attached to the error + * @param type Error type + * @return True on success + */ + bool sendStanzaError(XMPPError::Type error, const char* reason = 0, + XMPPError::ErrorType type = XMPPError::TypeModify); + + /** + * Release the link with the stream to let the stream continue with events + * @param release True to release the reference to the stream + */ + void releaseStream(bool release = false); + + /** + * Get the name of an event type + * @return The name an event type + */ + inline static const char* lookup(int type) + { return TelEngine::lookup(type,s_type); } + +private: + static const TokenDict s_type[]; // Event names + JBEvent() {} // Don't use it! + bool init(JBStream* stream, XmlElement* element, + const JabberID* from = 0, const JabberID* to = 0); + + Type m_type; // Type of this event + JBStream* m_stream; // The stream that generated this event + bool m_link; // Stream link state + XmlElement* m_element; // Received XML element, if any + XmlElement* m_child; // The first child element for 'iq' elements + String m_stanzaType; // Stanza's 'type' attribute + JabberID m_from; // Stanza's 'from' attribute + JabberID m_to; // Stanza's 'to' attribute + String m_id; // 'id' attribute if the received element has one + String m_text; // The stanza's text or termination reason for + // Terminated/Destroy events +}; + + +/** + * Base class for all Jabber streams. Basic stream data processing: send/receive + * XML elements, keep stream state, generate events + * @short A Jabber stream + */ +class YJABBER_API JBStream : public RefObject, public DebugEnabler, public Mutex +{ + friend class JBEngine; + friend class JBEvent; +public: + /** + * Stream type enumeration + */ + enum Type { + c2s = 0, // Client to server + s2s = 1, // Server to server + TypeCount = 2 // Unknown + }; + + /** + * Stream state enumeration + */ + enum State { + Idle = 0, // Stream is waiting to be connected or destroyed + Connecting = 1, // Outgoing stream is waiting for the socket to connect + WaitStart = 2, // Waiting for remote's stream start + // (outgoing: stream start already sent) + Starting = 3, // Incoming stream is processing a stream start element + Features = 4, // Outgoing: waiting for stream features + // Incoming: stream features sent + WaitTlsRsp = 5, // 'starttls' sent: waiting for response + Securing = 10, // Stream is currently negotiating the TLS + Auth = 11, // Auth element (db:result for s2s streams) sent + Challenge = 12, // 'challenge' element sent/received + Register = 20, // A new user is currently registering + Running = 100, // Established. Allow XML stanzas to pass over the stream + Destroy, // Stream is destroying. No more traffic allowed + }; + + /** + * Stream behaviour options + */ + enum Flags { + NoAutoRestart = 0x00000001,// Don't restart stream when down + TlsRequired = 0x00000002,// TLS is mandatory on this stream + AllowPlainAuth = 0x00000004,// Allow plain password authentication + // If not allowed and this is the only method + // offered by server the stream will be terminated + DialbackOnly = 0x00000008,// Outgoing s2s dialback stream + RegisterUser = 0x00000010,// Outgoing c2s register new user + // Flags to be managed by the upper layer + RosterRequested = 0x00000100,// c2s: the roster was already requested + AvailableResource = 0x00000200,// c2s: available presence was sent/received + PositivePriority = 0x00000400,// c2s: the resource advertised by the client has priority >= 0 + // Internal flags (cleared when the stream is re-started) + StreamSecured = 0x00020000,// TLS stage was done (possible without using TLS) + StreamTls = 0x00040000,// The stream is using TLS + StreamAuthenticated = 0x00080000,// Stream already authenticated + StreamRemoteVer1 = 0x00100000,// Remote party advertised RFC3920 version=1.0 + StreamWaitBindRsp = 0x01000000,// Outgoing c2s waiting for bind response + StreamWaitSessRsp = 0x02000000,// Outgoing c2s waiting for session response + StreamWaitChallenge = 0x04000000,// Outgoing waiting for auth challenge + StreamWaitChgRsp = 0x08000000,// Outgoing waiting challenge response confirmation + StreamRfc3920Chg = 0x10000000,// Outgoing sent empty response to challenge with rspauth (RFC3920) + // Flag masks + StreamFlags = 0x000000ff, + InternalFlags = 0xffff0000, + }; + + /** + * Destructor. + * Gracefully close the stream and the socket + */ + virtual ~JBStream(); + + /** + * Get the type of this stream. See the protocol enumeration of the engine + * @return The type of this stream + */ + inline int type() const + { return m_type; } + + /** + * Retrieve this stream's default namespace + * @return The stream default namespace + */ + inline int xmlns() const + { return m_xmlns; } + + /** + * Get the stream state + * @return The stream state as enumeration. + */ + inline State state() const + { return m_state; } + + /** + * Get the stream direction + * @return True if the stream is an incoming one + */ + inline bool incoming() const + { return m_incoming; } + + /** + * Get the stream direction + * @return True if the stream is an outgoing one + */ + inline bool outgoing() const + { return !m_incoming; } + + /** + * Get the stream's owner + * @return Pointer to the engine owning this stream + */ + inline JBEngine* engine() const + { return m_engine; } + + /** + * Get the stream's name + * @return The stream's name + */ + inline const char* name() const + { return m_name; } + + /** + * Get the stream id + * @return The stream id + */ + inline const String& id() const + { return m_id; } + + /** + * Get the JID of the local side of this stream + * @return The JID of the local side of this stream + */ + inline const JabberID& local() const + { return m_local; } + + /** + * Set the local party's JID + * @param jid Local party's jid to set + */ + inline void setLocal(const char* jid) + { m_local.set(jid); } + + /** + * Get the JID of the remote side of this stream + * @return The JID of the remote side of this stream + */ + inline const JabberID& remote() const + { return m_remote; } + + /** + * Get the remote party's address + * This method is thread safe + * @param addr The socket address to be filled with remote party's address + * @return True on success + */ + inline bool remoteAddr(SocketAddr& addr) { + Lock lock(this); + return m_socket && m_socket->getPeerName(addr); + } + + /** + * Get the local address + * This method is thread safe + * @param addr The socket address to be filled with local address + * @return True on success + */ + inline bool localAddr(SocketAddr& addr) { + Lock lock(this); + return m_socket && m_socket->getSockName(addr); + } + + /** + * Get the stream flags + * @return Stream flags + */ + inline int flags() const + { return m_flags; } + + /** + * Check if a given option (or option mask) is set + * @param mask The flag(s) to check + * @return True if set + */ + inline bool flag(int mask) const + { return 0 != (m_flags & mask); } + + /** + * Set or reset the TLS required flag + * This method is not thread safe + * @param set True to set, false to reset the flag + */ + inline void setTlsRequired(bool set) { + if (set) + m_flags |= TlsRequired; + else + m_flags &= ~TlsRequired; + } + + /** + * Retrieve remote ip/port used to connect to + * This method is not thread safe + * @param addr The ip + * @param port The port + */ + inline void connectAddr(String& addr, int& port) const { + addr = m_connectAddr; + port = m_connectPort; + } + + /** + * Set/reset RosterRequested flag + * This method is thread safe + * @param ok True to set, false to reset it + */ + void setRosterRequested(bool ok); + + /** + * Set/reset AvailableResource/PositivePriority flags + * This method is thread safe + * @param ok True to set, false to reset it + * @param positive True if an available resource has positive priority + * @return True if changed + */ + bool setAvailableResource(bool ok, bool positive = true); + + /** + * Read data from socket. Send it to the parser. + * Terminate the stream on socket or parser error + * @param buf Destination buffer + * @param len Buffer length (must be greater then 1) + * @return True if data was received + */ + bool readSocket(char* buf, unsigned int len); + + /** + * Get a client stream from this one + * @return JBClientStream pointer or 0 + */ + virtual JBClientStream* clientStream() + { return 0; } + + /** + * Get a server stream from this one + * @return JBServerStream pointer or 0 + */ + virtual JBServerStream* serverStream() + { return 0; } + + /** + * Stream state processor. + * This method is thread safe + * @param time Current time + * @return JBEvent pointer or 0 + */ + JBEvent* getEvent(u_int64_t time = Time::msecNow()); + + /** + * Send a stanza ('iq', 'message' or 'presence') in Running state. + * This method is thread safe + * @param xml Element to send (will be consumed and zeroed) + * @return True on success + */ + bool sendStanza(XmlElement*& xml); + + /** + * Send stream related XML when negotiating the stream or some other + * stanza in non Running state + * All elements will be consumed + * This method is thread safe + * @param newState The new stream state to set on success + * @param first The first element to send + * @param second Optional second element to send + * @param third Optional third element to send + * @return True on success + */ + bool sendStreamXml(State newState, XmlElement* first, XmlElement* second = 0, + XmlElement* third = 0); + + /** + * Start the stream. This method should be called by the upper layer + * when processing an incoming stream Start event. For outgoing streams + * this method is called internally on succesfully connect. + * This method is thread safe + * @param features Optional features to advertise to the remote party of an + * incoming stream. The caller is responsable of freeing it. + * If processed, list's elements will be moved to stream's features list + * @param caps Optional entity capabilities to be added to the stream features. + * Ignored for outgoing streams + */ + void start(XMPPFeatureList* features = 0, XmlElement* caps = 0); + + /** + * Auth event result. This method should be called by the + * upper layer when processing an Auth event + * This method is thread safe + * @param ok True if the remote party was authenticated, + * false if authentication failed + * @param rsp Optional success response content. Ignored if not authenticated + * @param error Failure reason. Ignored if authenticated + * @return False if stream state is incorrect + */ + bool authenticated(bool ok, const String& rsp = String::empty(), + XMPPError::Type error = XMPPError::NotAuthorized); + + /** + * Terminate the stream. Send stream end tag or error. + * Reset the stream. Deref stream if destroying. + * This method is thread safe + * @param location The terminate request location: + * -1: upper layer, 0: internal, 1: remote + * @param destroy True to destroy. False to terminate + * @param xml Received XML element. The element will be consumed + * @param error Termination reason. Set it to NoError to send stream end tag + * @param reason Optional text to be added to the error stanza + * @param final True if called from destructor + */ + void terminate(int location, bool destroy, XmlElement* xml, + int error = XMPPError::NoError, const char* reason = "", + bool final = false); + + /** + * Outgoing stream connect terminated notification. + * Send stream start if everithing is ok + * @param sock The connected socket, will be consumed and zeroed + */ + virtual void connectTerminated(Socket*& sock); + + /** + * Get an object from this stream + * @param name The name of the object to get + */ + virtual void* getObject(const String& name) const; + + /** + * Get the name of a stream state + * @return The name of the stream state + */ + inline const char* stateName() + { return lookup(state(),s_stateName); } + + /** + * Get the name of a stream type + * @return The name of the stream type + */ + inline const char* typeName() + { return lookup(type(),s_typeName); } + + /** + * Get the string representation of this stream + * @return Stream name + */ + virtual const String& toString() const; + + /** + * Get the stream type associated with a given text + * @param text Stream type text to find + * @param defVal Value to return if not found + * @return The stream type associated with a given text + */ + static inline Type lookupType(const char* text, Type defVal = TypeCount) + { return (Type)lookup(text,s_typeName,defVal); } + + /** + * SASL authentication data + */ + SASL* m_sasl; + + /** + * Dictionary keeping the stream state names + */ + static const TokenDict s_stateName[]; + + /** + * Dictionary keeping the flag names + */ + static const TokenDict s_flagName[]; + + /** + * Dictionary keeping the stream type names + */ + static const TokenDict s_typeName[]; + +protected: + /** + * Constructor. Build an incoming stream from a socket + * @param engine Engine owning this stream + * @param socket The socket + * @param t Stream type as enumeration + */ + JBStream(JBEngine* engine, Socket* socket, Type t); + + /** + * Constructor. Build an outgoing stream + * @param engine Engine owning this stream + * @param t Stream type as enumeration + * @param local Local party jabber id + * @param remote Remote party jabber id + * @param name Optional stream name + * @param params Optional stream parameters + */ + JBStream(JBEngine* engine, Type t, const JabberID& local, const JabberID& remote, + const char* name = 0, const NamedList* params = 0); + + /** + * Close the stream. Release memory + */ + virtual void destroyed(); + + /** + * Check if stream state processor can continue. + * This method is called from getEvent() with the stream locked + * @param time Current time + * @return True to indicate stream availability to process its state, + * false to return the last event, if any + */ + virtual bool canProcess(u_int64_t time); + + /** + * Process stream state. Get XML from parser's queue and process it + * This method is called from getEvent() with the stream locked + * @param time Current time + */ + virtual void process(u_int64_t time); + + /** + * Process elements in Running state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processRunning(XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Check stream timeouts. + * This method is called from getEvent() with the stream locked, after + * the process() method returned without setting the last event + * @param time Current time + */ + virtual void checkTimeouts(u_int64_t time); + + /** + * Reset the stream's connection. Build a new XML parser if the socket is valid + * Release the old connection + * @param sock The new socket + */ + virtual void resetConnection(Socket* sock = 0); + + /** + * Build a stream start XML element + * @return XmlElement pointer + */ + virtual XmlElement* buildStreamStart(); + + /** + * Process stream start elements while waiting for them + * @param xml Received xml element + * @param from The 'from' attribute + * @param to The 'to' attribute + * @return False if stream termination was initiated + */ + virtual bool processStart(const XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Process elements in Auth state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processAuth(XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Process elements in Register state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processRegister(XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Check if a received stream start element is correct. + * Check namespaces and set stream version + * Check and set the id for outgoing streams + * Generate an id for incoming streams + * Terminate the stream if this conditions are met + * @param xml Received xml element. Will be consumed and zeroed if false is returned + * @return False if stream termination was initiated + */ + bool processStreamStart(const XmlElement* xml); + + /** + * Check if a received element is a stream error one + * @param xml Received xml element + * @return True if stream termination was initiated (the xml will be consumed) + */ + bool streamError(XmlElement* xml); + + /** + * Retrieve and check the 'from' and 'to' JIDs from a receive element + * @param xml Received xml element + * @param from Jabber ID to set from the 'from' attribute + * @param to Jabber ID to set from the 'to' attribute + * @return False if stream termination was initiated (the xml will be consumed) + */ + bool getJids(XmlElement* xml, JabberID& from, JabberID& to); + + /** + * Check if a received element is a presence, message or iq qualified by the stream + * namespace and the stream is not authenticated. + * Validate 'from' for c2s streams + * Validate s2s 'to' domain and 'from' jid + * Fix 'from' or 'to' is needed + * @param xml Received xml element (will be consumed if false is returned) + * @param from The sender of the stanza + * @param to Stanza recipient + * @return False if the element was consumed (error was sent or stream termination was initiated) + */ + bool checkStanzaRecv(XmlElement* xml, JabberID& from, JabberID& to); + + /** + * Change stream state. Reset state depending data + * @param newState The new stream state + * @param time Current time + */ + void changeState(State newState, u_int64_t time = Time::msecNow()); + + /** + * Check for pending events. Set the last event + */ + void checkPendingEvent(); + + /** + * Send pending stream XML or stanzas + * Terminate the stream on error + * @param streamOnly Try to send only existing stream related XML elements + * @return True on success + */ + bool sendPending(bool streamOnly = false); + + /** + * Write data to socket. Terminate the stream on socket error + * @param data Buffer to sent + * @param len The number of bytes to send. Filled with actually sent bytes on exit + * @return True on success, false if stream termination was initiated + */ + bool writeSocket(const char* data, unsigned int& len); + + /** + * Update stream flags and remote connection data from engine + */ + void updateFromRemoteDef(); + + /** + * Retrieve the first required feature in the list + * @return XMPPFeature pointer or 0 + */ + XMPPFeature* firstRequiredFeature(); + + /** + * Drop (delete) received XML element + * @param xml The element to delete + * @param reason The reason + * @return True + */ + bool dropXml(XmlElement*& xml, const char* reason); + + /** + * Terminate (destroy) the stream. Drop (delete) received XML element + * @param xml The element to delete + * @param error Terminate error + * @param reason Drop reason + * @return False + */ + inline bool destroyDropXml(XmlElement*& xml, XMPPError::Type error, const char* reason) { + dropXml(xml,reason); + terminate(0,true,0,error); + return false; + } + + State m_state; // Stream state + String m_id; // Stream id + JabberID m_local; // Local peer's jid + JabberID m_remote; // Remote peer's jid + int m_flags; // Stream flags + XMPPNamespace::Type m_xmlns; // Stream namespace + XMPPFeatureList m_features; // Advertised features + JBEvent* m_lastEvent; // Last event generated by this stream + ObjList m_events; // Queued events + ObjList m_pending; // Pending outgoing elements + // Timers + u_int64_t m_setupTimeout; // Overall stream setup timeout + u_int64_t m_startTimeout; // Incoming: wait stream start period + u_int64_t m_pingTimeout; // Sent ping timeout + u_int64_t m_nextPing; // Next ping + u_int64_t m_idleTimeout; // Stream idle timeout + u_int64_t m_connectTimeout; // Stream connect timeout + // + unsigned int m_restart; // Remaining restarts + u_int64_t m_timeToFillRestart; // The next time to increase the restart counter + + String m_pingId; + +private: + // Forbidden default constructor + inline JBStream() {} + // Process incoming elements in Challenge state + // The element will be consumed + // Return false if stream termination was initiated + bool processChallenge(XmlElement* xml, const JabberID& from, + const JabberID& to); + // Process incoming 'auth' elements qualified by SASL namespace + // The element will be consumed + // Return false if stream termination was initiated + bool processSaslAuth(XmlElement* xml, const JabberID& from, + const JabberID& to); + // Process received elements in Features state (incoming stream) + // The element will be consumed + // Return false if stream termination was initiated + bool processFeaturesIn(XmlElement* xml, const JabberID& from, + const JabberID& to); + // Process received elements in Features state (outgoing stream) + // The element will be consumed + // Return false if stream termination was initiated + bool processFeaturesOut(XmlElement* xml, const JabberID& from, + const JabberID& to); + // Process received elements in WaitTlsRsp state (outgoing stream) + // The element will be consumed + // Return false if stream termination was initiated + bool processWaitTlsRsp(XmlElement* xml, const JabberID& from, + const JabberID& to); + // Set secured flag. Remove feature from list + inline void setSecured() { + m_flags |= StreamSecured; + m_features.remove(XMPPNamespace::Tls); + } + // Set stream namespace from type + void setXmlns(); + // Event termination notification + // @param event The notifier. Ignored if it's not m_lastEvent + void eventTerminated(const JBEvent* event); + + enum { + SocketCanRead = 0x01, + SocketReading = 0x02, + SocketWriting = 0x10, + }; + inline void socketSetCanRead(bool ok) { + if (ok) + m_socketFlags |= SocketCanRead; + else + m_socketFlags &= ~SocketCanRead; + } + inline void socketSetReading(bool ok) { + if (ok) + m_socketFlags |= SocketReading; + else + m_socketFlags &= ~SocketReading; + } + inline void socketSetWriting(bool ok) { + if (ok) + m_socketFlags |= SocketWriting; + else + m_socketFlags &= ~SocketWriting; + } + inline bool socketCanRead() const + { return m_socket && (m_socketFlags & SocketCanRead); } + inline bool socketReading() const + { return m_socketFlags & SocketReading; } + inline bool socketWriting() const + { return m_socketFlags & SocketWriting; } + + JBEngine* m_engine; // The owner of this stream + int m_type; // Stream type + bool m_incoming; // Stream direction + String m_name; // Local (internal) name + JBEvent* m_terminateEvent; // Pending terminate event + // Pending outgoing XML + String m_outStreamXml; + // Connection related data + XmlDomParser* m_xmlDom; + Socket* m_socket; + char m_socketFlags; // Socket flags: 0: unavailable + String m_connectAddr; // Remote ip to connect to + int m_connectPort; // Remote port to connect to +}; + + +/** + * This class holds a client to server stream + * @short A client to server stream + */ +class YJABBER_API JBClientStream : public JBStream +{ + YCLASS(JBClientStream,JBStream) + friend class JBStream; +public: + /** + * Constructor. Build an incoming stream from a socket + * @param engine Engine owning this stream + * @param socket The socket + */ + JBClientStream(JBEngine* engine, Socket* socket); + + /** + * Constructor. Build an outgoing stream + * @param engine Engine owning this stream + * @param jid User jid + * @param account Account (stream) name + * @param params Stream parameters + */ + JBClientStream(JBEngine* engine, const JabberID& jid, const String& account, + const NamedList& params); + + /** + * Retrieve stream's user data + * @return GenObject pointer or 0 + */ + inline GenObject* userData() + { return m_userData; } + + /** + * Set stream's user data. Transfer data ownership to the stream + * This method is thread safe + * @param data Data to set + */ + inline void userData(GenObject* data) { + Lock lock(this); + TelEngine::destruct(m_userData); + m_userData = data; + } + + /** + * Get a client stream from this one + * @return JBClientStream pointer + */ + virtual JBClientStream* clientStream() + { return this; } + + /** + * Bind a resource to an incoming stream. This method should be called + * after processing a Bind event + * This method is thread safe + * @param resource Resource to bind. Empty on error + * @param id Received bind request id + * @param error Failure reason. Ignored on success + */ + void bind(const String& resource, const char* id, + XMPPError::Type error = XMPPError::NoError); + + /** + * Request account register or change an outgoing stream. + * This method is thread safe + * @param data True to request registration/change, false to request info + * @param set True to request new user registration, false to remove account from server + * @param newPass New password when requesting account setup on an already + * authenticated stream + * @return True on success + */ + bool requestRegister(bool data, bool set = true, + const String& newPass = String::empty()); + +protected: + /** + * Process elements in Running state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processRunning(XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Process stream start elements while waiting for them + * @param xml Received xml element + * @param from The 'from' attribute + * @param to The 'to' attribute + * @return False if stream termination was initiated + */ + virtual bool processStart(const XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Process elements in Auth state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processAuth(XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Process elements in Register state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processRegister(XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Release memory + */ + virtual void destroyed(); + + /** + * Start outgoing stream authentication + * @return True on success + */ + bool startAuth(); + + /** + * Start resource binding on outgoing stream + * @return True on success + */ + bool bind(); + +private: + inline bool isRegisterId(XmlElement& xml) { + if (!m_registerReq) + return false; + String* id = xml.getAttribute("id"); + return id && id->length() == 1 && (*id)[0] == m_registerReq; + } + + GenObject* m_userData; // User (upper layer) data + String m_password; // The password + String m_newPassword; // New password + char m_registerReq; // Register requested. 1(data) 2(register) 3(remove) +}; + + +/** + * This class holds a server to server stream + * @short A server to server stream + */ +class YJABBER_API JBServerStream : public JBStream +{ + YCLASS(JBServerStream,JBStream) +public: + /** + * Constructor. Build an incoming stream from a socket + * @param engine Engine owning this stream + * @param socket The socket + */ + JBServerStream(JBEngine* engine, Socket* socket); + + /** + * Constructor. Build an outgoing stream + * @param engine Engine owning this stream + * @param local Local party jabber id + * @param remote Remote party jabber id + * @param dbId Optional dialback id (stream id) + * @param dbKey Optional dialback key to verify + * @param dbOnly True if this is a dialback only stream + */ + JBServerStream(JBEngine* engine, const JabberID& local, const JabberID& remote, + const char* dbId = 0, const char* dbKey = 0, bool dbOnly = false); + + /** + * Check if this is an outgoing dialback stream + * @return True if this stream is an outgoing dialback one + */ + inline bool dialback() const + { return outgoing() && flag(DialbackOnly); } + + /** + * Take the dialback key from this stream + * @return NamedString pointer or 0 if there is no dialback key held by this stream + */ + inline NamedString* takeDb() { + Lock lock(this); + NamedString* tmp = m_dbKey; + m_dbKey = 0; + return tmp; + } + + /** + * Get a server stream from this one + * @return JBServerStream pointer + */ + virtual JBServerStream* serverStream() + { return this; } + + /** + * Send a dialback verify response + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id The 'id' attribute + * @param valid True if valid, false if invalid + * @return True on success + */ + inline bool sendDbVerify(const char* from, const char* to, const char* id, + bool valid) { + XmlElement* rsp = XMPPUtils::createDialbackVerifyRsp(from,to,id,valid); + return sendStreamXml(state(),rsp); + } + + /** + * Send a dialback key response. + * If the stream is in Dialback state change it's state to Running if valid or + * terminate it if invalid + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param valid True if valid, false if invalid + * @return True on success + */ + bool sendDbResult(const JabberID& from, const JabberID& to, bool valid); + + /** + * Send dialback data (key/verify) + * @return Flase if stream termination was initiated + */ + bool sendDialback(); + +protected: + /** + * Release memory + */ + virtual void destroyed(); + + /** + * Process elements in Running state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processRunning(XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Build a stream start XML element + * @return XmlElement pointer + */ + virtual XmlElement* buildStreamStart(); + + /** + * Process stream start elements while waiting for them + * @param xml Received xml element + * @param from The 'from' attribute + * @param to The 'to' attribute + * @return False if stream termination was initiated + */ + virtual bool processStart(const XmlElement* xml, const JabberID& from, + const JabberID& to); + + /** + * Process elements in Auth state + * @param xml Received element (will be consumed) + * @param from Already parsed source JID + * @param to Already parsed destination JID + * @return False if stream termination was initiated + */ + virtual bool processAuth(XmlElement* xml, const JabberID& from, + const JabberID& to); + +private: + NamedString* m_dbKey; // Outgoing: initial dialback key to check +}; + + +/** + * This class holds data related to a remote domain. + * The String holds the domain + * @short Options and connect settings for a remote domain + */ +class YJABBER_API JBRemoteDomainDef : public String +{ + YCLASS(JBRemoteDomainDef,String) +public: + /** + * Constructor + * @param domain Domain name + */ + inline JBRemoteDomainDef(const char* domain = 0) + : String(domain), m_port(0), m_flags(0) + {} + + /** + * Remote address used to connect to + */ + String m_address; + + /** + * Remote port used to connect to + */ + int m_port; + + /** + * Domain flags + */ + int m_flags; +}; + + +/** + * This class holds data used to connect an outgoing stream + * A descendant class should implement the thread run method + * @short A socket connector + */ +class YJABBER_API JBConnect : public GenObject +{ + YCLASS(JBConnect,GenObject) +public: + /** + * Constructor. Add itself to the stream's engine + * @param stream The stream to connect + */ + JBConnect(const JBStream& stream); + + /** + * Destructor. Remove from engine if still there + */ + virtual ~JBConnect(); + + /** + * Stop the thread. This method should be re-implemented + */ + virtual void stopConnect(); + + /** + * Retrieve the stream name + * @return Stream name + */ + virtual const String& toString() const; + +protected: + /** + * Connect the socket. + * Retrieve ip/port from engine ant use them if valid or try to use SRV records returned by + * the given domain or use the domain's ip address and the default port given by the stream type. + * Notify the stream on termination. + * This method should be called from it's own thread + */ + void connect(); + +private: + // No default constructor + inline JBConnect() + {} + // Check if exiting. Release socket if exiting + bool exiting(Socket*& sock); + // Connect a socket + bool connect(Socket*& sock, const char* addr, int port); + // Notify termination, remove from engine + void terminated(Socket* sock, bool final); + + String m_domain; // Remote domain + String m_address; // Remote ip address + int m_port; // Port to connect to + JBEngine* m_engine; // The engine owning this connector + String m_stream; // Stream name + JBStream::Type m_streamType; // Stream type +}; + + +/** + * This class holds a Jabber engine + * @short A Jabber engine + */ +class YJABBER_API JBEngine : public DebugEnabler, public Mutex, public GenObject +{ + YCLASS(JBEngine,GenObject) + friend class JBStream; + friend class JBConnect; + friend class JBStreamSetProcessor; +public: + /** + * Constructor + * @param name Engine name + */ + JBEngine(const char* name = "jbengine"); + + /** + * Destructor + */ + virtual ~JBEngine(); + + /** + * Retrieve the stream read buffer length + * @return Stream read buffer length + */ + inline unsigned int streamReadBuffer() const + { return m_streamReadBuffer; } + + /** + * Check if this engine is exiting + * @return True if this engine is exiting + */ + inline bool exiting() const + { return m_exiting; } + + /** + * Set the exiting flag. Terminate all streams + */ + inline void setExiting() { + if (m_exiting) + return; + m_exiting = true; + dropAll(JBStream::TypeCount,JabberID::empty(),JabberID::empty(), + XMPPError::Shutdown); + } + + /** + * Find a remote domain definition. Return the default settings if not found. + * This method is not thread safe + * @param domain The domain to find + * @return Valid JBRemoteDomainDef pointer + */ + inline JBRemoteDomainDef* remoteDomainDef(const String& domain) { + ObjList* o = m_remoteDomains.find(domain); + return o ? static_cast(o->get()) : &m_remoteDomain; + } + + /** + * Cleanup streams. Stop all threads owned by this engine. Release memory + */ + virtual void destruct(); + + /** + * Initialize the engine's parameters. Start private streams if requested + * @param params Engine's parameters + */ + virtual void initialize(const NamedList& params); + + /** + * Stop connect threads. Drop all streams. Stop all stream sets. Release memory if final + * @param final True if called from destructor + * @param waitTerminate True to wait for all streams to terminate + */ + virtual void cleanup(bool final = false, bool waitTerminate = true); + + /** + * Accept an incoming stream connection. Build a stream. + * Don't delete the socket if false is returned + * @param sock Accepted socket + * @param remote Remote ip and port + * @param t Expected stream type + * @return True on success + */ + bool acceptConn(Socket* sock, SocketAddr& remote, JBStream::Type t); + + /** + * Find a stream by its name. This method is thread safe + * @param id The internal id of the stream to find + * @param hint Optional stream type hint + * @return Referenced JBStream pointer or 0 + */ + virtual JBStream* findStream(const String& id, + JBStream::Type hint = JBStream::TypeCount); + + /** + * Find all c2s streams whose local or remote bare jid matches a given one. + * Ignore destroying streams. + * This method is thread safe + * @param in True for incoming, false for outgoing + * @param jid JID to compare (the local one for outgoing, remote jid for incoming) + * @param flags Optional stream flag to match + * @return List of referenced JBClientStream pointers or 0 + */ + ObjList* findClientStreams(bool in, const JabberID& jid, int flags = 0xffffffff); + + /** + * Find all c2s streams whose local or remote bare jid matches a given one and + * their resource is found in the given list. + * Ignore destroying streams. + * This method is thread safe + * @param in True for incoming, false for outgoing + * @param jid JID to compare (the local one for outgoing, remote jid for incoming) + * @param resources The list of resources to match + * @param flags Optional stream flag to match + * @return List of referenced JBClientStream pointers or 0 + */ + ObjList* findClientStreams(bool in, const JabberID& jid, const ObjList& resources, + int flags = 0xffffffff); + + /** + * Find a c2s stream by its local or remote jid. + * This method is thread safe + * @param in True for incoming, false for outgoing + * @param jid JID to compare (the local one for outgoing, remote jid for incoming) + * @return Referenced JBClientStream pointer or 0 + */ + JBClientStream* findClientStream(bool in, const JabberID& jid); + + /** + * Terminate all streams matching type and/or local/remote jid + * @param type Stream type. Match all stream types if unknown + * @param local Optional local jid to match + * @param remote Optional remote jid to match + * @param error Optional error to be sent to the client + * @param reason Optional error text to be sent to the client + * @return The number of stream terminated + */ + virtual unsigned int dropAll(JBStream::Type type = JBStream::TypeCount, + const JabberID& local = JabberID::empty(), + const JabberID& remote = JabberID::empty(), + XMPPError::Type error = XMPPError::NoError, const char* reason = 0); + + /** + * Build an internal stream name + * @param name Destination buffer + */ + virtual void buildStreamName(String& name) + {} + + /** + * Check if a domain is serviced by this engine + * @param domain Domain to check + * @return True if the given domain is serviced by this engine + */ + virtual bool hasDomain(const String& domain) + { return false; } + + /** + * Process an event. The default implementation will return the event + * to this engine + * @param ev The event to process + */ + virtual void processEvent(JBEvent* ev); + + /** + * Return an event to this engine. The default implementation will send an + * error if apropriate and delete the event + * @param ev The event to return + * @param error Optional error to be returned to the event's XML sender + * @param reason Optional text to be attached to the error + */ + virtual void returnEvent(JBEvent* ev, XMPPError::Type error = XMPPError::NoError, + const char* reason = 0); + + /** + * Start stream TLS + * @param stream The stream to enchrypt + */ + virtual void encryptStream(JBStream* stream); + + /** + * Connect an outgoing stream + * @param stream The stream to connect + */ + virtual void connectStream(JBStream* stream); + + /** + * Build a dialback key + * @param id The stream id + * @param key The dialback key + */ + virtual void buildDialbackKey(const String& id, String& key); + + /** + * Check if an outgoing stream exists with the same id and remote peer + * @param stream The calling stream + * @return True if a duplicate is found + */ + bool checkDupId(const JBStream* stream); + + /** + * Print XML to output + * @param stream Stream requesting the operation + * @param send True if sending, false if receiving + * @param xml XML to print + */ + virtual void printXml(const JBStream* stream, bool send, XmlChild& xml) const; + + /** + * Print an XML fragment to output + * @param stream Stream requesting the operation + * @param send True if sending, false if receiving + * @param frag XML fragment to print + */ + virtual void printXml(const JBStream* stream, bool send, XmlFragment& frag) const; + +protected: + /** + * Add a stream to one of the stream lists + * @param stream The stream to add + */ + virtual void addStream(JBStream* stream); + + /** + * Remove a stream + * @param stream The stream to remove + * @param delObj True to release the stream, false to remove it from list + * without releasing it + */ + virtual void removeStream(JBStream* stream, bool delObj = true); + + /** + * Stop all stream sets + * @param waitTerminate True to wait for all streams to terminate + */ + virtual void stopStreamSets(bool waitTerminate = true) + {} + + /** + * Retrieve the list of streams of a given type. + * Descendant must implement it + * @param list The destination list to set + * @param type Stream type + */ + virtual void getStreamList(RefPointer& list, int type) + {} + + /** + * Retrieve all streams + * @param list The destination list to set. The first index will be filled with the + * c2s streams list, the second index will be set to the s2s stream list + * @param type Optional stream type + */ + inline void getStreamLists(RefPointer list[JBStream::TypeCount], + int type = JBStream::TypeCount) { + if (type == JBStream::c2s || type == JBStream::TypeCount) + getStreamList(list[JBStream::c2s],JBStream::c2s); + if (type == JBStream::s2s || type == JBStream::TypeCount) + getStreamList(list[JBStream::s2s],JBStream::s2s); + } + + /** + * Find a stream by its name in a given set list + * @param id The name of the stream to find + * @param list The list to search for a stream + * @return Referenced JBStream pointer or 0 + */ + JBStream* findStream(const String& id, JBStreamSetList* list); + + bool m_exiting; // Engine exiting flag + JBRemoteDomainDef m_remoteDomain; // Default remote domain definition + ObjList m_remoteDomains; // Remote domain definitions + unsigned char m_restartMax; // Maximum value for stream restart counter + unsigned int m_restartUpdInterval; // Update interval for stream restart counter + unsigned int m_setupTimeout; // Overall stream setup timeout + unsigned int m_startTimeout; // Wait stream start period + unsigned int m_connectTimeout; // Outgoing: socket connect timeout + unsigned int m_pingInterval; // Stream idle interval (no data received) + unsigned int m_pingTimeout; // Sent ping timeout + unsigned int m_idleTimeout; // Stream idle timeout (nothing sent or received) + unsigned int m_streamReadBuffer; // Stream read buffer length + unsigned int m_maxIncompleteXml; // Maximum length of an incomplete xml + int m_printXml; // Print XML data to output + bool m_initialized; // True if already initialized + +private: + // Add/remove a connect stream thread when started/stopped + void connectStatus(JBConnect* conn, bool started); + + ObjList m_connect; // Connecting streams +}; + +/** + * This class implements a Jabber server engine + * @short A Jabber server engine + */ +class YJABBER_API JBServerEngine : public JBEngine +{ + YCLASS(JBServerEngine,GenObject) +public: + /** + * Constructor + * @param name Engine name + */ + JBServerEngine(const char* name = "jbserverengine"); + + /** + * Destructor + */ + ~JBServerEngine(); + + /** + * Terminate all streams. Stop all sets processors. Release memory if final + * @param final True if called from destructor + * @param waitTerminate True to wait for all streams to terminate + */ + virtual void cleanup(bool final = false, bool waitTerminate = true); + + /** + * Build an internal stream name + * @param name Destination buffer + */ + virtual void buildStreamName(String& name) + { name << "stream/" << getStreamIndex(); } + + /** + * Find a server to server stream by local/remote domain. + * Skip over outgoing dialback only streams + * This method is thread safe + * @param local Local domain + * @param remote Remote domain + * @param out True to find an outgoing stream, false to find an incoming one + * @return Referenced JBServerStream pointer or 0 + */ + JBServerStream* findServerStream(const String& local, const String& remote, bool out); + + /** + * Create an outgoing s2s stream. + * @param local Local party domain + * @param remote Remote party domain + * @param dbId Optional dialback id (stream id) + * @param dbKey Optional dialback key to verify + * @param dbOnly True if this is a dialback only stream + * @return Referenced JBServerStream pointer or 0 if a stream already exists + */ + JBServerStream* createServerStream(const String& local, const String& remote, + const char* dbId = 0, const char* dbKey = 0, bool dbOnly = false); + + /** + * Terminate all incoming c2s streams matching a given JID + * This method is thread safe + * @param jid Client JID + * @param error Optional error to be sent to the client + * @param reason Optional error text to be sent to the client + * @return The number of stream terminated + */ + unsigned int terminateClientStreams(const JabberID& jid, + XMPPError::Type error = XMPPError::NoError, const char* reason = 0); + +protected: + /** + * Add a stream to one of the stream lists + * @param stream The stream to add + */ + virtual void addStream(JBStream* stream); + + /** + * Remove a stream + * @param stream The stream to remove + * @param delObj True to release the stream, false to remove it from list + * without releasing it + */ + virtual void removeStream(JBStream* stream, bool delObj = true); + + /** + * Stop all stream sets + * @param waitTerminate True to wait for all streams to terminate + */ + virtual void stopStreamSets(bool waitTerminate = true); + + /** + * Retrieve the list of streams of a given type + * @param list The destination list to set + * @param type Stream type + */ + virtual void getStreamList(RefPointer& list, int type); + + /** + * Increment and return the stream index counter + * @return Current stream index + */ + inline unsigned int getStreamIndex() { + Lock lock(this); + return ++m_streamIndex; + } + + unsigned int m_streamIndex; // Index used to build stream name + JBStreamSetList* m_c2sReceive; // c2s streams receive list + JBStreamSetList* m_c2sProcess; // c2s streams process list + JBStreamSetList* m_s2sReceive; // s2s streams receive list + JBStreamSetList* m_s2sProcess; // s2s streams process list +}; + +/** + * This class implements a Jabber client engine + * @short A Jabber client engine + */ +class YJABBER_API JBClientEngine : public JBEngine +{ + YCLASS(JBClientEngine,GenObject) +public: + /** + * Constructor + * @param name Engine name + */ + JBClientEngine(const char* name = "jbclientengine"); + + /** + * Destructor + */ + ~JBClientEngine(); + + /** + * Terminate all streams. Stop all sets processors. Release memory if final + * @param final True if called from destructor + * @param waitTerminate True to wait for all streams to terminate + */ + virtual void cleanup(bool final = false, bool waitTerminate = true); + + /** + * Build an outgoing client stream + * @param account Account (stream) name + * @param params Stream parameters + * @return Referenced JBClientStream pointer or 0 if a stream already exists + */ + JBClientStream* create(const String& account, const NamedList& params); + +protected: + /** + * Add a stream to one of the stream lists + * @param stream The stream to add + */ + virtual void addStream(JBStream* stream); + + /** + * Remove a stream + * @param stream The stream to remove + * @param delObj True to release the stream, false to remove it from list + * without releasing it + */ + virtual void removeStream(JBStream* stream, bool delObj = true); + + /** + * Stop all stream sets + * @param waitTerminate True to wait for all streams to terminate + */ + virtual void stopStreamSets(bool waitTerminate = true); + + /** + * Retrieve the list of streams of a given type + * @param list The destination list to set + * @param type Stream type + */ + virtual void getStreamList(RefPointer& list, int type); + + JBStreamSetList* m_receive; // Streams receive list + JBStreamSetList* m_process; // Streams process list +}; + +/** + * This class holds a set of streams to be processed in an uniform way. + * This is a base class for specialized stream list processors. + * Its process() method should be called in its own thread + * @short A set of streams to be processed in an uniform way + */ +class YJABBER_API JBStreamSet : public GenObject, public Mutex +{ + YCLASS(JBStreamSet,GenObject); + friend class JBStreamSetList; +public: + /** + * Destructor. Delete the owned streams. Remove from owner + */ + virtual ~JBStreamSet(); + + /** + * Retrieve the list of clients. + * Make sure the set is locked before calling this method + * @return The list of clients + */ + inline ObjList& clients() + { return m_clients; } + + /** + * Add a stream to the set. The stream's reference counter will be increased. + * This method doesn't check if the stream is already added + * @param client The stream to append + * @return True on success, false if there is no more room in this set + */ + virtual bool add(JBStream* client); + + /** + * Remove a stream from set + * @param client The stream to remove + * @param delObj True to release the stream, false to remove it from list + * without releasing it + * @return True on success, false if not found + */ + virtual bool remove(JBStream* client, bool delObj = true); + + /** + * Terminate all streams matching local/remote jid + * @param local Optional local jid to match + * @param remote Optional remote jid to match + * @param error Optional error to be sent to the client + * @param reason Optional error text to be sent to the client + * @return The number of streams terminated + */ + unsigned int dropAll(const JabberID& local = JabberID::empty(), + const JabberID& remote = JabberID::empty(), + XMPPError::Type error = XMPPError::NoError, const char* reason = 0); + + /** + * Process the list. + * Returns as soon as there are no more streams in the list + */ + void run(); + + /** + * Start running + * @return True on success + */ + virtual bool start(); + + /** + * Stop running + */ + virtual void stop(); + +protected: + /** + * Constructor + * @param owner The list owning this set + */ + JBStreamSet(JBStreamSetList* owner); + + /** + * This method is called from run() with the list unlocked and stream's + * reference counter increased. + * A specialized processor must implement this method + * @param stream The stream to process + * @return True if something was processed + */ + virtual bool process(JBStream& stream) = 0; + + bool m_changed; // List changed flag + bool m_exiting; // The thread is exiting (don't accept clients) + JBStreamSetList* m_owner; // The list owning this set + ObjList m_clients; // The streams list + +private: + JBStreamSet() {} // Private default constructor (forbidden) +}; + + +/** + * This class holds a set specialized in stream processing + * @short Specialized stream processor + */ +class YJABBER_API JBStreamSetProcessor : public JBStreamSet +{ + YCLASS(JBStreamSetProcessor,JBStreamSet); +protected: + /** + * Constructor + * @param owner The list owning this set + */ + inline JBStreamSetProcessor(JBStreamSetList* owner) + : JBStreamSet(owner) + {} + + /** + * This method is called from run() with the list unlocked and stream's + * reference counter increased. + * Calls stream's getEvent(). Pass a generated event to the engine + * Remove the stream from its engine on destroy + * @param stream The stream to process + * @return True if an event was generated by the stream + */ + virtual bool process(JBStream& stream); +}; + + +/** + * This class holds a set specialized in stream data receiver + * @short Specialized stream data receiver + */ +class YJABBER_API JBStreamSetReceive : public JBStreamSet +{ + YCLASS(JBStreamSetReceive,JBStreamSet); +protected: + /** + * Constructor. Build the read buffer + * @param owner The list owning this set + */ + JBStreamSetReceive(JBStreamSetList* owner); + + /** + * This method is called from run() with the list unlocked and stream's + * reference counter increased. + * Calls stream's readSocket() + * @param stream The stream to process + * @return True if the stream received any data + */ + virtual bool process(JBStream& stream); + +protected: + DataBlock m_buffer; // Read buffer +}; + + +/** + * This class holds a list of stream sets. + * The purpose is to create a list of threads + * @short A list of stream sets + */ +class YJABBER_API JBStreamSetList : public RefObject, public Mutex +{ + YCLASS(JBStreamSetList,RefObject); + friend class JBStreamSet; +public: + /** + * Constructor + * @param engine Engine owning this list + * @param max Maximum streams per set (0 for maximum possible) + * @param sleepMs Time to sleep when idle + * @param name List name (for debugging purposes) + */ + JBStreamSetList(JBEngine* engine, unsigned int max, unsigned int sleepMs, + const char* name); + + /** + * Retrieve the stream set list. + * Make sure the list is locked before calling this method + * @return The stream set list + */ + inline ObjList& sets() + { return m_sets; } + + /** + * Destructor + */ + virtual ~JBStreamSetList(); + + /** + * Retrieve the maximum number of streams per set + * @return The maximum number of streams per set + */ + inline unsigned int max() const + { return m_max; } + + /** + * Retrieve the number of streams in all sets + * @return The number of streams in all sets + */ + inline unsigned int streamCount() const + { return m_streamCount; } + + /** + * Retrieve the engine owning this list + * @return The engine owning this list + */ + inline JBEngine* engine() const + { return m_engine; } + + /** + * Add a stream to the list. Build a new set if there is no room in existing sets + * @param client The stream to add + * @return True on success + */ + bool add(JBStream* client); + + /** + * Remove a stream from list + * @param client The stream to remove + * @param delObj True to release the stream, false to remove it from list + * without releasing it + */ + void remove(JBStream* client, bool delObj = true); + + /** + * Stop one set or all sets + * @param set The set to stop, 0 to stop all + * @param waitTerminate True to wait for all streams to terminate + */ + void stop(JBStreamSet* set = 0, bool waitTerminate = true); + + /** + * Get the string representation of this list + * @return The list name + */ + virtual const String& toString() const; + +protected: + /** + * Stop all sets. Release memory + */ + virtual void destroyed(); + + /** + * Remove a set from list without deleting it + * @param set The set to remove + */ + void remove(JBStreamSet* set); + + /** + * Build a specialized stream set. Descendants must override this method + * @return JBStreamSet pointer or 0 + */ + virtual JBStreamSet* build(); + + JBEngine* m_engine; // The engine owning this list + String m_name; // List name + unsigned int m_max; // The maximum number of streams per set + unsigned int m_sleepMs; // Time to sleep if nothig processed + ObjList m_sets; // The sets list + +private: + JBStreamSetList() {} // Private default constructor (forbidden) + + unsigned int m_streamCount; // Current number of streams in this list +}; + + +/** + * This class holds entity capability data + * Implements XEP 0115 support + * @short Entity capability + */ +class YJABBER_API JBEntityCaps : public String +{ + YCLASS(JBEntityCaps,String); +public: + /** + * Supported XEP 0115 versions + */ + enum { + Ver1_3 = 1, // Version lower then 1.4 (m_data is the node version + advertised extensions) + Ver1_4 = 2, // Version 1.4 or greater (m_data is the SHA-1 hash of features and identities) + }; + + /** + * Constructor + * @param id Object id + * @param version Entity caps version + * @param node Entity node + * @param data Entity data + */ + inline JBEntityCaps(const char* id, char version, const char* node, const char* data) + : String(id), + m_version(version), m_node(node), m_data(data) + {} + + /** + * Build an entity caps id + * @param buf Destination buffer + * @param version Entity caps version + * @param node Entity node + * @param data Entity data + * @param ext Optional entity extensions + */ + static inline void buildId(String& buf, char version, const char* node, + const char* data, String* ext = 0) + { buf << (int)version << node << data << (ext ? ext->c_str() : ""); } + + char m_version; + String m_node; + String m_data; + XMPPFeatureList m_features; + +private: + JBEntityCaps() {} +}; + + +/** + * This class holds data and offer entity capability services. + * Implements XEP 0115 support + * @short Entity capability list manager + */ +class YJABBER_API JBEntityCapsList : public ObjList, public Mutex +{ + YCLASS(JBEntityCapsList,ObjList); +public: + /** + * Constructor + */ + inline JBEntityCapsList() + : Mutex(true,"JBEntityCapsList"), m_enable(true), m_reqIndex(0) + { m_reqPrefix << "xep0115" << (unsigned int)Time::msecNow() << "_"; } + + /** + * Retrieve an entity caps object. This method is not thread safe + * @param id The id to find + * @return JBEntityCaps pointer or 0 + */ + inline JBEntityCaps* findCaps(const String& id) { + for (ObjList* o = skipNull(); o; o = o->skipNext()) + if (o->get()->toString() == id) + return static_cast(o->get()); + return 0; + } + + /** + * Expire pending requests. + * This method is thread safe + * @param msecNow Current time + */ + void expire(u_int64_t msecNow = Time::msecNow()); + + /** + * Process a response. + * This method is thread safe + * @param rsp The element to process + * @param id The element's id + * @param ok True if the response is a result one, false if it's an error + * @return True if the element was processed (handled) + */ + bool processRsp(XmlElement* rsp, const String& id, bool ok); + + /** + * Request entity capabilities. + * This method is thread safe + * @param stream The stream to send the request + * @param from The 'from' attribute + * @param to The 'to' attribute + * @param id Entity caps id + * @param version Entity caps version + * @param node Entity node + * @param data Entity caps data + */ + void requestCaps(JBStream* stream, const char* from, const char* to, const String& id, + char version, const char* node, const char* data); + + /** + * Build an XML document from this list. + * This method is thread safe + * @param rootName Document root element name + * @return XmlDocument pointer + */ + XmlDocument* toDocument(const char* rootName = "entitycaps"); + + /** + * Build this list from an XML document. + * This method is thread safe + * @param doc Document to build from + * @param rootName Document root element name (it will be checked if set) + * @return XmlDocument pointer + */ + void fromDocument(XmlDocument& doc, const char* rootName = "entitycaps"); + + /** + * Process an element containing an entity capabily child. + * Request capabilities if not found in the list. + * This method is thread safe + * @param capsId String to be filled with entity caps object id + * (empty if an entity caps child is not found in element ) + * @param xml XML element to process + * @param stream The stream used to request capabilities + * @param from The 'from' attribute of the request stanza + * @param to The 'to' attribute of the request stanza + * @return XmlDocument pointer + */ + virtual void processCaps(String& capsId, XmlElement* xml, JBStream* stream, + const char* from, const char* to); + + /** + * Add capabilities to a list. + * This method is thread safe + * @param list Destination list + * @param id Entity caps id + */ + inline void addCaps(NamedList& list, const String& id) { + Lock lock(this); + JBEntityCaps* caps = findCaps(id); + if (caps) + addCaps(list,*caps); + } + + /** + * Add capabilities to a list. + * This method is not thread safe + * @param list Destination list + * @param caps Entity caps to add + */ + virtual void addCaps(NamedList& list, JBEntityCaps& caps); + + /** + * Load (reset) this list from an XML document file. + * This method is thread safe + * @param file The file to load + * @param enabler The debug enabler used to output messages + * @return True on success + */ + bool loadXmlDoc(const char* file, DebugEnabler* enabler = 0); + + /** + * Save this list to an XML document file. + * This method is thread safe + * @param file The file to save + * @param enabler The debug enabler used to output messages + * @return True on success + */ + bool saveXmlDoc(const char* file, DebugEnabler* enabler = 0); + + /** + * Check if an XML element has a 'c' entity capability child and decode it + * @param xml The element to process + * @param version Entity caps version + * @param node Entity node attribute + * @param ver Entity ver attribute + * @param ext Entity ext attribute if version is less the 1.4 + * @return True if a child was succesfully decoded + */ + static bool decodeCaps(const XmlElement& xml, char& version, String*& node, + String*& ver, String*& ext); + + /** + * Enabled flag + */ + bool m_enable; + +protected: + /** + * Caps list item add notification for descendants. + * This method is called when processing responses with the list locked + * @param caps Changed caps object. 0 if none specified + */ + virtual void capsAdded(JBEntityCaps* caps) + {} + + unsigned int m_reqIndex; // Disco info request index + String m_reqPrefix; // Prefix for disco info stanza id + ObjList m_requests; // List of sent disco info requests +}; + +}; // namespace TelEngine + +#endif /* __YATEJABBER_H */ + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/yatejingle.h b/libs/yjabber/yatejingle.h similarity index 71% rename from libs/yjingle/yatejingle.h rename to libs/yjabber/yatejingle.h index a274f77c..6a718b1a 100644 --- a/libs/yjingle/yatejingle.h +++ b/libs/yjabber/yatejingle.h @@ -51,7 +51,7 @@ class JGSentStanza; // Sent stanza timeout info * This class holds a Jingle data payload description * @short A Jingle data payload */ -class YJINGLE_API JGRtpMedia : public GenObject +class YJABBER_API JGRtpMedia : public GenObject { public: /** @@ -59,21 +59,24 @@ public: * @param id The 'id' attribute * @param name The 'name' attribute * @param clockrate The 'clockrate' attribute - * @param channels The 'channels' attribute - * @param synonym The 'synonym' attribute + * @param synonym Application synonym for this payload + * @param channels Optional 'channels' attribute (the number of channels) + * @param pTime Optional "ptime" attribute (packet time) + * @param maxPTime Optional "maxptime" attribute (maximum packet time) */ inline JGRtpMedia(const char* id, const char* name, const char* clockrate, - const char* channels, const char* synonym) + const char* synonym, const char* channels = 0, + const char* pTime = 0, const char* maxPTime = 0) : m_params("") - { set(id,name,clockrate,channels,synonym); } + { set(id,name,clockrate,synonym,channels); } /** * Constructor. Fill this object from an XML element * @param xml The element to fill from */ - inline JGRtpMedia(XMLElement* xml) + inline JGRtpMedia(XmlElement* xml) : m_params("") - { fromXML(xml); } + { fromXml(xml); } /** * Copy constructor @@ -81,23 +84,29 @@ public: inline JGRtpMedia(const JGRtpMedia& src) : GenObject(), m_params(src.m_params) - { set(src.m_id,src.m_name,src.m_clockrate,src.m_channels,src.m_synonym); } + { set(src.m_id,src.m_name,src.m_clockrate,src.m_synonym,src.m_channels); } /** * Set the data * @param id The 'id' attribute * @param name The 'name' attribute * @param clockrate The 'clockrate' attribute - * @param channels The 'channels' attribute - * @param synonym The 'synonym' attribute + * @param synonym Application synonym for this payload + * @param channels Optional 'channels' attribute (the number of channels) + * @param pTime Optional "ptime" attribute (packet time) + * @param maxPTime Optional "maxptime" attribute (maximum packet time) */ inline void set(const char* id, const char* name, const char* clockrate, - const char* channels, const char* synonym) { + const char* synonym = 0, const char* channels = 0, + const char* pTime = 0, const char* maxPTime = 0) { m_id = id; m_name = name; m_clockrate = clockrate; - m_channels = channels; m_synonym = synonym; + m_channels = channels; + m_pTime = pTime; + m_maxPTime = maxPTime; + m_params.clearParams(); } /** @@ -109,15 +118,22 @@ public: /** * Create a 'payload-type' element from this object - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - XMLElement* toXML() const; + XmlElement* toXml() const; /** * Fill this object from a given element * @param xml The element */ - void fromXML(XMLElement* xml); + void fromXml(XmlElement* xml); + + /** + * Build a telephone-event media + * @return JGRtpMedia pointer + */ + static inline JGRtpMedia* telEvent() + { return new JGRtpMedia("106","telephone-event","8000",""); } /** * The numeric id of this payload @@ -134,15 +150,25 @@ public: */ String m_clockrate; + /** + * A synonym of this payload's name + */ + String m_synonym; + /** * The number of channels */ String m_channels; /** - * A synonym of this payload's name + * Packet time */ - String m_synonym; + String m_pTime; + + /** + * Maximum packet time + */ + String m_maxPTime; /** * List of optional parameters @@ -152,10 +178,10 @@ public: /** * This class holds a content description's crypto data. - * The tag is kepti in the String component + * The tag is kept in the String component * @short Content crypto data */ -class YJINGLE_API JGCrypto : public String +class YJABBER_API JGCrypto : public String { public: /** @@ -175,20 +201,36 @@ public: * Constructor. Build this element from a received element * @param xml The received xml element */ - inline JGCrypto(const XMLElement* xml) - { fromXML(xml); } + inline JGCrypto(const XmlElement* xml) + { fromXml(xml); } /** * Create a 'crypto' element from this object - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - XMLElement* toXML() const; + XmlElement* toXml() const; /** * Build this element from a received element * @param xml The received xml element */ - void fromXML(const XMLElement* xml); + void fromXml(const XmlElement* xml); + + /** + * Build an 'encryption' element from a list of crypto objects + * @param list The list of crypto objects + * @param required True if encryption is required + * @return XmlElement pointer or 0 if the list is empty + */ + static XmlElement* buildEncryption(const ObjList& list, bool required); + + /** + * Decode an 'encryption' element. Clear the list before starting + * @param xml The element to decode + * @param list The list to be filled with crypto objects + * @param required Variable to be filled with the value of the 'required' attribute + */ + static void decodeEncryption(const XmlElement* xml, ObjList& list, bool& required); String m_suite; String m_keyParams; @@ -199,7 +241,7 @@ public: * Hold a list of RTP data payloads * @short A List of Jingle RTP data payloads */ -class YJINGLE_API JGRtpMediaList : public ObjList +class YJABBER_API JGRtpMediaList : public ObjList { public: /** @@ -214,12 +256,18 @@ public: /** * Constructor * @param m Media type as enumeration - * @param cryptoMandatory True to require media encryption + * @param cryptoRequired True to require media encryption */ - inline JGRtpMediaList(Media m = MediaMissing, bool cryptoMandatory = false) - : m_media(m), m_cryptoMandatory(cryptoMandatory), m_ready(false) + inline JGRtpMediaList(Media m = MediaMissing, bool cryptoRequired = false) + : m_media(m), m_bandwidth(0), m_cryptoRequired(cryptoRequired), m_ready(false) {} + /** + * Destructor + */ + inline ~JGRtpMediaList() + { TelEngine::destruct(m_bandwidth); } + /** * Get the media type of the payloads owned by this list * @return Media type as enumeration @@ -229,15 +277,23 @@ public: /** * Append a new data payload - * @param id The payload's id - * @param name The payload's name - * @param clockrate The payload's clockrate - * @param bitrate The payload's bitrate - * @param synonym The payload's synonym + * @param id The 'id' attribute + * @param name The 'name' attribute + * @param clockrate The 'clockrate' attribute + * @param synonym Optional application synonym for the payload + * @param channels Optional 'channels' attribute (the number of channels) + * @param pTime Optional "ptime" attribute (packet time) + * @param maxPTime Optional "maxptime" attribute (maximum packet time) */ inline void add(const char* id, const char* name, const char* clockrate, - const char* bitrate, const char* synonym) - { append(new JGRtpMedia(id,name,clockrate,bitrate,synonym)); } + const char* synonym = 0, const char* channels = 0, + const char* pTime = 0, const char* maxPTime = 0) + { append(new JGRtpMedia(id,name,clockrate,synonym,channels,pTime,maxPTime)); } + + /** + * Reset the list and data + */ + void reset(); /** * Find a data payload by its id @@ -256,15 +312,15 @@ public: /** * Create a 'description' element and add payload children to it * @param telEvent True to append a telephone event data payload - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - XMLElement* toXML(bool telEvent = true) const; + XmlElement* toXml(bool telEvent = true) const; /** * Fill this list from an XML element's children. Clear before attempting to fill * @param xml The source XML element */ - void fromXML(XMLElement* xml); + void fromXml(XmlElement* xml); /** * Create a list from data payloads @@ -278,17 +334,28 @@ public: /** * The list of media type names */ - static TokenDict s_media[]; + static const TokenDict s_media[]; /** * The media type */ Media m_media; + /** + * Synchronization source + */ + String m_ssrc; + + /** + * Optional SDP media bandwith. The name of the string keeps the type ('bwtype') + * and its value keeps the actual bandwith + */ + NamedString* m_bandwidth; + /** * Crypto (SRTP) params */ - bool m_cryptoMandatory; + bool m_cryptoRequired; ObjList m_cryptoLocal; ObjList m_cryptoRemote; @@ -321,22 +388,22 @@ public: * @param xml Received xml element * @param container The transport container */ - inline JGRtpCandidate(XMLElement* xml, const JGRtpCandidates& container) + inline JGRtpCandidate(XmlElement* xml, const JGRtpCandidates& container) { fromXml(xml,container); } /** * Create a 'candidate' element from this object using local address/port * @param container The transport container - * @return Valid XMLElement pointer if type is a known one + * @return Valid XmlElement pointer if type is a known one */ - XMLElement* toXml(const JGRtpCandidates& container) const; + XmlElement* toXml(const JGRtpCandidates& container) const; /** * Fill this object from a candidate element using remote address/port * @param xml Received xml element * @param container The transport container */ - void fromXml(XMLElement* xml, const JGRtpCandidates& container); + void fromXml(XmlElement* xml, const JGRtpCandidates& container); String m_address; String m_port; @@ -353,7 +420,7 @@ public: * This class holds a list of jingle RTP transport candidates * @short A list of RTP transport candidates */ -class YJINGLE_API JGRtpCandidates : public ObjList +class YJABBER_API JGRtpCandidates : public ObjList { public: /** @@ -407,15 +474,15 @@ public: * Create a 'transport' element from this object. Add candidates * @param addCandidates True to add the candidate children * @param addAuth RtpIceUdp only: add auth data - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - XMLElement* toXML(bool addCandidates, bool addAuth) const; + XmlElement* toXml(bool addCandidates, bool addAuth) const; /** * Fill this object from a given element * @param element The element */ - void fromXML(XMLElement* element); + void fromXml(XmlElement* element); /** * Generate a random password or username to be used with ICE-UDP transport @@ -444,7 +511,7 @@ public: /** * The list of type names */ - static TokenDict s_type[]; + static const TokenDict s_type[]; Type m_type; String m_password; @@ -457,7 +524,7 @@ public: * it can build an xml element from itself * @short A Jingle session content */ -class YJINGLE_API JGSessionContent : public RefObject +class YJABBER_API JGSessionContent : public RefObject { public: /** @@ -565,9 +632,9 @@ public: * @param addTrans True to add the transport child * @param addCandidates True to add the transport candidate children * @param addAuth RtpIceUdp only: add auth data - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - XMLElement* toXml(bool minimum, bool addDesc, + XmlElement* toXml(bool minimum, bool addDesc, bool addTrans, bool addCandidates, bool addAuth) const; /** @@ -577,18 +644,18 @@ public: * @param error Error text to be sent on failure * @return Valid JGSessionContent pointer on success */ - static JGSessionContent* fromXml(XMLElement* xml, XMPPError::Type& err, + static JGSessionContent* fromXml(XmlElement* xml, XMPPError::Type& err, String& error); /** * The list containing the text values for Senders enumeration */ - static TokenDict s_senders[]; + static const TokenDict s_senders[]; /** * The list containing the text values for Creator enumeration */ - static TokenDict s_creator[]; + static const TokenDict s_creator[]; /** * The RTP media description if used @@ -650,33 +717,33 @@ public: /** * Build an XML element from this stream host - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - XMLElement* toXml(); + XmlElement* toXml(); /** * Build a stream host from an XML element * @param xml The element to build from * @return Valid JGStreamHost pointer or 0 on error */ - static JGStreamHost* fromXml(XMLElement* xml); + static JGStreamHost* fromXml(XmlElement* xml); /** * Build a query XML element carrying a list of stream hosts * @param hosts List of JGStreamHost objects * @param sid The query element's sid attribute * @param mode The query element's mode attribute - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - static XMLElement* buildHosts(const ObjList& hosts, const char* sid, + static XmlElement* buildHosts(const ObjList& hosts, const char* sid, const char* mode = "tcp"); /** * Build a query XML element with a streamhost-used child * @param jid The jid of the stream host used - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - static XMLElement* buildRsp(const char* jid); + static XmlElement* buildRsp(const char* jid); String m_address; int m_port; @@ -688,7 +755,7 @@ public: * This class is a base class for all specific jingle sessions * @short A basic Jingle session */ -class YJINGLE_API JGSession : public RefObject, public Mutex +class YJABBER_API JGSession : public RefObject, public Mutex { friend class JGEvent; friend class JGEngine; @@ -703,21 +770,43 @@ public: }; /** - * Jingle defined termination reasons + * Jingle defined reasons and errors */ enum Reason { - ReasonBusy, // - ReasonDecline, // - ReasonConn, // - ReasonMedia, // - ReasonTransport, // - ReasonNoError, // - ReasonOk, // - ReasonNoApp, // - ReasonAltSess, // - ReasonUnknown, // - ReasonTransfer, // - ReasonNone // None of the above + ReasonUnknown = 0, + // Session termination reason + ReasonOk, // success + ReasonBusy, // busy + ReasonDecline, // decline + ReasonCancel, // cancel + ReasonExpired, // expired + ReasonConn, // connectivity-error + ReasonFailApp, // failed-application + ReasonFailTransport, // failed-transport + ReasonGone, // gone + ReasonParams, // incompatible-parameters + ReasonMedia, // media-error + ReasonTransport, // unsupported-transports + ReasonApp, // unsupported-applications + ReasonSecurity, // security-error + ReasonTimeout, // timeout + ReasonGeneral, // general-error + ReasonAltSess, // alternative-session + // Session transfer (XEP 0251) + Transferred, // transferred + // RTP session errors (XEP 0167) + CryptoRequired, // crypto-required + InvalidCrypto, // invalid-crypto + }; + + /** + * RTP session info (XEP 0167) + */ + enum RtpInfo { + RtpActive, // active + RtpHold, // hold + RtpMute, // mute + RtpRinging, // ringing }; /** @@ -738,17 +827,20 @@ public: ActAccept, // session-accept ActInitiate, // session-initiate ActTerminate, // session-terminate + ActReject, // reject ActInfo, // session-info ActTransportInfo, // transport-info ActTransportAccept, // transport-accept ActTransportReject, // transport-reject ActTransportReplace, // transport-replace + ActCandidates, // candidates ActContentAccept, // content-accept ActContentAdd, // content-add ActContentModify, // content-modify ActContentReject, // content-reject ActContentRemove, // content-remove ActContentInfo, // content-info + ActDescriptionInfo, // description-info ActTransfer, // session-info: Transfer ActRinging, // session-info: Ringing ActTrying, // session-info: Trying @@ -773,6 +865,13 @@ public: inline Version version() const { return m_version; } + /** + * Retrieve the engine owning this session + * @return The engine owning this session + */ + inline JGEngine* engine() const + { return m_engine; } + /** * Get the session direction * @return True if it is an outgoing session @@ -792,14 +891,14 @@ public: * @return The local peer's JID */ inline const JabberID& local() const - { return m_localJID; } + { return m_local; } /** * Get the remote peer's JID * @return The remote peer's JID */ inline const JabberID& remote() const - { return m_remoteJID; } + { return m_remote; } /** * Get the session state. @@ -808,13 +907,6 @@ public: inline State state() const { return m_state; } - /** - * Get the stream this session is bound to - * @return The stream this session is bound to - */ - inline const JBStream* stream() const - { return m_stream; } - /** * Get the arbitrary user data of this session * @return The arbitrary user data of this session @@ -829,41 +921,42 @@ public: inline void userData(void* userdata) { m_private = userdata; } - /** - * Check if a given XML element is valid jingle one - * @param xml Element to check - * @return The given element if it's a valid jingle element, 0 otherwise - */ - virtual XMLElement* checkJingle(XMLElement* xml) - { return 0; } - /** * Get an action (jingle element type) from a jingle element * @param xml Element to check * @return The found action, ActCount if not found or unknown */ - inline Action getAction(XMLElement* xml) - { return xml ? lookupAction(xml->getAttribute("type"),m_version) : ActCount; } + inline Action getAction(XmlElement* xml) + { return xml ? lookupAction(xml->attribute("type"),m_version) : ActCount; } /** - * Ask this session to accept an event - * @param event The event to accept - * @param sid The session id if this is a request - * @return True if accepted (the event was enqueued), false if not + * Ask this session to accept an incoming xml 'iq' element + * @param type Iq type as enumeration + * @param from The sender + * @param to The recipient + * @param id The session id of this is a request (set/get) or the stanza id + * @param xml The received element + * @return True if accepted (the element was enqueued), false if not */ - bool acceptEvent(JBEvent* event, const String& sid = String::empty()); + bool acceptIq(XMPPUtils::IqType type, const JabberID& from, const JabberID& to, + const String& id, XmlElement* xml); /** - * Confirm a received element. If the error is NoError a result stanza will be sent. - * Otherwise, an error stanza will be created and sent and the received element is - * consumed (attached to the sent error stanza) + * Confirm (send result) a received element * @param xml The element to confirm + * @return False if send failed or element is 0 + */ + bool confirmResult(XmlElement* xml); + + /** + * Confirm (send error) a received element + * @param xml The element to confirm (will be consumed and zeroed) * @param error The error condition * @param text Optional text to add to the error element * @param type Error type * @return False if send failed or element is 0 */ - bool confirm(XMLElement* xml, XMPPError::Type error = XMPPError::NoError, + bool confirmError(XmlElement*& xml, XMPPError::Type error, const char* text = 0, XMPPError::ErrorType type = XMPPError::TypeModify); /** @@ -879,24 +972,44 @@ public: /** * Close a Pending or Active session * This method is thread safe - * @param reason Termination reason - * @param msg Optional termination message + * @param reason Optional termination reason * @return False if send failed */ - virtual bool hangup(int reason, const char* msg = 0); + virtual bool hangup(XmlElement* reason = 0); /** - * Create a 'hold' child to be added to a session-info element - * @return Valid XMLElement pointer or 0 + * Create a RTP info child to be added to a session-info element + * @param info The informational tag as enumeration + * @return Valid XmlElement pointer or 0 if not supported */ - virtual XMLElement* createHoldXml() + virtual XmlElement* createRtpInfoXml(RtpInfo info) { return 0; } /** - * Create an 'active' child to be added to a session-info element - * @return Valid XMLElement pointer or 0 + * Create a termination reason element + * @param reason The reason code + * @param text Optional reason text child + * @param child Optional additional reason child + * @return Valid XmlElement pointer or 0 if not supported */ - virtual XMLElement* createActiveXml() + virtual XmlElement* createReason(int reason, const char* text = 0, + XmlElement* child = 0) + { return 0; } + + /** + * Create a transfer reason element + * @param reason The reason code + * @return Valid XmlElement pointer or 0 if not supported + */ + virtual XmlElement* createTransferReason(int reason) + { return 0; } + + /** + * Create a RTP session reason element + * @param reason The reason code + * @return Valid XmlElement pointer or 0 if not supported + */ + virtual XmlElement* createRtpSessionReason(int reason) { return 0; } /** @@ -960,11 +1073,11 @@ public: /** * Send a session info element to the remote peer. * This method is thread safe - * @param xml The XMLElement carried by the session info element + * @param xml The XmlElement carried by the session info element * @param stanzaId Optional string to be filled with sent stanza id (used to track the response) * @return False on failure */ - bool sendInfo(XMLElement* xml, String* stanzaId = 0); + bool sendInfo(XmlElement* xml, String* stanzaId = 0); /** * Send a dtmf string to remote peer. If the string's length is greater then 1, each @@ -976,17 +1089,6 @@ public: */ bool sendDtmf(const char* dtmf, unsigned int msDuration = 0, String* stanzaId = 0); - /** - * Send a message to the remote peer. - * This method is thread safe - * @param msg The message to send - * @return False on socket error - */ - inline bool sendMessage(const char* msg) { - return sendStanza(JBMessage::createMessage(JBMessage::Chat, - m_localJID,m_remoteJID,0,msg),0,false); - } - /** * Check if the remote party supports a given feature * @param feature The requested feature @@ -999,9 +1101,9 @@ public: * @param transferTo The JID to transfer to * @param transferFrom The transferror's JID * @param sid Optional session id used for attended transfer (empty for unattended transfer) - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - static XMLElement* buildTransfer(const String& transferTo, const String& transferFrom, + static XmlElement* buildTransfer(const String& transferTo, const String& transferFrom, const String& sid = String::empty()); /** @@ -1067,50 +1169,55 @@ public: /** * Session version names */ - static TokenDict s_versions[]; + static const TokenDict s_versions[]; /** - * Termination reasons + * Termination reasons and errors */ - static TokenDict s_reasons[]; + static const TokenDict s_reasons[]; + + /** + * RTP session info (XEP 0167) + */ + static const TokenDict s_rtpInfo[]; /** * Session state names */ - static TokenDict s_states[]; + static const TokenDict s_states[]; /** * Action names for version Version0 */ - static TokenDict s_actions0[]; + static const TokenDict s_actions0[]; /** * Action names for version Version1 */ - static TokenDict s_actions1[]; + static const TokenDict s_actions1[]; protected: /** * Constructor. Create an outgoing session * @param ver The session version - * @param engine The engine that owns this session - * @param stream The stream this session is bound to - * @param callerJID The caller's full JID - * @param calledJID The called party's full JID - * @param msg Optional message to be sent before session initiate + * @param engine The engine owning this session + * @param caller The caller's full JID + * @param called The called party's full JID */ - JGSession(Version ver, JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, - const char* msg = 0); + JGSession(Version ver, JGEngine* engine, + const JabberID& caller, const JabberID& called); /** * Constructor. Create an incoming session. * @param ver The session version - * @param engine The engine that owns this session - * @param event A valid Jabber Jingle event with action session initiate + * @param engine The engine owning this session + * @param caller The caller's full JID + * @param called The called party's full JID + * @param xml A valid Jabber Jingle xml with action session initiate * @param id Session id */ - JGSession(Version ver, JGEngine* engine, JBEvent* event, const String& id); + JGSession(Version ver, JGEngine* engine, const JabberID& caller, + const JabberID& called, XmlElement* xml, const String& id); /** * Build and send the initial message on an outgoing session @@ -1119,7 +1226,7 @@ protected: * @param subject Optional session subject * @return True on success */ - virtual bool initiate(const ObjList& contents, XMLElement* extra, + virtual bool initiate(const ObjList& contents, XmlElement* extra, const char* subject = 0) = 0; /** @@ -1135,13 +1242,6 @@ protected: */ virtual void destroyed(); - /** - * Enqueue a Jabber engine event. - * This method is thread safe - * @param event The event event to process - */ - void enqueue(JBEvent* event); - /** * Send a stanza to the remote peer * @param stanza The stanza to send @@ -1150,7 +1250,7 @@ protected: * @param ping True if the stanza is a ping one * @return True on success */ - bool sendStanza(XMLElement* stanza, String* stanzaId = 0, bool confirmation = true, + bool sendStanza(XmlElement* stanza, String* stanzaId = 0, bool confirmation = true, bool ping = false); /** @@ -1161,11 +1261,12 @@ protected: bool sendPing(u_int64_t msecNow); /** - * Decode a valid jingle set event. Set the event's data on success - * @param jbev The event to decode + * Decode a jingle element + * @param xml The element to decode + * @param child The element's first child * @return JGEvent pointer or 0 */ - virtual JGEvent* decodeJingle(JBEvent* jbev) = 0; + virtual JGEvent* decodeJingle(XmlElement*& xml, XmlElement* child) = 0; /** * Create an 'iq' of type 'set' with a 'jingle' child @@ -1173,38 +1274,43 @@ protected: * @param element1 Optional child element * @param element2 Optional child element * @param element3 Optional child element - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - virtual XMLElement* createJingle(Action action, XMLElement* element1 = 0, - XMLElement* element2 = 0, XMLElement* element3 = 0) = 0; + virtual XmlElement* createJingle(Action action, XmlElement* element1 = 0, + XmlElement* element2 = 0, XmlElement* element3 = 0) = 0; /** * Create a dtmf XML element * @param dtmf The dtmf string * @param msDuration The tone duration in miliseconds. Ignored if 0 - * @return Valid XMLElement pointer or 0 + * @return Valid XmlElement pointer or 0 */ - virtual XMLElement* createDtmf(const char* dtmf, unsigned int msDuration = 0) = 0; + virtual XmlElement* createDtmf(const char* dtmf, unsigned int msDuration = 0) = 0; /** - * Method called in getEvent() to process a last event set from a - * jingle set jabber event - * @param ev The event to process + * Method called in getEvent() to process a last event decoded from a + * received jingle element + * @param ev The event to process (will be consumed and zeroed) + * @return JGEvent pointer or 0 */ - virtual void processJingleSetLastEvent(JBEvent& ev); + virtual JGEvent* processJingleSetEvent(JGEvent*& ev); /** * Method called in getEvent() to process a jabber event carrying a response - * @param ev The event to process - * @return False to stop further processing + * @param result True if the element is a result, false if it's an error response + * @param xml Xml element to process + * @return JGEvent pointer or 0 */ - virtual bool processJabberIqResponse(JBEvent& ev); + virtual JGEvent* processJabberIqResponse(bool result, XmlElement*& xml); /** - * Method called in getEvent() to process a generic jabber iq event - * @param ev The event to process + * Decode a file transfer element + * @param set True if the xml is an iq 'set', false if type is 'get' + * @param xml The element to decode + * @param child The element's first child + * @return JGEvent pointer or 0 */ - virtual void processJabberIqEvent(JBEvent& ev); + virtual JGEvent* processFileTransfer(bool set, XmlElement*& xml, XmlElement* child); /** * Terminate notification from an event. Reset the last generated event @@ -1219,23 +1325,17 @@ protected: void changeState(State newState); Version m_version; // Session version - // State info State m_state; // Session state u_int64_t m_timeToPing; // Time to send ping (empty session-info) - // Links JGEngine* m_engine; // The engine that owns this session - JBStream* m_stream; // The stream this session is bound to - // Session info bool m_outgoing; // Session direction String m_sid; // Session id - JabberID m_localJID; // Local peer's JID - JabberID m_remoteJID; // Remote peer's JID - // Session data - ObjList m_events; // Incoming events from Jabber engine + JabberID m_local; // Local peer's JID + JabberID m_remote; // Remote peer's JID + XmlFragment m_queue; // Incoming, unprocessed, xml elements JGEvent* m_lastEvent; // Last generated event bool m_recvTerminate; // Flag indicating whether session-terminate was received void* m_private; // Arbitrary user data - // Sent stanzas id generation String m_localSid; // Local session id (used to generate element's id) u_int32_t m_stanzaId; // Sent stanza id counter ObjList m_sentStanza; // Sent stanzas' id @@ -1249,7 +1349,7 @@ private: * A session implementing the old jingle protocol * @short The version 0 of a jingle session */ -class YJINGLE_API JGSession0 : public JGSession +class YJABBER_API JGSession0 : public JGSession { friend class JGEvent; friend class JGEngine; @@ -1259,13 +1359,6 @@ public: */ virtual ~JGSession0(); - /** - * Check if a given XML element is valid jingle one - * @param xml Element to check - * @return The given element if it's a valid jingle element, 0 otherwise - */ - virtual XMLElement* checkJingle(XMLElement* xml); - /** * Accept a Pending incoming session. * This method is thread safe @@ -1278,22 +1371,22 @@ public: protected: /** * Constructor. Create an outgoing session - * @param engine The engine that owns this session - * @param stream The stream this session is bound to - * @param callerJID The caller's full JID - * @param calledJID The called party's full JID - * @param msg Optional message to be sent before session initiate + * @param engine The engine owning this session + * @param caller The caller's full JID + * @param called The called party's full JID */ - JGSession0(JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, const char* msg = 0); + JGSession0(JGEngine* engine, const JabberID& caller, const JabberID& called); /** * Constructor. Create an incoming session. - * @param engine The engine that owns this session - * @param event A valid Jabber Jingle event with action session initiate + * @param engine The engine owning this session + * @param caller The caller's full JID + * @param called The called party's full JID + * @param xml A valid Jabber Jingle xml with action session initiate * @param id Session id */ - JGSession0(JGEngine* engine, JBEvent* event, const String& id); + JGSession0(JGEngine* engine, const JabberID& caller, const JabberID& called, + XmlElement* xml, const String& id); /** * Build and send the initial message on an outgoing session @@ -1302,7 +1395,7 @@ protected: * @param subject Optional session subject * @return True on success */ - virtual bool initiate(const ObjList& contents, XMLElement* extra, + virtual bool initiate(const ObjList& contents, XmlElement* extra, const char* subject = 0); /** @@ -1317,11 +1410,12 @@ protected: virtual bool sendContent(Action action, const ObjList& contents, String* stanzaId = 0); /** - * Decode a valid jingle set event. Set the event's data on success - * @param jbev The event to decode + * Decode a jingle element + * @param xml The element to decode + * @param child The element's first child * @return JGEvent pointer or 0 */ - virtual JGEvent* decodeJingle(JBEvent* jbev); + virtual JGEvent* decodeJingle(XmlElement*& xml, XmlElement* child); /** * Create an 'iq' of type 'set' with a 'jingle' child @@ -1329,28 +1423,29 @@ protected: * @param element1 Optional child element * @param element2 Optional child element * @param element3 Optional child element - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - virtual XMLElement* createJingle(Action action, XMLElement* element1 = 0, - XMLElement* element2 = 0, XMLElement* element3 = 0); + virtual XmlElement* createJingle(Action action, XmlElement* element1 = 0, + XmlElement* element2 = 0, XmlElement* element3 = 0); /** * Create a dtmf XML element * @param dtmf The dtmf string * @param msDuration The tone duration in miliseconds. Ignored if 0 - * @return Valid XMLElement pointer or 0 + * @return Valid XmlElement pointer or 0 */ - virtual XMLElement* createDtmf(const char* dtmf, unsigned int msDuration = 0); + virtual XmlElement* createDtmf(const char* dtmf, unsigned int msDuration = 0); protected: String m_sessContentName; // Content name advertised to upper layer + Action m_candidatesAction; // Use candidates/transport-info for candidates }; /** * A session implementing the Jingle protocol including session transfer and file transfer * @short The version 1 of a jingle session */ -class YJINGLE_API JGSession1 : public JGSession +class YJABBER_API JGSession1 : public JGSession { friend class JGEvent; friend class JGEngine; @@ -1360,13 +1455,6 @@ public: */ virtual ~JGSession1(); - /** - * Check if a given XML element is valid jingle one - * @param xml Element to check - * @return The given element if it's a valid jingle element, 0 otherwise - */ - virtual XMLElement* checkJingle(XMLElement* xml); - /** * Accept a Pending incoming session. * This method is thread safe @@ -1377,16 +1465,35 @@ public: virtual bool accept(const ObjList& contents, String* stanzaId = 0); /** - * Create a 'hold' child to be added to a session-info element - * @return Valid XMLElement pointer or 0 + * Create a RTP info child to be added to a session-info element + * @param info The informational tag as enumeration + * @return Valid XmlElement pointer or 0 if not supported */ - virtual XMLElement* createHoldXml(); + virtual XmlElement* createRtpInfoXml(RtpInfo info); /** - * Create an 'active' child to be added to a session-info element - * @return Valid XMLElement pointer or 0 + * Create a termination reason element + * @param reason The reason code + * @param text Optional reason text child + * @param child Optional additional reason child + * @return Valid XmlElement pointer or 0 if not supported */ - virtual XMLElement* createActiveXml(); + virtual XmlElement* createReason(int reason, const char* text = 0, + XmlElement* child = 0); + + /** + * Create a transfer reason element + * @param reason The reason code + * @return Valid XmlElement pointer or 0 if not supported + */ + virtual XmlElement* createTransferReason(int reason); + + /** + * Create a RTP session reason element + * @param reason The reason code + * @return Valid XmlElement pointer or 0 if not supported + */ + virtual XmlElement* createRtpSessionReason(int reason); /** * Send a stanza with session content(s) @@ -1422,22 +1529,22 @@ public: protected: /** * Constructor. Create an outgoing session - * @param engine The engine that owns this session - * @param stream The stream this session is bound to - * @param callerJID The caller's full JID - * @param calledJID The called party's full JID - * @param msg Optional message to be sent before session initiate + * @param engine The engine owning this session + * @param caller The caller's full JID + * @param called The called party's full JID */ - JGSession1(JGEngine* engine, JBStream* stream, - const String& callerJID, const String& calledJID, const char* msg = 0); + JGSession1(JGEngine* engine, const JabberID& caller, const JabberID& called); /** * Constructor. Create an incoming session. - * @param engine The engine that owns this session - * @param event A valid Jabber Jingle event with action session initiate + * @param engine The engine owning this session + * @param caller The caller's full JID + * @param called The called party's full JID + * @param xml A valid Jabber Jingle xml with action session initiate * @param id Session id */ - JGSession1(JGEngine* engine, JBEvent* event, const String& id); + JGSession1(JGEngine* engine, const JabberID& caller, const JabberID& called, + XmlElement* xml, const String& id); /** * Build and send the initial message on an outgoing session @@ -1446,15 +1553,16 @@ protected: * @param subject Optional session subject * @return True on success */ - virtual bool initiate(const ObjList& contents, XMLElement* extra, + virtual bool initiate(const ObjList& contents, XmlElement* extra, const char* subject = 0); /** - * Decode a valid jingle set event. Set the event's data on success - * @param jbev The event to decode + * Decode a jingle element + * @param xml The element to decode + * @param child The element's first child * @return JGEvent pointer or 0 */ - virtual JGEvent* decodeJingle(JBEvent* jbev); + virtual JGEvent* decodeJingle(XmlElement*& xml, XmlElement* child); /** * Create an 'iq' of type 'set' with a 'jingle' child @@ -1462,23 +1570,27 @@ protected: * @param element1 Optional child element * @param element2 Optional child element * @param element3 Optional child element - * @return Valid XMLElement pointer + * @return Valid XmlElement pointer */ - virtual XMLElement* createJingle(Action action, XMLElement* element1 = 0, - XMLElement* element2 = 0, XMLElement* element3 = 0); + virtual XmlElement* createJingle(Action action, XmlElement* element1 = 0, + XmlElement* element2 = 0, XmlElement* element3 = 0); /** * Create a dtmf XML element * @param dtmf The dtmf string * @param msDuration The tone duration in miliseconds. Ignored if 0 - * @return Valid XMLElement pointer or 0 + * @return Valid XmlElement pointer or 0 */ - virtual XMLElement* createDtmf(const char* dtmf, unsigned int msDuration = 0); + virtual XmlElement* createDtmf(const char* dtmf, unsigned int msDuration = 0); /** - * @param ev The event to process + * Decode a file transfer element + * @param set True if the xml is an iq 'set', false if type is 'get' + * @param xml The element to decode + * @param child The element's first child + * @return JGEvent pointer or 0 */ - virtual void processJabberIqEvent(JBEvent& ev); + virtual JGEvent* processFileTransfer(bool set, XmlElement*& xml, XmlElement* child); }; @@ -1486,7 +1598,7 @@ protected: * This class holds an event generated by a Jingle session * @short A Jingle event */ -class YJINGLE_API JGEvent +class YJABBER_API JGEvent { friend class JGSession; friend class JGSession0; @@ -1499,7 +1611,6 @@ public: Jingle, // ResultOk, // Response for a sent stanza (iq with type=result) ResultError, // Response for a sent stanza (iq with type=error) - ResultWriteFail, // Response for a sent stanza (failed to send stanza) ResultTimeout, // Response for a sent stanza (stanza timeout) // Final Terminated, // m_element is the element that caused the termination @@ -1537,7 +1648,7 @@ public: * Get the XML element that generated this event * @return The XML element that generated this event */ - inline XMLElement* element() const + inline XmlElement* element() const { return m_element; } /** @@ -1545,7 +1656,7 @@ public: * Don't delete it after use: it is owned by the event * @return The Jingle child of the XML element carried by the event */ - inline XMLElement* jingle() const + inline XmlElement* jingle() const { return m_jingle; } /** @@ -1587,10 +1698,9 @@ public: * Get the XML element that generated this event and set it to 0 * @return The XML element that generated this event */ - inline XMLElement* releaseXML() { - TelEngine::destruct(m_jingle); - XMLElement* tmp = m_element; - m_element = 0; + inline XmlElement* releaseXml() { + XmlElement* tmp = m_element; + m_jingle = m_element = 0; return tmp; } @@ -1613,8 +1723,9 @@ public: if (m_session && element() && !m_confirmed) { m_confirmed = true; if (error == XMPPError::NoError) - return m_session->confirm(element()); - return m_session->confirm(releaseXML(),error,text,type); + return m_session->confirmResult(element()); + XmlElement* err = releaseXml(); + return m_session->confirmError(err,error,text,type); } return false; } @@ -1642,7 +1753,7 @@ public: /** * Dictionary with event type names */ - static TokenDict s_typeName[]; + static const TokenDict s_typeName[]; /** * The list of session contents if used @@ -1663,7 +1774,7 @@ protected: * @param reason Optional reason data * @param text Optional text data */ - inline JGEvent(Type type, JGSession* session, XMLElement* element = 0, + inline JGEvent(Type type, JGSession* session, XmlElement* element = 0, const char* reason = 0, const char* text = 0) : m_type(type), m_confirmed(true), m_session(0), m_element(element), m_jingle(0), m_action(JGSession::ActCount), m_reason(reason), m_text(text) @@ -1677,7 +1788,7 @@ protected: * @param reason Optional reason data * @param text Optional text data */ - inline JGEvent(JGSession::Action act, JGSession* session, XMLElement* element, + inline JGEvent(JGSession::Action act, JGSession* session, XmlElement* element, const char* reason = 0, const char* text = 0) : m_type(Jingle), m_confirmed(false), m_session(0), m_element(element), m_jingle(0), m_action(act), m_reason(reason), m_text(text) { @@ -1692,8 +1803,8 @@ private: Type m_type; // The type of this event bool m_confirmed; // Flag indicating that element was confirmed JGSession* m_session; // Jingle session that generated this event - XMLElement* m_element; // XML element that generated this event - XMLElement* m_jingle; // The session child, if present + XmlElement* m_element; // XML element that generated this event + XmlElement* m_jingle; // The session child, if present // Event specific JGSession::Action m_action; // The action if type is Jingle String m_id; // The element's id attribute @@ -1706,17 +1817,15 @@ private: * stanza write fail events and stream termination events * @short A Jingle engine */ -class YJINGLE_API JGEngine : public JBService, public JBThreadList +class YJABBER_API JGEngine : public DebugEnabler, public Mutex { friend class JGSession; public: /** - * Constructor. Constructs a Jingle service - * @param engine The Jabber engine - * @param params Service's parameters - * @param prio The priority of this service + * Constructor + * @param name Debug name */ - JGEngine(JBEngine* engine, const NamedList* params, int prio = 0); + JGEngine(const char* name = "jgengine"); /** * Destructor. Terminates all active sessions @@ -1743,6 +1852,23 @@ public: */ virtual void initialize(const NamedList& params); + /** + * Send a session's stanza. + * This method should be re-implemented + * @param session The session requesting the operation + * @param stanza The stanza to send. Will be consumed and zeroed + * @return True on success + */ + virtual bool sendStanza(JGSession* session, XmlElement*& stanza); + + /** + * Send a chat message on behalf of a session + * @param session The session requesting the operation + * @param body Message body + * @return True on success + */ + virtual bool sendMessage(JGSession* session, const char* body); + /** * Call getEvent() for each session list until an event is generated or the end is reached * This method is thread safe @@ -1755,18 +1881,33 @@ public: * Make an outgoing call. * 'media' and 'transport' will be invalid on exit. Don't delete them * @param ver The session version to use - * @param callerName The local peer's username - * @param remoteJID The remote peer's JID + * @param caller The caller + * @param called The called * @param contents The list of session content(s) * @param extra Optional extra child for session initiate element * @param msg Optional message to send before call * @param subject Optional session subject * @return Valid JGSession pointer (referenced) on success */ - JGSession* call(JGSession::Version ver, const String& callerName, const String& remoteJID, - const ObjList& contents, XMLElement* extra = 0, const char* msg = 0, + JGSession* call(JGSession::Version ver, const JabberID& caller, const JabberID& called, + const ObjList& contents, XmlElement* extra = 0, const char* msg = 0, const char* subject = 0); + /** + * Ask this engine to accept an incoming xml 'iq' element + * @param type Iq type as enumeration + * @param from The sender + * @param to The recipient + * @param id Element id attribute + * @param xml The received element + * @param error XMPPError result. This value should be check if false is returned. + * Any value different from NoError indicate an invalid element + * @param text Error text + * @return True if accepted (don't use the given pointer if accepted) + */ + bool acceptIq(XMPPUtils::IqType type, const JabberID& from, const JabberID& to, + const String& id, XmlElement* xml, XMPPError::Type& error, String& text); + /** * Default event processor. Delete event. * @param event The event to process @@ -1780,22 +1921,11 @@ public: */ virtual void processEvent(JGEvent* event); -protected: - /** - * Accept an event from the Jabber engine - * @param event The event to accept - * @param processed Set to true on exit to signal that the event was already processed - * @param insert Set to true if accepted to insert on top of the event queue - * @return False if not accepted, let the engine try another service - */ - virtual bool accept(JBEvent* event, bool& processed, bool& insert); - private: // Create a local session id void createSessionId(String& id); ObjList m_sessions; // List of sessions - Mutex m_sessionIdMutex; // Session id counter lock u_int32_t m_sessionId; // Session id counter u_int64_t m_stanzaTimeout; // The timeout of a sent stanza u_int64_t m_pingInterval; // Interval to send ping (empty session-info) @@ -1806,7 +1936,7 @@ private: * This class holds sent stanzas info used for timeout checking * @short Send stanza timeout info */ -class YJINGLE_API JGSentStanza : public String +class YJABBER_API JGSentStanza : public String { public: /** diff --git a/libs/yjingle/.cvsignore b/libs/yjingle/.cvsignore deleted file mode 100644 index efbd82da..00000000 --- a/libs/yjingle/.cvsignore +++ /dev/null @@ -1,8 +0,0 @@ -Makefile -YateLocal* -core* -*.o -*.a -*.orig -*~ -.*.swp diff --git a/libs/yjingle/jbengine.cpp b/libs/yjingle/jbengine.cpp deleted file mode 100644 index 84ac2213..00000000 --- a/libs/yjingle/jbengine.cpp +++ /dev/null @@ -1,2519 +0,0 @@ -/** - * jbengine.cpp - * Yet Another Jabber Component Protocol Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include - -using namespace TelEngine; - -TokenDict JBEvent::s_type[] = { - {"Terminated", Terminated}, - {"Destroy", Destroy}, - {"Running", Running}, - {"WriteFail", WriteFail}, - {"Presence", Presence}, - {"Message", Message}, - {"Iq", Iq}, - {"IqError", IqError}, - {"IqResult", IqResult}, - {"IqDiscoInfoGet", IqDiscoInfoGet}, - {"IqDiscoInfoSet", IqDiscoInfoSet}, - {"IqDiscoInfoRes", IqDiscoInfoRes}, - {"IqDiscoInfoErr", IqDiscoInfoErr}, - {"IqDiscoItemsGet", IqDiscoItemsGet}, - {"IqDiscoItemsSet", IqDiscoItemsSet}, - {"IqDiscoItemsRes", IqDiscoItemsRes}, - {"IqDiscoItemsErr", IqDiscoItemsErr}, - {"IqCommandGet", IqCommandGet}, - {"IqCommandSet", IqCommandSet}, - {"IqCommandRes", IqCommandRes}, - {"IqCommandErr", IqCommandErr}, - {"IqJingleGet", IqJingleGet}, - {"IqJingleSet", IqJingleSet}, - {"IqJingleRes", IqJingleRes}, - {"IqJingleErr", IqJingleErr}, - {"IqRosterSet", IqRosterSet}, - {"IqRosterRes", IqRosterRes}, - {"IqRosterErr", IqRosterErr}, - {"IqClientRosterUpdate", IqClientRosterUpdate}, - {"Unhandled", Unhandled}, - {"Invalid", Invalid}, - {0,0} -}; - -TokenDict JBEngine::s_protoName[] = { - {"component", Component}, - {"client", Client}, - {0,0} -}; - -static TokenDict s_serviceType[] = { - {"jingle", JBEngine::ServiceJingle}, - {"iq", JBEngine::ServiceIq}, - {"message", JBEngine::ServiceMessage}, - {"presence", JBEngine::ServicePresence}, - {"command", JBEngine::ServiceCommand}, - {"disco", JBEngine::ServiceDisco}, - {"stream", JBEngine::ServiceStream}, - {"write-fail", JBEngine::ServiceWriteFail}, - {"roster", JBEngine::ServiceRoster}, - {0,0} -}; - -static TokenDict s_threadNames[] = { - {"Jabber Connect", JBThread::StreamConnect}, - {"Jabber Receive", JBThread::EngineReceive}, - {"Jabber Process", JBThread::EngineProcess}, - {"Jabber Presence", JBThread::Presence}, - {"Jabber Messages", JBThread::Message}, - {"Jingle Events", JBThread::Jingle}, - {0,0} -}; - -TokenDict JBMessage::s_msg[] = { - {"chat", Chat}, - {"groupchat", GroupChat}, - {"headline", HeadLine}, - {"normal", Normal}, - {"error", Error}, - {0,0} -}; - -TokenDict JIDResource::s_show[] = { - {"away", ShowAway}, - {"chat", ShowChat}, - {"dnd", ShowDND}, - {"xa", ShowXA}, - {0,0}, -}; - -TokenDict JBPresence::s_presence[] = { - {"error", Error}, - {"probe", Probe}, - {"subscribe", Subscribe}, - {"subscribed", Subscribed}, - {"unavailable", Unavailable}, - {"unsubscribe", Unsubscribe}, - {"unsubscribed", Unsubscribed}, - {0,0} -}; - -// Private thread class -class JBPrivateThread : public Thread, public JBThread -{ -public: - inline JBPrivateThread(Type type, JBThreadList* list, void* client, - int sleep, int prio) - : Thread(lookup(type,s_threadNames),Thread::priority((Priority)prio)), - JBThread(type,list,client,sleep) - {} - virtual void cancelThread(bool hard = false) - { Thread::cancel(hard); } - virtual void run() - { JBThread::runClient(); } -}; - -/** - * JBThread - */ -// Constructor. Append itself to the list -JBThread::JBThread(Type type, JBThreadList* list, void* client, int sleep) - : m_type(type), m_owner(list), m_client(client), m_sleep(sleep) -{ - if (!m_owner) - return; - Lock lock(m_owner->m_mutex); - m_owner->m_threads.append(this)->setDelete(false); -} - -// Destructor. Remove itself from the list -JBThread::~JBThread() -{ - Debug(m_owner?m_owner->owner():0,DebugAll, - "'%s' private thread terminated client=(%p) [%p]", - lookup(m_type,s_threadNames),m_client,this); - if (!m_owner) - return; - Lock lock(m_owner->m_mutex); - m_owner->m_threads.remove(this,false); -} - -// Create and start a private thread -bool JBThread::start(Type type, JBThreadList* list, void* client, - int sleep, int prio) -{ - if (sleep <= 0) - sleep = Thread::idleMsec(); - Lock lock(list->m_mutex); - const char* error = 0; - bool ok = !list->m_cancelling; - if (ok) - ok = (new JBPrivateThread(type,list,client,sleep,prio))->startup(); - else - error = ". Owner's threads are beeing cancelled"; - if (!ok) - Debug(list?list->owner():0,DebugNote, - "'%s' private thread failed to start client=(%p)%s", - lookup(type,s_threadNames),client,error?error:""); - return ok; -} - -// Process the client -void JBThread::runClient() -{ - if (!m_client) - return; - Debug(m_owner?m_owner->owner():0,DebugAll, - "'%s' private thread is running client=(%p) [%p]", - lookup(m_type,s_threadNames),m_client,this); - switch (m_type) { - case StreamConnect: - { - // Keep the stream alive while connecting - RefPointer stream = static_cast(m_client); - stream->connect(); - stream = 0; - } - break; - case EngineProcess: - while (true) - if (!((JBEngine*)m_client)->process(Time::msecNow())) - Thread::msleep(m_sleep,true); - else - Thread::check(true); - break; - case EngineReceive: - while (true) - if (!((JBEngine*)m_client)->receive()) - Thread::msleep(m_sleep,true); - else - Thread::check(true); - break; - case Presence: - while (true) - if (!((JBPresence*)m_client)->process()) - Thread::msleep(m_sleep,true); - else - Thread::check(true); - break; - case Jingle: - while (true) { - bool ok = false; - while (true) { - if (Thread::check(false)) - break; - JGEvent* event = ((JGEngine*)m_client)->getEvent(Time::msecNow()); - if (!event) - break; - ok = true; - ((JGEngine*)m_client)->processEvent(event); - } - if (!ok) - Thread::msleep(m_sleep,true); - else - Thread::check(true); - } - break; - case Message: - while (true) { - JBEvent* event = ((JBMessage*)m_client)->getMessage(); - if (event) { - ((JBMessage*)m_client)->processMessage(event); - Thread::check(true); - } - else - Thread::yield(true); - } - break; - default: - Debug(DebugStub,"JBThread::run() unhandled type %u",m_type); - } -} - - -/** - * JBThreadList - */ -void JBThreadList::cancelThreads(bool wait, bool hard) -{ - // Destroy private threads - m_mutex.lock(); - for (ObjList* o = m_threads.skipNull(); o; o = o->skipNext()) { - JBThread* p = static_cast(o->get()); - Debug(owner(),DebugAll,"Cancelling '%s' private thread hard=%s", - lookup(p->type(),s_threadNames),String::boolText(hard)); - p->cancelThread(hard); - } - m_cancelling = true; - m_mutex.unlock(); - // Wait to terminate - if (!hard && wait) { - DDebug(owner(),DebugAll,"Waiting for private threads to terminate"); - while (m_threads.skipNull()) - Thread::yield(); - Debug(owner(),DebugAll,"All private threads terminated"); - } - m_cancelling = false; -} - -// Default values -#define JB_RESTART_COUNT 2 // Stream restart counter default value -#define JB_RESTART_COUNT_MIN 1 -#define JB_RESTART_COUNT_MAX 10 - -#define JB_RESTART_UPDATE 15000 // Stream restart counter update interval -#define JB_RESTART_UPDATE_MIN 5000 -#define JB_RESTART_UPDATE_MAX 300000 -#define JB_SETUP_INTERVAL 10000 // Stream setup timeout -#define JB_IDLE_INTERVAL 60000 // Stream idle timeout - -// Presence values -#define JINGLE_VERSION "1.0" // Version capability -#define JINGLE_VOICE "voice-v1" // Voice capability for Google Talk - -/** - * JBEngine - */ - -JBEngine::JBEngine(Protocol proto) - : Mutex(true,"JBEngine"), m_protocol(proto), - m_restartUpdateInterval(JB_RESTART_UPDATE), m_restartCount(JB_RESTART_COUNT), - m_streamSetupInterval(JB_SETUP_INTERVAL), m_streamIdleInterval(JB_IDLE_INTERVAL), - m_printXml(0), m_identity(0), m_componentCheckFrom(1), - m_serverMutex(true,"JBEngine::server"), - m_servicesMutex(true,"JBEngine::services"), - m_initialized(false) -{ - JBThreadList::setOwner(this); - for (int i = 0; i < ServiceCount; i++) - m_services[i].setDelete(false); - debugName("jbengine"); - XDebug(this,DebugAll,"JBEngine [%p]",this); -} - -JBEngine::~JBEngine() -{ - cleanup(); - cancelThreads(); - // Remove streams if alive - if (m_streams.skipNull()) { - Debug(this,DebugNote,"Engine destroyed while still owning streams [%p]",this); - ListIterator iter(m_streams); - for (GenObject* o = 0; 0 != (o = iter.get());) - TelEngine::destruct(static_cast(o)); - } - TelEngine::destruct(m_identity); - XDebug(this,DebugAll,"~JBEngine [%p]",this); -} - -// Cleanup streams. Stop all threads owned by this engine. Release memory -void JBEngine::destruct() -{ - cleanup(); - cancelThreads(); - GenObject::destruct(); -} - -// Initialize the engine's parameters -void JBEngine::initialize(const NamedList& params) -{ - int lvl = params.getIntValue("debug_level",-1); - if (lvl != -1) - debugLevel(lvl); - - int recv = -1, proc = -1; - - if (!m_initialized) { - // Build engine Jabber identity and features - if (m_protocol == Component) - m_identity = new JIDIdentity(JIDIdentity::Gateway,JIDIdentity::GatewayGeneric); - else - m_identity = new JIDIdentity(JIDIdentity::Account,JIDIdentity::AccountRegistered); - m_features.add(XMPPNamespace::Jingle); - m_features.add(XMPPNamespace::JingleError); - m_features.add(XMPPNamespace::JingleAppsRtp); - m_features.add(XMPPNamespace::JingleAppsRtpInfo); - m_features.add(XMPPNamespace::JingleTransportIceUdp); - m_features.add(XMPPNamespace::JingleTransportRawUdp); - m_features.add(XMPPNamespace::JingleTransfer); - m_features.add(XMPPNamespace::Dtmf); - m_features.add(XMPPNamespace::DiscoInfo); - - recv = params.getIntValue("private_receive_threads",1); - for (int i = 0; i < recv; i++) - JBThread::start(JBThread::EngineReceive,this,this); - proc = params.getIntValue("private_process_threads",1); - for (int i = 0; i < proc; i++) - JBThread::start(JBThread::EngineProcess,this,this); - } - - m_serverMutex.lock(); - m_server.clear(); - m_serverMutex.unlock(); - - String tmp = params.getValue("printxml"); - m_printXml = tmp.toBoolean() ? -1: ((tmp == "verbose") ? 1 : 0); - - // Alternate domain names - m_alternateDomain.set(0,params.getValue("extra_domain")); - // Stream restart update interval - m_restartUpdateInterval = - params.getIntValue("stream_restartupdateinterval",JB_RESTART_UPDATE); - if (m_restartUpdateInterval < JB_RESTART_UPDATE_MIN) - m_restartUpdateInterval = JB_RESTART_UPDATE_MIN; - else - if (m_restartUpdateInterval > JB_RESTART_UPDATE_MAX) - m_restartUpdateInterval = JB_RESTART_UPDATE_MAX; - // Stream restart count - m_restartCount = - params.getIntValue("stream_restartcount",JB_RESTART_COUNT); - if (m_restartCount < JB_RESTART_COUNT_MIN) - m_restartCount = JB_RESTART_COUNT_MIN; - else - if (m_restartCount > JB_RESTART_COUNT_MAX) - m_restartCount = JB_RESTART_COUNT_MAX; - // Stream setup timer interval - int t = params.getIntValue("stream_setuptimeout",(int)m_streamSetupInterval); - m_streamSetupInterval = (t >= 1000) ? t : 0; - // XML parser max receive buffer - XMLParser::s_maxDataBuffer = - params.getIntValue("xmlparser_maxbuffer",XMLPARSER_MAXDATABUFFER); - // Default resource - m_defaultResource = params.getValue("default_resource","yate"); - // Check 'from' attribute for component streams - String chk = params.getValue("component_checkfrom"); - if (chk == "none") - m_componentCheckFrom = 0; - else if (chk == "remote") - m_componentCheckFrom = 2; - else - m_componentCheckFrom = 1; - - if (debugAt(DebugInfo)) { - String s; - s << " protocol=" << lookup(m_protocol,s_protoName); - s << " default_resource=" << m_defaultResource; - s << " component_checkfrom=" << m_componentCheckFrom; - s << " stream_restartupdateinterval=" << (unsigned int)m_restartUpdateInterval; - s << " stream_restartcount=" << (unsigned int)m_restartCount; - s << " stream_setuptimeout=" << (unsigned int)m_streamSetupInterval; - s << " xmlparser_maxbuffer=" << (unsigned int)XMLParser::s_maxDataBuffer; - s << " printxml=" << m_printXml; - if (recv > -1) - s << " private_receive_threads=" << recv; - if (proc > -1) - s << " private_process_threads=" << proc; - Debug(this,DebugInfo,"Jabber engine initialized:%s [%p]",s.c_str(),this); - } - - m_initialized = true; -} - -// Terminate all streams -void JBEngine::cleanup() -{ - Lock lock(this); - // Use an iterator: the stream might be destroyed when terminating - ListIterator iter(m_streams); - for (GenObject* o = 0; 0 != (o = iter.get());) { - JBStream* s = static_cast(o); - s->terminate(true,0,XMPPError::Shutdown,0,true); - } -} - -// Set the default component server to use -void JBEngine::setComponentServer(const char* domain) -{ - if (m_protocol != Component) - return; - Lock lock(m_serverMutex); - XMPPServerInfo* p = findServerInfo(domain,true); - // If doesn't exists try to get the first one from the list - if (!p) { - ObjList* obj = m_server.skipNull(); - p = obj ? static_cast(obj->get()) : 0; - } - if (!p) { - Debug(this,DebugNote,"No default component server [%p]",this); - return; - } - m_componentDomain.set(0,p->name()); - m_componentAddr = p->address(); - DDebug(this,DebugAll,"Default component server set to '%s' (%s) [%p]", - m_componentDomain.c_str(),m_componentAddr.c_str(),this); -} - -// Find a stream by its name -JBStream* JBEngine::findStream(const String& name) -{ - Lock lock(this); - ObjList* tmp = m_streams.find(name); - JBStream* stream = tmp ? static_cast(tmp->get()) : 0; - return stream && stream->ref() ? stream : 0; -} - -// Get a stream. Create it not found and requested -// For the component protocol, the jid parameter may contain the domain to find, -// otherwise, the default component will be used -JBStream* JBEngine::getStream(const JabberID* jid, bool create) -{ - Lock lock(this); - if (exiting()) - return 0; - - // Client - JBStream* stream = 0; - if (m_protocol == Client) { - if (!(jid && jid->bare())) - return 0; - for (ObjList* o = m_streams.skipNull(); o; o = o->skipNext()) { - stream = static_cast(o->get()); - if (stream->local().match(*jid)) - break; - stream = 0; - } - return ((stream && stream->ref()) ? stream : 0); - } - - const JabberID* remote = jid; - if (!remote) - remote = &m_componentDomain; - for (ObjList* o = m_streams.skipNull(); o; o = o->skipNext()) { - stream = static_cast(o->get()); - if (stream->remote() == *remote) - break; - stream = 0; - } - - if (!stream && create && m_protocol != Client) { - XMPPServerInfo* info = findServerInfo(remote->domain(),true); - if (!info) { - Debug(this,DebugNote,"No server info to create stream to '%s' [%p]", - remote->c_str(),this); - return 0; - } - stream = new JBComponentStream(this,*info,JabberID(0,info->identity(),0),*remote); - m_streams.append(stream); - } - return ((stream && stream->ref()) ? stream : 0); -} - -// Try to get a stream if stream parameter is 0 -bool JBEngine::getStream(JBStream*& stream, bool& release) -{ - release = false; - if (stream) - return true; - stream = getStream(0,true); - if (stream) { - release = true; - return true; - } - return false; -} - -// Create a new client stream if no other stream exists for the given account -JBClientStream* JBEngine::createClientStream(NamedList& params) -{ - NamedString* account = params.getParam("account"); - if (!account) - return 0; - - // Check for existing stream - JBStream* stream = findStream(*account); - if (stream) { - if (stream->type() != Client) - TelEngine::destruct(stream); - return static_cast(stream); - } - - Lock lock(this); - const char* domain = params.getValue("domain"); - const char* address = params.getValue("server",params.getValue("address")); - if (!domain) - domain = address; - JabberID jid(params.getValue("username"),domain,params.getValue("resource")); - // Build server info and create a new stream - if (!address) - address = jid.domain(); - if (!(address && jid.node() && jid.domain())) { - Debug(this,DebugNote,"Can't create client stream: invalid jid=%s or address=%s", - jid.bare().c_str(),address); - params.setParam("error","Invalid id or address"); - return 0; - } - int port = params.getIntValue("port",5222); - int flags = XMPPUtils::decodeFlags(params.getValue("options"),XMPPServerInfo::s_flagName); - XMPPServerInfo* info = new XMPPServerInfo("",address,port, - params.getValue("password"),"","",flags); - stream = new JBClientStream(this,*info,jid,params); - m_streams.append(stream); - TelEngine::destruct(info); - return stream->ref() ? static_cast(stream) : 0; -} - -// Keep calling receive() for each stream until no data is received or the thread is terminated -bool JBEngine::receive() -{ - bool ok = false; - lock(); - ListIterator iter(m_streams); - for (;;) { - JBStream* stream = static_cast(iter.get()); - // End of iteration? - if (!stream) - break; - // Get a reference - RefPointer sref = stream; - if (!sref) - continue; - // Read socket - unlock(); - if (Thread::check(false)) - return false; - if (sref->receive()) - ok = true; - lock(); - } - unlock(); - return ok; -} - -// Get events from the streams owned by this engine -bool JBEngine::process(u_int64_t time) -{ - lock(); - ListIterator iter(m_streams); - bool gotEvent = false; - for (;;) { - if (Thread::check(false)) - break; - JBStream* stream = static_cast(iter.get()); - // End of iteration? - if (!stream) - break; - // Get a reference - RefPointer sref = stream; - if (!sref) - continue; - // Get event - unlock(); - JBEvent* event = sref->getEvent(time); - if (!event) { - lock(); - continue; - } - - gotEvent = true; - bool recv = false; - // Send events to the registered services - switch (event->type()) { - case JBEvent::Message: - recv = received(ServiceMessage,event); - break; - case JBEvent::IqJingleGet: - case JBEvent::IqJingleSet: - case JBEvent::IqJingleRes: - case JBEvent::IqJingleErr: - recv = received(ServiceJingle,event); - break; - case JBEvent::Iq: - case JBEvent::IqError: - case JBEvent::IqResult: - recv = received(ServiceIq,event); - break; - case JBEvent::Presence: - recv = received(ServicePresence,event); - break; - case JBEvent::IqDiscoInfoGet: - case JBEvent::IqDiscoInfoSet: - case JBEvent::IqDiscoInfoRes: - case JBEvent::IqDiscoInfoErr: - case JBEvent::IqDiscoItemsGet: - case JBEvent::IqDiscoItemsSet: - case JBEvent::IqDiscoItemsRes: - case JBEvent::IqDiscoItemsErr: - recv = received(ServiceDisco,event) || processDisco(event); - break; - case JBEvent::IqCommandGet: - case JBEvent::IqCommandSet: - case JBEvent::IqCommandRes: - case JBEvent::IqCommandErr: - recv = received(ServiceCommand,event) || processCommand(event); - break; - case JBEvent::IqRosterSet: - case JBEvent::IqRosterRes: - case JBEvent::IqRosterErr: - case JBEvent::IqClientRosterUpdate: - recv = received(ServiceRoster,event); - break; - case JBEvent::WriteFail: - recv = received(ServiceWriteFail,event); - break; - case JBEvent::Terminated: - case JBEvent::Destroy: - case JBEvent::Running: - recv = received(ServiceStream,event); - break; - default: ; - } - if (!recv && event) { - Debug(this,DebugAll,"Delete unhandled event (%p,%s) [%p]", - event,event->name(),this); - TelEngine::destruct(event); - } - lock(); - } - unlock(); - return gotEvent; -} - -// Check for duplicate stream id at a remote server -bool JBEngine::checkDupId(const JBStream* stream) -{ - if (!(stream && stream->outgoing())) - return false; - Lock lock(this); - for (ObjList* o = m_streams.skipNull(); o; o = o->skipNext()) { - JBStream* s = static_cast(o->get()); - if (s != stream && s->outgoing() && - s->remote() == stream->remote() && s->id() == stream->id()) - return true; - } - return false; -} - -// Check the 'from' attribute received by a Component stream at startup -// 0: no check 1: local identity 2: remote identity -bool JBEngine::checkComponentFrom(JBComponentStream* stream, const char* from) -{ - if (!stream) - return false; - JabberID tmp(from); - switch (m_componentCheckFrom) { - case 1: - return stream->local() == tmp; - case 2: - return stream->remote() == tmp; - case 0: - return true; - } - return false; -} - -// Asynchronously call the connect method of the given stream if the stream is idle -void JBEngine::connect(JBStream* stream) -{ - XDebug(this,DebugAll,"JBEngine::connect(%p) [%p]",stream,this); - if (stream && stream->state() == JBStream::Idle) - JBThread::start(JBThread::StreamConnect,this,stream); -} - -// Setup the transport layer security for a stream -bool JBEngine::encryptStream(JBStream* stream) -{ - if (!stream) - return false; - Debug(this,DebugStub, - "Unable to start TLS for stream (%p) local=%s remote=%s [%p]", - stream,stream->local().c_str(),stream->remote().c_str(),this); - return false; -} - -// Append server info the list -void JBEngine::appendServer(XMPPServerInfo* server, bool open) -{ - if (!server) - return; - // Add if doesn't exists. Delete if already in the list - XMPPServerInfo* p = findServerInfo(server->name(),true); - if (!p) { - m_serverMutex.lock(); - m_server.append(server); - Debug(this,DebugAll,"Added server '%s' port=%d [%p]", - server->name().c_str(),server->port(),this); - m_serverMutex.unlock(); - } - else - TelEngine::destruct(server); - // Open stream - if (open && m_protocol == Component) { - JabberID jid(0,server->name(),0); - JBStream* stream = getStream(&jid); - if (stream) - TelEngine::destruct(stream); - } -} - -// Get the identity of the given server -bool JBEngine::getServerIdentity(String& destination, bool full, - const char* token, bool domain) -{ - Lock lock(m_serverMutex); - XMPPServerInfo* server = findServerInfo(token,domain); - if (!server) - return false; - destination = full ? server->fullIdentity() : server->identity(); - return true; -} - -// Find a server info object -XMPPServerInfo* JBEngine::findServerInfo(const char* token, bool domain) -{ - if (!token) - token = domain ? m_componentDomain : m_componentAddr; - if (!token) - return 0; - if (domain) - for (ObjList* o = m_server.skipNull(); o; o = o->skipNext()) { - XMPPServerInfo* server = static_cast(o->get()); - if (server->name() &= token) - return server; - } - else - for (ObjList* o = m_server.skipNull(); o; o = o->skipNext()) { - XMPPServerInfo* server = static_cast(o->get()); - if (server->address() == token) - return server; - } - return 0; -} - -// Attach a service to this engine -void JBEngine::attachService(JBService* service, Service type, int prio) -{ - if (!service) - return; - Lock lock(m_servicesMutex); - if (m_services[type].find(service)) - return; - if (prio == -1) - prio = service->priority(); - ObjList* insert = m_services[type].skipNull(); - for (; insert; insert = insert->skipNext()) { - JBService* tmp = static_cast(insert->get()); - if (prio <= tmp->priority()) { - insert->insert(service); - break; - } - } - if (!insert) - m_services[type].append(service); - Debug(this,DebugInfo,"Attached service (%p) '%s' type=%s priority=%d [%p]", - service,service->debugName(),lookup(type,s_serviceType),prio,this); -} - -// Remove a service from all event handlers of this engine. -void JBEngine::detachService(JBService* service) -{ - if (!service) - return; - Lock lock(m_servicesMutex); - for (int i = 0; i < ServiceCount; i++) { - GenObject* o = m_services[i].find(service); - if (!o) - continue; - m_services[i].remove(service,false); - Debug(this,DebugInfo,"Removed service (%p) '%s' type=%s [%p]", - service,service->debugName(),lookup(i,s_serviceType),this); - } -} - -// Print an XML element to output -void JBEngine::printXml(const XMLElement& xml, const JBStream* stream, bool send) const -{ - if (!(m_printXml && debugAt(DebugInfo))) - return; - String s; - const char* dir = send ? "sending" : "receiving"; - if (m_printXml < 0) { - bool unclose = xml.type() == XMLElement::StreamStart || - xml.type() == XMLElement::StreamEnd; - xml.toString(s,unclose); - Debug(this,DebugInfo,"Stream %s %s [%p]",dir,s.c_str(),stream); - } - else { - XMPPUtils::print(s,(XMLElement&)xml); - Debug(this,DebugInfo,"Stream %s [%p]%s",dir,stream,s.c_str()); - } -} - -// Process disco info events -bool JBEngine::processDisco(JBEvent* event) -{ - JBStream* stream = event->stream(); - XMLElement* child = event->child(); - // Check if we should or can respond to it - if (!(event->type() == JBEvent::IqDiscoInfoGet && stream && child)) - return false; - - // Create response - if (m_identity) - m_identity->setName(stream->local()); - XMLElement* iq = XMPPUtils::createDiscoInfoRes(event->to(),event->from(),event->id(), - &m_features,m_identity); - stream->sendStanza(iq); - TelEngine::destruct(event); - return true; -} - -// Process commands -bool JBEngine::processCommand(JBEvent* event) -{ - JBStream* stream = event->stream(); - if (!stream || - (event->type() != JBEvent::IqCommandGet && event->type() != JBEvent::IqCommandSet)) - return false; - - // Send error - stream->sendStanza(event->createError(XMPPError::TypeCancel,XMPPError::SFeatureNotImpl)); - TelEngine::destruct(event); - return true; -} - -// Find a service to process a received event -bool JBEngine::received(Service service, JBEvent* event) -{ - if (!event) - return false; - Lock lock(m_servicesMutex); - for (ObjList* o = m_services[service].skipNull(); o; o = o->skipNext()) { - JBService* service = static_cast(o->get()); - XDebug(this,DebugAll,"Sending event (%p,%s) to service '%s' [%p]", - event,event->name(),service->debugName(),this); - if (service->received(event)) - return true; - } - return false; -} - - -/** - * JBService - */ -JBService::JBService(JBEngine* engine, const char* name, - const NamedList* params, int prio) - : Mutex(true,"JBService"), - m_initialized(false), m_engine(engine), m_priority(prio) -{ - debugName(name); - XDebug(this,DebugAll,"Jabber service created [%p]",this); - if (params) - initialize(*params); -} - -JBService::~JBService() -{ - XDebug(this,DebugAll,"JBService::~JBService() [%p]",this); -} - -// Remove from engine. Release memory -void JBService::destruct() -{ - if (m_engine) - m_engine->detachService(this); - Debug(this,DebugAll,"Jabber service destroyed [%p]",this); - GenObject::destruct(); -} - -// Accept an event from the engine -bool JBService::accept(JBEvent* event, bool& processed, bool& insert) -{ - Debug(this,DebugStub,"JBService::accept(%p)",event); - return false; -} - -// Receive an event from engine -bool JBService::received(JBEvent* event) -{ - if (!event) - return false; - bool insert = false; - bool processed = false; - Lock lock(this); - XDebug(this,DebugAll,"Receiving (%p,%s) [%p]",event,event->name(),this); - // Keep a reference to be able to show debug - event->ref(); - bool ok = true; - while (true) { - if (!accept(event,processed,insert)) { - ok = false; - break; - } - if (processed) { - // Don't use TelEngine::destruct(): it will set the pointer to 0 - event->deref(); - break; - } - event->releaseStream(); - if (insert) - m_events.insert(event); - else - m_events.append(event); - break; - } - DDebug(this,DebugAll,"Event (%p,%s) %s [%p]",event,event->name(), - ok?(processed?"processed":(insert?"inserted":"appended")):"not accepted",this); - TelEngine::destruct(event); - return ok; -} - -// Get an event from queue -JBEvent* JBService::deque() -{ - Lock lock(this); - ObjList* obj = m_events.skipNull(); - if (!obj) - return 0; - JBEvent* event = static_cast(obj->remove(false)); - DDebug(this,DebugAll,"Dequeued event (%p,%s) [%p]", - event,event->name(),this); - return event; -} - - -/** - * JBEvent - */ -JBEvent::JBEvent(Type type, JBStream* stream, XMLElement* element, XMLElement* child) - : m_type(type), m_stream(0), m_link(true), m_element(element), m_child(child) -{ - if (!init(stream,element)) - m_type = Invalid; -} - -JBEvent::JBEvent(Type type, JBStream* stream, XMLElement* element, const String& senderID) - : m_type(type), m_stream(0), m_link(true), m_element(element), m_child(0), - m_id(senderID) -{ - if (!init(stream,element)) - m_type = Invalid; -} - -JBEvent::~JBEvent() -{ - if (m_stream) { - releaseStream(); - TelEngine::destruct(m_stream); - } - releaseXML(true); - XDebug(DebugAll,"JBEvent::~JBEvent [%p]",this); -} - -void JBEvent::releaseStream() -{ - if (m_link && m_stream) { - m_stream->eventTerminated(this); - m_link = false; - } -} - -// Create an error response from this event if it contains a known type. -XMLElement* JBEvent::createError(XMPPError::ErrorType type, XMPPError::Type error, - const char* text) -{ - if (!element()) - return 0; - XMLElement* xml = 0; - switch (m_type) { - case Iq: - case IqDiscoInfoGet: - case IqDiscoInfoSet: - case IqDiscoItemsGet: - case IqDiscoItemsSet: - case IqCommandGet: - case IqCommandSet: - case IqJingleGet: - case IqJingleSet: - break; - case Message: - if (JBMessage::Error == JBMessage::msgType(element()->getAttribute("type"))) - return 0; - break; - case Presence: - if (JBPresence::Error == JBPresence::presenceType(element()->getAttribute("type"))) - return 0; - break; - default: - return 0; - } - xml = XMPPUtils::createError(releaseXML(),type,error,text); - return xml; -} - -bool JBEvent::init(JBStream* stream, XMLElement* element) -{ - bool bRet = true; - if (stream && stream->ref()) - m_stream = stream; - else - bRet = false; - m_element = element; - XDebug(DebugAll,"JBEvent::init type=%s stream=(%p) xml=(%p) [%p]", - name(),m_stream,m_element,this); - if (!m_element) - return bRet; - - // Most elements have these parameters: - m_stanzaType = m_element->getAttribute("type"); - m_from.set(m_element->getAttribute("from")); - m_to.set(m_element->getAttribute("to")); - m_id = m_element->getAttribute("id"); - - // Decode some data - switch (m_element->type()) { - case XMLElement::Message: - if (m_stanzaType != "error") { - XMLElement* body = m_element->findFirstChild("body"); - if (body) { - m_text = body->getText(); - TelEngine::destruct(body); - } - } - else - XMPPUtils::decodeError(m_element,m_text,m_text); - break; - case XMLElement::Iq: - case XMLElement::Presence: - if (m_stanzaType != "error") - break; - default: - XMPPUtils::decodeError(m_element,m_text,m_text); - } - return bRet; -} - - -/** - * JBMessage - */ -// Initialize service. Create private threads -void JBMessage::initialize(const NamedList& params) -{ - int lvl = params.getIntValue("debug_level",-1); - if (lvl != -1) - debugLevel(lvl); - - if (m_initialized) - return; - m_initialized = true; - m_syncProcess = params.getBoolValue("sync_process",m_syncProcess); - if (debugAt(DebugInfo)) { - String s; - s << " synchronous_process=" << m_syncProcess; - Debug(this,DebugInfo,"Jabber Message service initialized:%s [%p]", - s.c_str(),this); - } - if (!m_syncProcess) { - int c = params.getIntValue("private_process_threads",1); - for (int i = 0; i < c; i++) - JBThread::start(JBThread::Message,this,this); - } -} - -// Message processor -void JBMessage::processMessage(JBEvent* event) -{ - Debug(this,DebugStub,"Default message processing. Deleting (%p)",event); -} - -// Create a message element -XMLElement* JBMessage::createMessage(const char* type, const char* from, - const char* to, const char* id, const char* message) -{ - XMLElement* msg = new XMLElement(XMLElement::Message); - msg->setAttributeValid("type",type); - msg->setAttribute("from",from); - msg->setAttribute("to",to); - if (id) - msg->setAttributeValid("id",id); - if (message) - msg->addChild(new XMLElement(XMLElement::Body,0,message)); - return msg; -} - -// Accept an event from the engine and process it if configured to do that -bool JBMessage::accept(JBEvent* event, bool& processed, bool& insert) -{ - if (event->type() != JBEvent::Message) - return false; - if (m_syncProcess) { - processed = true; - processMessage(event); - } - return true; -} - - -/** - * JIDResource - */ -// Set the presence for this resource -bool JIDResource::setPresence(bool value) -{ - Presence p = value ? Available : Unavailable; - if (m_presence == p) - return false; - m_presence = p; - return true; -} - -// Change this resource from received element -bool JIDResource::fromXML(XMLElement* element) -{ - if (!(element && element->type() == XMLElement::Presence)) - return false; - JBPresence::Presence p = JBPresence::presenceType(element->getAttribute("type")); - if (p != JBPresence::None && p != JBPresence::Unavailable) - return false; - - m_info.clear(); - bool changed = setPresence(p == JBPresence::None); - for (XMLElement* c = element->findFirstChild(); c; c = element->findNextChild(c)) { - if (c->nameIs("show")) { - if (!changed && m_show != showType(c->getText())) - changed = true; - m_show = showType(c->getText()); - } - else if (c->nameIs("status")) { - if (!changed && m_status != c->getText()) - changed = true; - m_status = c->getText(); - } - else if (c->nameIs("c")) { - NamedList caps(""); - if (XMPPUtils::split(caps,c->getAttribute("ext"),' ',true)) { - // Check audio - bool tmp = (0 != caps.getParam(JINGLE_VOICE)); - if (tmp != hasCap(CapAudio)) { - changed = true; - if (tmp) - m_capability |= CapAudio; - else - m_capability &= ~CapAudio; - } - } - } - else - m_info.append(new XMLElement(*c)); - } - return changed; -} - -// Append this resource's capabilities to a given element -void JIDResource::addTo(XMLElement* element, bool addInfo) -{ - if (!element) - return; - if (m_show != ShowNone) - element->addChild(new XMLElement("show",0,showText(m_show))); - element->addChild(new XMLElement("status",0,m_status)); - // Add priority - XMLElement* priority = new XMLElement("priority",0,String(m_priority)); - element->addChild(priority); - // Add capabilities - XMLElement* c = new XMLElement("c"); - c->setAttribute("xmlns","http://jabber.org/protocol/caps"); - c->setAttribute("node","http://www.google.com/xmpp/client/caps"); - c->setAttribute("ver",JINGLE_VERSION); - if (hasCap(CapAudio)) - c->setAttribute("ext",JINGLE_VOICE); - element->addChild(c); - if (addInfo) - XMPPUtils::addChidren(element,m_info); -} - - -/** - * JIDResourceList - */ - -// Add a resource to the list -bool JIDResourceList::add(JIDResource* resource) -{ - if (!resource) - return false; - Lock lock(this); - if (get(resource->name())) { - TelEngine::destruct(resource); - return false; - } - // Add resource in the proper place - ObjList* o = m_resources.skipNull(); - for (; o; o = o->skipNext()) { - JIDResource* tmp = static_cast(o->get()); - if (resource->priority() >= tmp->priority()) - break; - } - if (o) - o->insert(resource); - else - m_resources.append(resource); - return true; -} - -// Get a resource from list -JIDResource* JIDResourceList::get(const String& name) -{ - Lock lock(this); - ObjList* obj = m_resources.skipNull(); - for (; obj; obj = obj->skipNext()) { - JIDResource* res = static_cast(obj->get()); - if (res->name() == name) - return res; - } - return 0; -} - -// Get the first resource with audio capabilities -JIDResource* JIDResourceList::getAudio(bool availableOnly) -{ - Lock lock(this); - ObjList* obj = m_resources.skipNull(); - for (; obj; obj = obj->skipNext()) { - JIDResource* res = static_cast(obj->get()); - if (res->hasCap(JIDResource::CapAudio) && - (!availableOnly || (availableOnly && res->available()))) - return res; - } - return 0; -} - - -/** - * XMPPUser - */ -// Constructor -XMPPUser::XMPPUser(XMPPUserRoster* local, const char* node, const char* domain, - XMPPDirVal sub, bool subTo, bool sendProbe) - : Mutex(true,"XMPPUser"), - m_local(0), m_jid(node,domain), m_nextProbe(0), m_expire(0) -{ - if (local && local->ref()) - m_local = local; - else { - Debug(DebugFail,"XMPPUser. No local user for remote=%s [%p]",m_jid.c_str(),this); - return; - } - m_local->addUser(this); - DDebug(m_local->engine(),DebugAll,"User(%s). Added remote=%s [%p]", - m_local->jid().c_str(),m_jid.c_str(),this); - // Done if no engine - if (!m_local->engine()) { - m_subscription.set((int)sub); - return; - } - // Update subscription - switch (sub) { - case XMPPDirVal::None: - break; - case XMPPDirVal::Both: - updateSubscription(true,true,0); - updateSubscription(false,true,0); - break; - case XMPPDirVal::From: - updateSubscription(true,true,0); - break; - case XMPPDirVal::To: - updateSubscription(false,true,0); - break; - } - // Subscribe to remote user if not already subscribed and auto subscribe is true - if (subTo || (!m_subscription.to() && (m_local->engine()->autoSubscribe().to()))) - sendSubscribe(JBPresence::Subscribe,0); - // Probe remote user - if (sendProbe) - probe(0); -} - -XMPPUser::~XMPPUser() -{ - if (!m_local) - return; - DDebug(m_local->engine(),DebugAll, "~XMPPUser() local=%s remote=%s [%p]", - m_local->jid().c_str(),m_jid.c_str(),this); - // Remove all local resources: this will make us unavailable - clearLocalRes(); - m_local->removeUser(this); - TelEngine::destruct(m_local); -} - -// Add a resource for the user -bool XMPPUser::addLocalRes(JIDResource* resource, bool send) -{ - if (!resource) - return false; - Lock lock(this); - if (!m_localRes.add(resource)) - return false; - DDebug(m_local->engine(),DebugAll, - "User(%s). Added local resource name=%s audio=%s avail=%s [%p]", - m_local->jid().c_str(),resource->name().c_str(), - String::boolText(resource->hasCap(JIDResource::CapAudio)), - String::boolText(resource->available()),this); - if (send && m_subscription.from()) - sendPresence(resource,0,true); - return true; -} - -// Remove a resource of the user -void XMPPUser::removeLocalRes(JIDResource* resource) -{ - if (!(resource && m_localRes.get(resource->name()))) { - TelEngine::destruct(resource); - return; - } - Lock lock(this); - resource->setPresence(false); - if (m_subscription.from()) - sendPresence(resource,0); - DDebug(m_local->engine(),DebugAll, - "User(%s). Removing local resource name=%s [%p]", - m_local->jid().c_str(),resource->name().c_str(),this); - m_localRes.remove(resource); -} - -// Remove all user's resources -void XMPPUser::clearLocalRes() -{ - Lock lock(this); - m_localRes.clear(); - if (m_subscription.from()) - sendUnavailable(0); -} - -// Add a remote resource to the list -bool XMPPUser::addRemoteRes(JIDResource* resource) -{ - if (!resource) - return false; - Lock lock(this); - if (!m_remoteRes.add(resource)) - return false; - DDebug(m_local->engine(),DebugAll, - "User(%s). Added remote resource name=%s audio=%s avail=%s [%p]", - m_local->jid().c_str(),resource->name().c_str(), - String::boolText(resource->hasCap(JIDResource::CapAudio)), - String::boolText(resource->available()),this); - return true; -} - -// Remove a remote resource from the list -void XMPPUser::removeRemoteRes(JIDResource* resource) -{ - if (!(resource && m_remoteRes.get(resource->name()))) { - TelEngine::destruct(resource); - return; - } - Lock lock(this); - DDebug(m_local->engine(),DebugAll, - "User(%s). Removing remote resource name=%s [%p]", - m_local->jid().c_str(),resource->name().c_str(),this); - m_remoteRes.remove(resource); -} - -// Process an error stanza -void XMPPUser::processError(JBEvent* event) -{ - String code, type, error; - JBPresence::decodeError(event->element(),code,type,error); - DDebug(m_local->engine(),DebugAll,"User(%s). Received error=%s code=%s [%p]", - m_local->jid().c_str(),error.c_str(),code.c_str(),this); -} - -// Process presence probe -void XMPPUser::processProbe(JBEvent* event, const String* resName) -{ - updateTimeout(true); - XDebug(m_local->engine(),DebugAll,"User(%s). Received probe [%p]", - m_local->jid().c_str(),this); - if (resName) - notifyResource(false,*resName,event->stream(),true); - else - notifyResources(false,event->stream(),true); -} - -// Process presence stanzas -bool XMPPUser::processPresence(JBEvent* event, bool available) -{ - updateTimeout(true); - // No resource ? - Lock lock(&m_remoteRes); - if (event->from().resource().null()) { - // Available without resource ? No way !!! - if (available) - return true; - // Check if should notify - bool notify = true; - // User is unavailable for all resources - ListIterator iter(m_remoteRes.m_resources); - GenObject* obj; - for (; (obj = iter.get());) { - JIDResource* res = static_cast(obj); - if (res->setPresence(false)) { - DDebug(m_local->engine(),DebugInfo, - "User(%s). Resource %s state=%s audio=%s [%p]", - m_local->jid().c_str(),res->name().c_str(), - res->available()?"available":"unavailable", - String::boolText(res->hasCap(JIDResource::CapAudio)),this); - notify = false; - if (m_local->engine()) - m_local->engine()->notifyPresence(this,res); - } - if (!m_local->engine() || m_local->engine()->delUnavailable()) - removeRemoteRes(res); - - } - // Done if no presence service - if (!m_local->engine()) - return true; - - if (m_local->engine() && notify) - m_local->engine()->notifyPresence(event,false); - // No more resources ? Remove user - if (!m_remoteRes.getFirst() && m_local->engine()->delUnavailable()) - return false; - // Notify local resources to remote user if not already done - if (m_subscription.from()) - notifyResources(false,event->stream(),false); - return true; - } - - // 'from' has a resource: check if we already have one - ObjList* obj = m_remoteRes.m_resources.skipNull(); - JIDResource* res = 0; - for (; obj; obj = obj->skipNext()) { - res = static_cast(obj->get()); - if (res->name() == event->from().resource()) - break; - res = 0; - } - // Add a new resource if we don't have one - if (!res) { - res = new JIDResource(event->from().resource()); - m_remoteRes.add(res); - DDebug(m_local->engine(),DebugInfo, - "User(%s). remote=%s added resource '%s' [%p]", - m_local->jid().c_str(),event->from().bare().c_str(),res->name().c_str(),this); - } - // Changed: notify - if (res->fromXML(event->element())) { - DDebug(m_local->engine(),DebugInfo, - "User(%s). remote=%s resource %s state=%s audio=%s [%p]", - m_local->jid().c_str(),event->from().bare().c_str(),res->name().c_str(), - res->available()?"available":"unavailable", - String::boolText(res->hasCap(JIDResource::CapAudio)),this); - if (m_local->engine()) - m_local->engine()->notifyPresence(this,res); - } - if (!available && (!m_local->engine() || m_local->engine()->delUnavailable())) { - removeRemoteRes(res); - // No more resources ? Remove user - if (!m_remoteRes.getFirst()) - return false; - } - // Done if no presence service - if (!m_local->engine()) - return true; - // Notify local resources to remote user if not already done - if (m_subscription.from()) - notifyResources(false,event->stream(),false); - return true; -} - -// Process subscribe requests -void XMPPUser::processSubscribe(JBEvent* event, JBPresence::Presence type) -{ - Lock lock(this); - switch (type) { - case JBPresence::Subscribe: - // Already subscribed to us: Confirm subscription - if (m_subscription.from()) { - sendSubscribe(JBPresence::Subscribed,event->stream()); - return; - } - // Approve if auto subscribing - if (m_local->engine()->autoSubscribe().from()) - sendSubscribe(JBPresence::Subscribed,event->stream()); - break; - case JBPresence::Subscribed: - // Already subscribed to remote user: do nothing - if (m_subscription.to()) - return; - updateSubscription(false,true,event->stream()); - break; - case JBPresence::Unsubscribe: - // Already unsubscribed from us: confirm it - if (!m_subscription.from()) { - sendSubscribe(JBPresence::Unsubscribed,event->stream()); - return; - } - // Approve if auto subscribing - if (m_local->engine()->autoSubscribe().from()) - sendSubscribe(JBPresence::Unsubscribed,event->stream()); - break; - case JBPresence::Unsubscribed: - // If not subscribed to remote user ignore the unsubscribed confirmation - if (!m_subscription.to()) - return; - updateSubscription(false,false,event->stream()); - break; - default: - return; - } - // Notify - if (m_local->engine()) - m_local->engine()->notifySubscribe(this,type); -} - -// Probe a remote user -bool XMPPUser::probe(JBStream* stream, u_int64_t time) -{ - if (!m_local->engine()) - return false; - Lock lock(this); - updateTimeout(false,time); - XDebug(m_local->engine(),DebugAll,"User(%s). Sending probe [%p]", - m_local->jid().c_str(),this); - XMLElement* xml = JBPresence::createPresence(m_local->jid().bare(),m_jid.bare(), - JBPresence::Probe); - return m_local->engine()->sendStanza(xml,stream); -} - -// Send a subscribe request -bool XMPPUser::sendSubscribe(JBPresence::Presence type, JBStream* stream) -{ - Lock lock(this); - bool from = false; - bool value = false; - switch (type) { - case JBPresence::Subscribed: - from = true; - value = true; - break; - case JBPresence::Unsubscribed: - from = true; - break; - case JBPresence::Subscribe: - case JBPresence::Unsubscribe: - break; - default: - return false; - } - XDebug(m_local->engine(),DebugAll,"User(%s). Sending '%s' to %s [%p]", - m_local->jid().c_str(),JBPresence::presenceText(type), - m_jid.bare().c_str(),this); - bool result = false; - if (m_local->engine()) { - XMLElement* xml = JBPresence::createPresence(m_local->jid().bare(),m_jid.bare(),type); - result = m_local->engine()->sendStanza(xml,stream); - } - // Set subscribe data. Not for subscribe/unsubscribe - if (from && result) - updateSubscription(true,value,stream); - return result; -} - -// Check timeouts -bool XMPPUser::timeout(u_int64_t time) -{ - Lock lock(this); - if (!m_expire) { - if (m_nextProbe < time) - probe(0,time); - return false; - } - if (m_expire > time) - return false; - // Timeout. Clear resources & Notify - Debug(m_local->engine(),DebugNote, - "User(%s). Remote %s expired. Set unavailable [%p]", - m_local->jid().c_str(),m_jid.bare().c_str(),this); - m_remoteRes.clear(); - // Fake an unavailable presence event - XMLElement* xml = JBPresence::createPresence(m_jid,m_local->jid(),JBPresence::Unavailable); - JBEvent* event = new JBEvent(JBEvent::Presence,0,xml); - m_local->engine()->notifyPresence(event,false); - TelEngine::destruct(event); - return true; -} - -// Send presence notifications for all resources -bool XMPPUser::sendPresence(JIDResource* resource, JBStream* stream, - bool force) -{ - Lock lock(this); - if (!m_local->engine()) - return false; - // Send presence for all resources - JabberID from(m_local->jid().node(),m_local->jid().domain()); - if (!resource) { - ObjList* obj = m_localRes.m_resources.skipNull(); - for (; obj; obj = obj->skipNext()) { - JIDResource* res = static_cast(obj->get()); - from.resource(res->name()); - XMLElement* xml = JBPresence::createPresence(from,m_jid.bare(), - res->available() ? JBPresence::None : JBPresence::Unavailable); - // Add capabilities - if (res->available()) - res->addTo(xml); - m_local->engine()->sendStanza(xml,stream); - } - return true; - } - // Don't send if already done and not force - if (resource->presence() != JIDResource::Unknown && !force) - return false; - from.resource(resource->name()); - XMLElement* xml = JBPresence::createPresence(from,m_jid.bare(), - resource->available() ? JBPresence::None : JBPresence::Unavailable); - // Add capabilities - if (resource->available()) - resource->addTo(xml); - return m_local->engine()->sendStanza(xml,stream); -} - -// Notify the presence of a given resource -void XMPPUser::notifyResource(bool remote, const String& name, - JBStream* stream, bool force) -{ - if (remote) { - Lock lock(&m_remoteRes); - JIDResource* res = m_remoteRes.get(name); - if (res) - m_local->engine()->notifyPresence(this,res); - return; - } - Lock lock(&m_localRes); - JIDResource* res = m_localRes.get(name); - if (res) - sendPresence(res,stream,force); -} - -// Notify the presence for all resources -void XMPPUser::notifyResources(bool remote, JBStream* stream, bool force) -{ - if (remote) { - Lock lock(&m_remoteRes); - ObjList* obj = m_remoteRes.m_resources.skipNull(); - for (; obj; obj = obj->skipNext()) { - JIDResource* res = static_cast(obj->get()); - m_local->engine()->notifyPresence(this,res); - } - return; - } - Lock lock(&m_localRes); - ObjList* obj = m_localRes.m_resources.skipNull(); - for (; obj; obj = obj->skipNext()) { - JIDResource* res = static_cast(obj->get()); - sendPresence(res,stream,force); - } -} - -// Send unavailable -bool XMPPUser::sendUnavailable(JBStream* stream) -{ - XDebug(m_local->engine(),DebugAll,"User(%s). Sending 'unavailable' to %s [%p]", - m_local->jid().c_str(),m_jid.bare().c_str(),this); - XMLElement* xml = JBPresence::createPresence(m_local->jid().bare(),m_jid.bare(), - JBPresence::Unavailable); - return m_local->engine() && m_local->engine()->sendStanza(xml,stream); -} - -// Update the subscription state for a remote user -void XMPPUser::updateSubscription(bool from, bool value, JBStream* stream) -{ - Lock lock(this); - int sub = (from ? XMPPDirVal::From : XMPPDirVal::To); - // Don't update if nothing changed - if (value == (0 != m_subscription.flag(sub))) - return; - if (value) - m_subscription.set(sub); - else - m_subscription.reset(sub); - DDebug(m_local->engine(),DebugInfo, - "User(%s). Updated subscription (%s) for remote=%s [%p]", - m_local->jid().c_str(),XMPPDirVal::lookup((int)m_subscription), - m_jid.bare().c_str(),this); - // Send presence if remote user is subscribed to us - if (from && m_subscription.from()) { - sendUnavailable(stream); - sendPresence(0,stream,true); - } -} - -// Update some timeout values -void XMPPUser::updateTimeout(bool from, u_int64_t time) -{ - if (!m_local->engine()) - return; - Lock lock(this); - m_nextProbe = time + m_local->engine()->probeInterval(); - if (from) - m_expire = 0; - else - m_expire = time + m_local->engine()->expireInterval(); -} - -/** - * XMPPUserRoster - */ -XMPPUserRoster::XMPPUserRoster(JBPresence* engine, const char* node, - const char* domain, JBEngine::Protocol proto) - : Mutex(true,"XMPPUserRoster"), - m_jid(node,domain), - m_identity(0), - m_engine(engine) -{ - if (m_engine) - m_engine->addRoster(this); - - if (proto == JBEngine::Component) - m_identity = new JIDIdentity(JIDIdentity::Client,JIDIdentity::ComponentGeneric); - else if (proto == JBEngine::Client) - m_identity = new JIDIdentity(JIDIdentity::Client,JIDIdentity::AccountRegistered); - else - m_identity = new JIDIdentity(JIDIdentity::CategoryUnknown,JIDIdentity::TypeUnknown); - m_features.add(XMPPNamespace::Jingle); - m_features.add(XMPPNamespace::JingleAppsRtp); - m_features.add(XMPPNamespace::JingleAppsRtpInfo); - m_features.add(XMPPNamespace::JingleAppsRtpAudio); - m_features.add(XMPPNamespace::JingleTransportIceUdp); - m_features.add(XMPPNamespace::JingleTransportRawUdp); - m_features.add(XMPPNamespace::JingleTransportRawUdpInfo); - m_features.add(XMPPNamespace::JingleTransportByteStreams); - m_features.add(XMPPNamespace::JingleAppsFileTransfer); - m_features.add(XMPPNamespace::CapVoiceV1); - - Debug(m_engine,DebugAll, "XMPPUserRoster %s [%p]",m_jid.c_str(),this); -} - -XMPPUserRoster::~XMPPUserRoster() -{ - if (m_engine) - m_engine->removeRoster(this); - TelEngine::destruct(m_identity); - cleanup(); - Debug(m_engine,DebugAll, "~XMPPUserRoster %s [%p]",m_jid.c_str(),this); -} - -// Get a remote user from roster -// Add a new one if requested -XMPPUser* XMPPUserRoster::getUser(const JabberID& jid, bool add, bool* added) -{ - Lock lock(this); - ObjList* obj = m_remote.skipNull(); - XMPPUser* u = 0; - for (; obj; obj = obj->skipNext()) { - u = static_cast(obj->get()); - if (jid.bare() &= u->jid().bare()) - break; - u = 0; - } - if (!u && !add) - return 0; - if (!u) { - u = new XMPPUser(this,jid.node(),jid.domain(),XMPPDirVal::From); - if (added) - *added = true; - Debug(m_engine,DebugAll,"User(%s) added remote=%s [%p]", - m_jid.c_str(),u->jid().bare().c_str(),this); - } - return u->ref() ? u : 0; -} - -// Remove an user from roster -bool XMPPUserRoster::removeUser(const JabberID& remote) -{ - Lock lock(this); - ObjList* obj = m_remote.skipNull(); - for (; obj; obj = obj->skipNext()) { - XMPPUser* u = static_cast(obj->get()); - if (remote.bare() &= u->jid().bare()) { - Debug(m_engine,DebugAll,"User(%s) removed remote=%s [%p]", - m_jid.c_str(),u->jid().bare().c_str(),this); - m_remote.remove(u,true); - break; - } - } - return (0 != m_remote.skipNull()); -} - -// Check the presence timeout for all remote users -bool XMPPUserRoster::timeout(u_int64_t time) -{ - Lock lock(this); - ListIterator iter(m_remote); - GenObject* obj; - for (; (obj = iter.get());) { - XMPPUser* u = static_cast(obj); - if (u->timeout(time)) - m_remote.remove(u,true); - } - return (0 == m_remote.skipNull()); -} - -/** - * JBPresence - */ - -// Build the service -JBPresence::JBPresence(JBEngine* engine, const NamedList* params, int prio) - : JBService(engine,"jbpresence",params,prio), - m_delUnavailable(false), m_autoRoster(false), m_ignoreNonRoster(false), - m_autoProbe(true), m_probeInterval(1800000), m_expireInterval(300000), - m_defIdentity(0) -{ - JBThreadList::setOwner(this); - m_defIdentity = new JIDIdentity(JIDIdentity::Client,JIDIdentity::ComponentGeneric); - m_defFeatures.add(XMPPNamespace::Jingle); - m_defFeatures.add(XMPPNamespace::JingleAppsRtp); - m_defFeatures.add(XMPPNamespace::JingleAppsRtpInfo); - m_defFeatures.add(XMPPNamespace::JingleAppsRtpAudio); - m_defFeatures.add(XMPPNamespace::JingleTransportIceUdp); - m_defFeatures.add(XMPPNamespace::JingleTransportRawUdp); - m_defFeatures.add(XMPPNamespace::JingleTransportRawUdpInfo); - m_defFeatures.add(XMPPNamespace::JingleTransportByteStreams); - m_defFeatures.add(XMPPNamespace::JingleAppsFileTransfer); - m_defFeatures.add(XMPPNamespace::CapVoiceV1); -} - -JBPresence::~JBPresence() -{ - cancelThreads(); - Lock lock(this); - ListIterator iter(m_rosters); - GenObject* obj; - for (; (obj = iter.get());) { - XMPPUserRoster* ur = static_cast(obj); - ur->cleanup(); - m_rosters.remove(ur,true); - } - TelEngine::destruct(m_defIdentity); -} - -// Initialize the service -void JBPresence::initialize(const NamedList& params) -{ - int lvl = params.getIntValue("debug_level",-1); - if (lvl != -1) - debugLevel(lvl); - - m_autoSubscribe.replace(params.getValue("auto_subscribe")); - m_delUnavailable = params.getBoolValue("delete_unavailable",true); - m_ignoreNonRoster = params.getBoolValue("ignorenonroster",false); - m_autoProbe = params.getBoolValue("auto_probe",true); - NamedString* addSubParam = params.getParam("add_onsubscribe"); - if (addSubParam) - m_addOnSubscribe.replace(addSubParam->c_str()); - NamedString* addPresParam = params.getParam("add_onpresence"); - if (addPresParam) - m_addOnPresence.replace(addPresParam->c_str()); - NamedString* addProbeParam = params.getParam("add_onprobe"); - if (addProbeParam) - m_addOnProbe.replace(addProbeParam->c_str()); - - // Override missing add_ params if should keep the roster - // Automatically process (un)subscribe and probe requests if no roster - if (engine()) { - XMPPServerInfo* info = engine()->findServerInfo(engine()->componentServer(),true); - if (info) { - if (info->flag(XMPPServerInfo::KeepRoster)) { - if (!addSubParam) - m_addOnSubscribe.set(XMPPDirVal::Both); - if (!addPresParam) - m_addOnPresence.set(XMPPDirVal::Both); - if (!addProbeParam) - m_addOnProbe.set(XMPPDirVal::Both); - } - else { - m_autoProbe = true; - m_autoSubscribe.replace(XMPPDirVal::From); - } - } - } - - m_probeInterval = 1000 * params.getIntValue("probe_interval",m_probeInterval/1000); - m_expireInterval = 1000 * params.getIntValue("expire_interval",m_expireInterval/1000); - - m_autoRoster = m_addOnSubscribe.flag(-1) || m_addOnProbe.flag(-1) || - m_addOnPresence.flag(-1); - - if (m_ignoreNonRoster) - m_autoProbe = false; - - if (debugAt(DebugInfo)) { - String s; - s << " auto_subscribe=" << XMPPDirVal::lookup((int)m_autoSubscribe); - s << " delete_unavailable=" << String::boolText(m_delUnavailable); - s << " ignorenonroster=" << String::boolText(m_ignoreNonRoster); - s << " add_onsubscribe=" << XMPPDirVal::lookup((int)m_addOnSubscribe); - s << " add_onprobe=" << XMPPDirVal::lookup((int)m_addOnProbe); - s << " add_onpresence=" << XMPPDirVal::lookup((int)m_addOnPresence); - s << " auto_probe=" << String::boolText(m_autoProbe); - s << " probe_interval=" << (unsigned int)m_probeInterval; - s << " expire_interval=" << (unsigned int)m_expireInterval; - Debug(this,DebugInfo,"Jabber Presence service initialized:%s [%p]", - s.c_str(),this); - } - - if (!m_initialized) { - m_initialized = true; - int c = params.getIntValue("private_process_threads",1); - for (int i = 0; i < c; i++) - JBThread::start(JBThread::Presence,this,this); - } -} - -// Accept an event from the engine -bool JBPresence::accept(JBEvent* event, bool& processed, bool& insert) -{ - if (!event) - return false; - bool disco = false; - switch (event->type()) { - case JBEvent::IqDiscoInfoGet: - case JBEvent::IqDiscoInfoSet: - case JBEvent::IqDiscoInfoRes: - case JBEvent::IqDiscoInfoErr: - case JBEvent::IqDiscoItemsGet: - case JBEvent::IqDiscoItemsSet: - case JBEvent::IqDiscoItemsRes: - case JBEvent::IqDiscoItemsErr: - disco = true; - case JBEvent::Presence: - insert = false; - break; - case JBEvent::IqRosterRes: - case JBEvent::IqRosterErr: - insert = true; - return true; - default: - return false; - } - - JabberID jid(event->to()); - // Check destination. Don't do that for client streams: already done - // Don't accept disco stanzas without node (reroute them to the engine) - // Presence stanzas might be a brodcast (no 'to' attribute) - if (disco) { - if (!jid.node()) - return false; - if (validDomain(jid.domain())) - return true; - } - else if (event->stream() && event->stream()->type() == JBEngine::Client) - return true; - else if (!event->to() || validDomain(jid.domain())) - return true; - - if (m_ignoreNonRoster) - DDebug(this,DebugNote,"Received element with invalid domain '%s' [%p]", - jid.domain().c_str(),this); - else { - Debug(this,DebugNote,"Received element with invalid domain '%s' [%p]", - jid.domain().c_str(),this); - // Respond only if stanza is not a response - if (event->stanzaType() != "error" && event->stanzaType() != "result") - sendStanza(event->createError(XMPPError::TypeModify,XMPPError::SNoRemote),event->stream()); - } - processed = true; - return true; -} - -// Process received events -bool JBPresence::process() -{ - if (Thread::check(false)) - return false; - Lock lock(this); - JBEvent* event = deque(); - if (!event) - return false; - switch (event->type()) { - case JBEvent::IqDiscoInfoGet: - case JBEvent::IqDiscoInfoSet: - case JBEvent::IqDiscoInfoRes: - case JBEvent::IqDiscoInfoErr: - case JBEvent::IqDiscoItemsGet: - case JBEvent::IqDiscoItemsSet: - case JBEvent::IqDiscoItemsRes: - case JBEvent::IqDiscoItemsErr: - processDisco(event); - TelEngine::destruct(event); - return true; - default: ; - } - DDebug(this,DebugAll,"Process presence: '%s' [%p]",event->stanzaType().c_str(),this); - Presence p = presenceType(event->stanzaType()); - switch (p) { - case JBPresence::Error: - processError(event); - break; - case JBPresence::Probe: - processProbe(event); - break; - case JBPresence::Subscribe: - case JBPresence::Subscribed: - case JBPresence::Unsubscribe: - case JBPresence::Unsubscribed: - processSubscribe(event,p); - break; - case JBPresence::Unavailable: - // Check destination only if we have one. No destination: broadcast - processUnavailable(event); - break; - default: - // Simple presence shouldn't have a type - if (event->element()->getAttribute("type")) { - if (m_ignoreNonRoster) - break; - Debug(this,DebugNote, - "Received unexpected presence type=%s from=%s to=%s [%p]", - event->element()->getAttribute("type"),event->from().c_str(), - event->to().c_str(),this); - sendStanza(event->createError(XMPPError::TypeModify,XMPPError::SFeatureNotImpl), - event->stream()); - break; - } - processPresence(event); - } - TelEngine::destruct(event); - return true; -} - -// Check timeouts for all users' roster -void JBPresence::checkTimeout(u_int64_t time) -{ - lock(); - ListIterator iter(m_rosters); - for (;;) { - if (Thread::check(false)) - break; - XMPPUserRoster* ur = static_cast(iter.get()); - // End of iteration? - if (!ur) - break; - // Get a reference - RefPointer sref = ur; - if (!sref) - continue; - // Check timeout - unlock(); - if (sref->timeout(time)) { - lock(); - m_rosters.remove(ur,true); - unlock(); - } - lock(); - } - unlock(); -} - - -// Process received disco -void JBPresence::processDisco(JBEvent* event) -{ - XDebug(this,DebugAll,"processDisco event=(%p,%s) local=%s remote=%s [%p]", - event,event->name(),event->to().c_str(),event->from().c_str(),this); - if (event->type() != JBEvent::IqDiscoInfoGet || !event->stream()) - return; - - XMLElement* rsp = 0; - JabberID from(event->to()); - XMPPUserRoster* roster = getRoster(event->to(),false,0); - if (roster) { - XMPPUser* user = roster->getUser(event->from()); - bool ok = false; - if (user) { - Lock lock(user); - if (from.resource()) - ok = (0 != user->m_localRes.get(from.resource())); - else { - JIDResource* res = user->m_localRes.getFirst(); - if (res) { - ok = true; - from.resource(res->name()); - } - } - } - if (ok) - rsp = roster->createDiscoInfoResult(from,event->from(),event->id()); - TelEngine::destruct(user); - TelEngine::destruct(roster); - } - - if (!rsp && !m_ignoreNonRoster) { - if (from.resource().null() && engine()) - from.resource(engine()->defaultResource()); - rsp = XMPPUtils::createDiscoInfoRes(from,event->from(),event->id(), - &m_defFeatures,m_defIdentity); - } - - if (rsp) - sendStanza(rsp,event->stream()); -} - -void JBPresence::processError(JBEvent* event) -{ - XDebug(this,DebugAll,"processError event=(%p,%s) local=%s remote=%s [%p]", - event,event->name(),event->to().c_str(),event->from().c_str(),this); - XMPPUser* user = recvGetRemoteUser("error",event->to(),event->from()); - if (user) - user->processError(event); - TelEngine::destruct(user); -} - -void JBPresence::processProbe(JBEvent* event) -{ - XDebug(this,DebugAll,"processProbe event=(%p,%s) local=%s remote=%s [%p]", - event,event->name(),event->to().c_str(),event->from().c_str(),this); - bool newUser = false; - XMPPUser* user = recvGetRemoteUser("probe",event->to(),event->from(), - m_addOnProbe.from(),0,m_addOnProbe.from(),&newUser); - if (!user) { - if (m_autoProbe) { - XMLElement* stanza = createPresence(event->to().bare(),event->from()); - JIDResource* resource = new JIDResource(engine()->defaultResource(), - JIDResource::Available,JIDResource::CapAudio); - resource->addTo(stanza); - TelEngine::destruct(resource); - if (event->stream()) - event->stream()->sendStanza(stanza); - else - TelEngine::destruct(stanza); - } - else if (!notifyProbe(event) && !m_ignoreNonRoster) - sendStanza(event->createError(XMPPError::TypeModify,XMPPError::SItemNotFound), - event->stream()); - return; - } - if (newUser) - notifyNewUser(user); - String resName = event->to().resource(); - if (resName.null()) - user->processProbe(event); - else - user->processProbe(event,&resName); - TelEngine::destruct(user); -} - -void JBPresence::processSubscribe(JBEvent* event, Presence presence) -{ - XDebug(this,DebugAll, - "processSubscribe '%s' event=(%p,%s) local=%s remote=%s [%p]", - presenceText(presence),event,event->name(), - event->to().c_str(),event->from().c_str(),this); - bool addLocal = (presence == Subscribe) ? m_addOnSubscribe.from() : false; - bool newUser = false; - XMPPUser* user = recvGetRemoteUser(presenceText(presence),event->to(),event->from(), - addLocal,0,addLocal,&newUser); - if (!user) { - if (!notifySubscribe(event,presence) && - (presence != Subscribed && presence != Unsubscribed) && - !m_ignoreNonRoster) - sendStanza(event->createError(XMPPError::TypeModify,XMPPError::SItemNotFound), - event->stream()); - return; - } - if (newUser) - notifyNewUser(user); - user->processSubscribe(event,presence); - TelEngine::destruct(user); -} - -void JBPresence::processUnavailable(JBEvent* event) -{ - XDebug(this,DebugAll,"processUnavailable event=(%p,%s) local=%s remote=%s [%p]", - event,event->name(),event->to().c_str(),event->from().c_str(),this); - // Don't add if delUnavailable is true - bool addLocal = m_addOnPresence.from() && !m_delUnavailable; - // Check if broadcast - if (event->to().null()) { - Lock lock(this); - ObjList* obj = m_rosters.skipNull(); - for (; obj; obj = obj->skipNext()) { - XMPPUserRoster* roster = static_cast(obj->get()); - bool newUser = false; - XMPPUser* user = getRemoteUser(roster->jid(),event->from(),addLocal,0, - addLocal,&newUser); - if (!user) - continue; - if (newUser) - notifyNewUser(user); - if (!user->processPresence(event,false)) - removeRemoteUser(event->to(),event->from()); - TelEngine::destruct(user); - } - return; - } - // Not broadcast: find user - bool newUser = false; - XMPPUser* user = recvGetRemoteUser("unavailable",event->to(),event->from(), - addLocal,0,addLocal,&newUser); - if (!user) { - if (!notifyPresence(event,false) && !m_ignoreNonRoster) - sendStanza(event->createError(XMPPError::TypeModify,XMPPError::SItemNotFound), - event->stream()); - return; - } - if (newUser) - notifyNewUser(user); - if (!user->processPresence(event,false)) - removeRemoteUser(event->to(),event->from()); - TelEngine::destruct(user); -} - -void JBPresence::processPresence(JBEvent* event) -{ - XDebug(this,DebugAll,"processPresence event=(%p,%s) local=%s remote=%s [%p]", - event,event->name(),event->to().c_str(),event->from().c_str(),this); - // Check if broadcast - if (event->to().null()) { - Lock lock(this); - ObjList* obj = m_rosters.skipNull(); - for (; obj; obj = obj->skipNext()) { - XMPPUserRoster* roster = static_cast(obj->get()); - bool newUser = false; - XMPPUser* user = getRemoteUser(roster->jid(),event->from(), - m_addOnPresence.from(),0,m_addOnPresence.from(),&newUser); - if (!user) - continue; - if (newUser) - notifyNewUser(user); - user->processPresence(event,true); - TelEngine::destruct(user); - } - return; - } - // Not broadcast: find user - bool newUser = false; - XMPPUser* user = recvGetRemoteUser("",event->to(),event->from(), - m_addOnPresence.from(),0,m_addOnPresence.from(),&newUser); - if (!user) { - if (!notifyPresence(event,true) && !m_ignoreNonRoster) - sendStanza(event->createError(XMPPError::TypeModify,XMPPError::SItemNotFound), - event->stream()); - return; - } - if (newUser) - notifyNewUser(user); - user->processPresence(event,true); - TelEngine::destruct(user); -} - -bool JBPresence::notifyProbe(JBEvent* event) -{ - DDebug(this,DebugStub,"notifyProbe local=%s remote=%s [%p]", - event->to().c_str(),event->from().c_str(),this); - return false; -} - -bool JBPresence::notifySubscribe(JBEvent* event, Presence presence) -{ - DDebug(this,DebugStub,"notifySubscribe local=%s remote=%s [%p]", - event->to().c_str(),event->from().c_str(),this); - return false; -} - -void JBPresence::notifySubscribe(XMPPUser* user, Presence presence) -{ - DDebug(this,DebugStub,"notifySubscribe user=%p [%p]",user,this); -} - -bool JBPresence::notifyPresence(JBEvent* event, bool available) -{ - DDebug(this,DebugStub,"notifyPresence local=%s remote=%s [%p]", - event->to().c_str(),event->from().c_str(),this); - return false; -} - -void JBPresence::notifyPresence(XMPPUser* user, JIDResource* resource) -{ - DDebug(this,DebugStub,"notifyPresence user=%p [%p]",user,this); -} - -void JBPresence::notifyNewUser(XMPPUser* user) -{ - DDebug(this,DebugStub,"notifyNewUser user=%p [%p]",user,this); -} - -// Get a user's roster. Add a new one if requested -XMPPUserRoster* JBPresence::getRoster(const JabberID& jid, bool add, bool* added) -{ - if (jid.node().null() || jid.domain().null()) - return 0; - Lock lock(this); - ObjList* obj = m_rosters.skipNull(); - for (; obj; obj = obj->skipNext()) { - XMPPUserRoster* ur = static_cast(obj->get()); - if (jid.bare() &= ur->jid().bare()) - return ur->ref() ? ur : 0; - } - if (!add) - return 0; - if (added) - *added = true; - XMPPUserRoster* ur = new XMPPUserRoster(this,jid.node(),jid.domain()); - DDebug(this,DebugAll,"Added roster for %s [%p]",jid.bare().c_str(),this); - return ur->ref() ? ur : 0; -} - -// Get a remote user's roster -XMPPUser* JBPresence::getRemoteUser(const JabberID& local, const JabberID& remote, - bool addLocal, bool* addedLocal, bool addRemote, bool* addedRemote) -{ - DDebug(this,DebugAll,"getRemoteUser local=%s add=%s remote=%s add=%s [%p]", - local.bare().c_str(),String::boolText(addLocal), - remote.bare().c_str(),String::boolText(addRemote),this); - XMPPUserRoster* ur = getRoster(local,addLocal,addedLocal); - if (!ur) - return 0; - XMPPUser* user = ur->getUser(remote,addRemote,addedRemote); - TelEngine::destruct(ur); - return user; -} - -void JBPresence::removeRemoteUser(const JabberID& local, const JabberID& remote) -{ - Lock lock(this); - ObjList* obj = m_rosters.skipNull(); - XMPPUserRoster* ur = 0; - for (; obj; obj = obj->skipNext()) { - ur = static_cast(obj->get()); - if (local.bare() &= ur->jid().bare()) { - if (ur->removeUser(remote)) - ur = 0; - break; - } - ur = 0; - } - if (ur) - m_rosters.remove(ur,true); -} - -// Check if a ddestination domain is a valid one -bool JBPresence::validDomain(const String& domain) -{ - if (engine()->getAlternateDomain() && - (engine()->getAlternateDomain().domain() &= domain)) - return true; - XMPPServerInfo* server = engine()->findServerInfo(engine()->componentServer(),true); - if (!server) - return false; - bool ok = ((domain &= server->identity()) || (domain &= server->fullIdentity())); - return ok; -} - -// Send a stanza -bool JBPresence::sendStanza(XMLElement* element, JBStream* stream) -{ - if (!element) - return true; - bool release = false; - if (!engine()->getStream(stream,release)) { - TelEngine::destruct(element); - return false; - } - JBStream::Error res = stream->sendStanza(element); - if (release) - TelEngine::destruct(stream); - if (res == JBStream::ErrorContext || - res == JBStream::ErrorNoSocket) - return false; - return true; -} - -// Create a presence stanza -XMLElement* JBPresence::createPresence(const char* from, - const char* to, Presence type) -{ - XMLElement* presence = new XMLElement(XMLElement::Presence); - presence->setAttributeValid("type",presenceText(type)); - presence->setAttribute("from",from); - presence->setAttribute("to",to); - return presence; -} - -// Decode a presence error stanza -bool JBPresence::decodeError(const XMLElement* element, - String& code, String& type, String& error) -{ - if (!(element && element->type() == XMLElement::Presence)) - return false; - code = ""; - type = ""; - error = ""; - XMLElement* child = ((XMLElement*)element)->findFirstChild("error"); - if (!child) - return true; - child->getAttribute("code",code); - child->getAttribute("type",type); - XMLElement* tmp = child->findFirstChild(); - TelEngine::destruct(child); - if (tmp) { - error = tmp->name(); - TelEngine::destruct(tmp); - } - return true; -} - -void JBPresence::cleanup() -{ - Lock lock(this); - DDebug(this,DebugAll,"Cleanup [%p]",this); - ListIterator iter(m_rosters); - GenObject* obj; - for (; (obj = iter.get());) { - XMPPUserRoster* ur = static_cast(obj); - ur->cleanup(); - m_rosters.remove(ur,true); - } -} - -inline XMPPUser* JBPresence::recvGetRemoteUser(const char* type, - const JabberID& local, const JabberID& remote, - bool addLocal, bool* addedLocal, bool addRemote, bool* addedRemote) -{ - XMPPUser* user = getRemoteUser(local,remote,addLocal,addedLocal,addRemote,addedRemote); - if (!user) - Debug(this,DebugAll, - "No destination for received presence type=%s local=%s remote=%s [%p]", - type,local.c_str(),remote.c_str(),this); - return user; -} - -void JBPresence::addRoster(XMPPUserRoster* ur) -{ - Lock lock(this); - m_rosters.append(ur); -} - -void JBPresence::removeRoster(XMPPUserRoster* ur) -{ - Lock lock(this); - m_rosters.remove(ur,false); -} - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/jbstream.cpp b/libs/yjingle/jbstream.cpp deleted file mode 100644 index f04a87bd..00000000 --- a/libs/yjingle/jbstream.cpp +++ /dev/null @@ -1,2384 +0,0 @@ -/** - * jbstream.cpp - * Yet Another Jabber Component Protocol Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include - -#include -#include -#include - -using namespace TelEngine; - -static XMPPNamespace s_ns; // Just use the operators -static XMPPError s_err; -static String s_qop = "auth"; // Used to build Digest MD5 SASL - -static TokenDict s_streamState[] = { - {"Idle", JBStream::Idle}, - {"Connecting", JBStream::Connecting}, - {"Started", JBStream::Started}, - {"Securing", JBStream::Securing}, - {"Register", JBStream::Register}, - {"Auth", JBStream::Auth}, - {"Running", JBStream::Running}, - {"Destroy", JBStream::Destroy}, - {0,0}, -}; - -TokenDict JBStream::s_flagName[] = { - {"autorestart", AutoRestart}, - {"noversion1", NoVersion1}, - {"noremoteversion1", NoRemoteVersion1}, - {"tls", UseTls}, - {"sasl", UseSasl}, - {"secured", StreamSecured}, - {"authenticated", StreamAuthenticated}, - {"allowplainauth", AllowPlainAuth}, - {"allowunsafesetup", AllowUnsafeSetup}, - {0,0} -}; - -static String s_version = "1.0"; // Protocol version -static String s_declaration = ""; - -// Utility: append a quoted param to a string -inline void appendQParam(String& dest, const char* name, const char* value, - bool quotes, bool first = false) -{ - if (!first) - dest << ","; - dest << name << "="; - if (quotes) - dest << "\"" << value << "\""; - else - dest << value; -} - -// Check if an element exists and have an expected namespace -inline bool checkValidXmlns(XMLElement* e, XMPPNamespace::Type ns, - XMPPError::Type& error) -{ - if (e && XMPPUtils::hasXmlns(*e,ns)) - return true; - error = e ? XMPPError::SFeatureNotImpl : XMPPError::SBadRequest; - return false; -} - -#define DROP_AND_EXIT { dropXML(xml); return; } -#define INVALIDXML_AND_EXIT(code,reason) { invalidStreamXML(xml,code,reason); return; } -#define ERRORXML_AND_EXIT { errorStreamXML(xml); return; } - -/** - * JBSocket - */ - -JBSocket::JBSocket(JBEngine* engine, JBStream* stream, - const char* address, int port) - : m_engine(engine), m_stream(stream), m_socket(0), m_remoteDomain(address), - m_address(PF_INET), - m_streamMutex(true,"JBSocket::stream"), - m_receiveMutex(true,"JBSocket::receive") -{ - m_address.port(port); -} - -// Connect the socket -bool JBSocket::connect(bool& terminated, const char* newAddr, int newPort) -{ - terminate(); - Lock2 lck1(m_streamMutex,m_receiveMutex); - m_socket = new Socket(PF_INET,SOCK_STREAM); - // Set new connection data. Resolve remote domain - if (newAddr) - m_remoteDomain = newAddr; - if (newPort) - m_address.port(newPort); - lck1.drop(); - m_address.host(m_remoteDomain); - Thread::check(true); - if (!m_address.host()) { - m_error = "Resolver failure"; - DDebug(m_engine,DebugWarn,"Stream. Failed to resolve '%s' [%p]", - m_remoteDomain.safe(),m_stream); - terminated = (m_socket == 0); - terminate(); - return false; - } - DDebug(m_engine,DebugInfo,"Stream. Attempt to connect to '%s:%d' [%p]", - m_address.host().safe(),m_address.port(),m_stream); - terminated = false; - bool res = m_socket && m_socket->connect(m_address); - Thread::check(true); - // Lock again to update data - Lock2 lck2(m_streamMutex,m_receiveMutex); - bool ok = false; - while (true) { - if (!m_socket) { - Debug(m_engine,DebugMild, - "Stream. Socket deleted while connecting [%p]",m_stream); - terminated = true; - break; - } - // Check connect result - if (!res) { - m_error = ""; - Thread::errorString(m_error,m_socket->error()); - if (m_error.null()) - m_error = "Socket connect failure"; - Debug(m_engine,DebugWarn, - "Stream. Failed to connect socket to '%s:%d'. %d: '%s' [%p]", - m_address.host().c_str(),m_address.port(), - m_socket->error(),m_error.c_str(),m_stream); - break; - } - // Connected - ok = true; - m_socket->setBlocking(false); - DDebug(m_engine,DebugAll,"Stream. Connected to '%s:%d'. [%p]", - m_address.host().c_str(),m_address.port(),m_stream); - break; - } - lck2.drop(); - if (!ok) - terminate(); - return ok; -} - -// Close the socket -void JBSocket::terminate(bool shutdown) -{ - Lock2 lck(m_streamMutex,m_receiveMutex); - if (!m_socket) - return; - Socket* tmp = m_socket; - m_socket = 0; - Debug(m_engine,DebugInfo, - "Stream. Terminating socket shutdown=%s error=%s [%p]", - String::boolText(shutdown),m_error.c_str(),m_stream); - m_error = ""; - lck.drop(); - if (shutdown) - tmp->shutdown(true,true); - else { - tmp->setLinger(-1); - tmp->terminate(); - } - delete tmp; -} - -// Read data from socket -bool JBSocket::recv(char* buffer, unsigned int& len) -{ - if (!valid()) { - if (m_error.null()) - m_error = "Socket read failure"; - return false; - } - - int read = m_socket->readData(buffer,len); - if (read != Socket::socketError()) { - if (!read) - Debug(m_engine,DebugInfo,"Stream EOF [%p]",this); -#ifdef XDEBUG - else { - String s(buffer,read); - Debug(m_engine,DebugAll,"Stream recv [%p]\r\n%s", - m_stream,s.c_str()); - } -#endif - len = read; - return (len != 0); - } - - len = 0; - if (!m_socket->canRetry()) { - m_error = ::strerror(m_socket->error()); - if (m_error.null()) - m_error = "Socket read failure"; - Debug(m_engine,DebugWarn, - "Stream. Socket read error: %d: '%s' [%p]", - m_socket->error(),::strerror(m_socket->error()),m_stream); - return false; - } - return true; -} - -// Write data to socket -bool JBSocket::send(const char* buffer, unsigned int& len) -{ - if (!valid()) { - if (m_error.null()) - m_error = "Socket write failure"; - return false; - } - - XDebug(m_engine,DebugAll,"Stream sending %s [%p]",buffer,m_stream); - int c = m_socket->writeData(buffer,len); - if (c != Socket::socketError()) { - len = c; - return true; - } - if (!m_socket->canRetry()) { - m_error = ::strerror(m_socket->error()); - if (m_error.null()) - m_error = "Socket write failure"; - Debug(m_engine,DebugWarn,"Stream. Socket send error: %d: '%s' [%p]", - m_socket->error(),::strerror(m_socket->error()),m_stream); - return false; - } - len = 0; - DDebug(m_engine,DebugMild, - "Stream. Socket temporary unavailable to send: %d: '%s' [%p]", - m_socket->error(),::strerror(m_socket->error()),m_stream); - return true; -} - - -/** - * JBStream - */ - -// Outgoing -JBStream::JBStream(JBEngine* engine, int type, XMPPServerInfo& info, - const JabberID& localJid, const JabberID& remoteJid) - : m_password(info.password()), m_flags(0), m_challengeCount(2), - m_waitState(WaitIdle), m_authMech(JIDFeatureSasl::MechNone), m_register(false), m_type(type), - m_state(Idle), m_outgoing(true), m_restart(0), m_restartMax(0), - m_timeToFillRestart(0), m_setupTimeout(0), m_idleTimeout(0), - m_local(localJid.node(),localJid.domain(),localJid.resource()), - m_remote(remoteJid.node(),remoteJid.domain(),remoteJid.resource()), - m_engine(engine), m_socket(engine,0,info.address(),info.port()), - m_lastEvent(0), m_terminateEvent(0), m_startEvent(0), m_recvCount(-1), - m_streamXML(0), m_declarationSent(0), m_nonceCount(0), m_registerId(0) -{ - m_socket.m_stream = this; - - if (!engine) { - Debug(DebugNote,"Can't create stream without engine [%p]",this); - return; - } - - // Update options from server info - if (!info.flag(XMPPServerInfo::NoAutoRestart)) - m_flags |= AutoRestart; - // Stream version supported by server - if (info.flag(XMPPServerInfo::OldStyleAuth)) - m_flags |= NoVersion1; - else { - // Force stream encryption if required by config - if (info.flag(XMPPServerInfo::TlsRequired)) - m_flags |= UseTls; - // Use RFC-3920 SASL instead of XEP-0078 authentication - m_flags |= UseSasl; - } - // Allow plain auth - if (info.flag(XMPPServerInfo::AllowPlainAuth)) - m_flags |= AllowPlainAuth; - // Allow unsecure user registration - if (info.flag(XMPPServerInfo::AllowUnsafeSetup)) - m_flags |= AllowUnsafeSetup; - - // Restart counter and update interval - if (flag(AutoRestart)) - m_restartMax = m_restart = engine->m_restartCount; - else - m_restartMax = m_restart = 1; - m_timeToFillRestart = Time::msecNow() + engine->m_restartUpdateInterval; - - if (m_engine->debugAt(DebugAll)) { - String f; - XMPPUtils::buildFlags(f,m_flags,s_flagName); - Debug(m_engine,DebugAll, - "Stream dir=outgoing type=%s local=%s remote=%s options=%s [%p]", - JBEngine::lookupProto(m_type),m_local.safe(),m_remote.safe(), - f.c_str(),this); - } -} - -JBStream::~JBStream() -{ - XDebug(m_engine,DebugAll,"JBStream::~JBStream() [%p]",this); -} - -// Close the stream. Release memory -void JBStream::destroyed() -{ - XDebug(m_engine,DebugAll,"Stream::destroyed() state=%s [%p]",lookupState(state()),this); - if (m_engine) { - Lock lock(m_engine); - m_engine->m_streams.remove(this,false); - } - terminate(false,0,XMPPError::NoError,0,false,true); - // m_terminateEvent shouldn't be valid: - // do that to print a DebugFail output for the stream inside the event - TelEngine::destruct(m_terminateEvent); - TelEngine::destruct(m_startEvent); - DDebug(m_engine,DebugAll,"Stream destroyed local=%s remote=%s [%p]", - m_local.safe(),m_remote.safe(),this); - RefObject::destroyed(); -} - -// Check the 'to' attribute of a received element -bool JBStream::checkDestination(XMLElement* xml, bool& respond) -{ - respond = false; - return true; -} - -// Connect the stream -void JBStream::connect() -{ - Lock2 lck(m_socket.m_streamMutex,m_socket.m_receiveMutex); - if (state() != Idle) { - Debug(m_engine,state()!=Connecting?DebugNote:DebugAll, - "Stream. Attempt to connect when not idle [%p]",this); - return; - } - DDebug(m_engine,DebugInfo, - "Stream. Attempt to connect local=%s remote=%s count=%u [%p]", - m_local.safe(),m_remote.safe(),m_restart,this); - // Check if we can restart. Destroy the stream if not auto restarting - if (m_restart) - m_restart--; - else - return; - // Reset data - m_id = ""; - m_parser.reset(); - lck.drop(); - // Re-connect socket - bool terminated = false; - changeState(Connecting); - // TODO: check with the engine if server info is available - // get address and port and pass them to socket - if (!m_socket.connect(terminated,0,0)) { - if (!terminated) - terminate(false,0,XMPPError::HostGone,m_socket.error(),false); - return; - } - - Debug(m_engine,DebugAll,"Stream. local=%s remote=%s connected to %s:%d [%p]", - m_local.safe(),m_remote.safe(),addr().host().safe(),addr().port(),this); - - m_setupTimeout = 0; - if (m_engine && m_engine->m_streamSetupInterval) - m_setupTimeout = Time::msecNow() + m_engine->m_streamSetupInterval; - - // Send stream start - sendStreamStart(); -} - -// Read data from socket and pass it to the parser -// Terminate stream on parser or socket error -bool JBStream::receive() -{ - char buf[1024]; - if (!m_recvCount || state() == Securing || state() == Destroy || - state() == Idle || state() == Connecting) - return false; - - XMPPError::Type error = XMPPError::NoError; - bool send = false; - // Lock between start read and end consume to serialize input - m_socket.m_receiveMutex.lock(); - const char* text = 0; - unsigned int len = (m_recvCount < 0 ? sizeof(buf) : 1); - if (m_socket.recv(buf,len)) { - if (len) { - XDebug(m_engine,DebugAll,"Stream. Received %u bytes [%p]",len,this); - if (!m_parser.consume(buf,len)) { - bool full = (m_parser.bufLen() > m_parser.s_maxDataBuffer); - text = m_parser.ErrorDesc(); - // Don't display the buffer if full - String tmp; - if (!full) - m_parser.getBuffer(tmp); - else - tmp << "overflow len=" << m_parser.bufLen() << " max=" << - m_parser.s_maxDataBuffer; - Debug(m_engine,DebugNote,"Stream. Parser error='%s' buffer='%s' [%p]", - text,tmp.c_str(),this); - error = full ? XMPPError::Internal : XMPPError::Xml; - send = true; - } - else - startIdleTimer(); - // Check if the parser consumed all it's buffer and the stream - // will start TLS - if (!m_parser.bufLen() && m_recvCount > 0) - setRecvCount(0); - } - } - else { - error = XMPPError::HostGone; - text = "remote server not found"; - } - m_socket.m_receiveMutex.unlock(); - if (error != XMPPError::NoError) - terminate(false,0,error,text,send); - return len != 0; -} - -// Send a stanza -JBStream::Error JBStream::sendStanza(XMLElement* stanza, const char* senderId) -{ - if (!stanza) - return ErrorContext; - - Lock lock(m_socket.m_streamMutex); - if (state() == Destroy) { - Debug(m_engine,DebugNote, - "Stream. Can't send stanza (%p,%s). Stream is destroying [%p]", - stanza,stanza->name(),this); - TelEngine::destruct(stanza); - return ErrorContext; - } - - DDebug(m_engine,DebugAll,"Stream. Posting stanza (%p,%s) id='%s' [%p]", - stanza,stanza->name(),senderId,this); - XMLElementOut* e = new XMLElementOut(stanza,senderId); - // List not empty: the return value will be ErrorPending - // Else: element will be sent - bool pending = (0 != m_outXML.skipNull()); - m_outXML.append(e); - // Send first element - Error result = sendPending(); - return pending ? ErrorPending : result; -} - -// Extract an element from parser and construct an event -JBEvent* JBStream::getEvent(u_int64_t time) -{ - Lock lock(m_socket.m_streamMutex); - - if (m_lastEvent) - return 0; - - if (!m_engine) { - Debug(DebugMild,"Stream. Engine vanished. Can't live as orphan [%p]",this); - terminate(true,0,XMPPError::Internal,"Engine is missing",false); - if (m_terminateEvent) { - m_lastEvent = m_terminateEvent; - m_terminateEvent = 0; - } - return m_lastEvent; - } - - // Increase stream restart counter if it's time to and should auto restart - if (flag(AutoRestart) && m_timeToFillRestart < time) { - m_timeToFillRestart = time + m_engine->m_restartUpdateInterval; - if (m_restart < m_restartMax) { - m_restart++; - Debug(m_engine,DebugAll,"Stream. restart count=%u max=%u [%p]", - m_restart,m_restartMax,this); - } - } - - // Do nothing if destroying or connecting - // Just check Terminated or Running events - // Idle: check if we can restart. Destroy the stream if not auto restarting - if (state() == Idle || state() == Destroy || state() == Connecting) { - if (state() == Idle) { - if (m_restart) { - lock.drop(); - m_engine->connect(this); - return 0; - } - if (!flag(AutoRestart)) - terminate(true,0,XMPPError::NoError,"connection-failed",false); - } - if (m_terminateEvent) { - m_lastEvent = m_terminateEvent; - m_terminateEvent = 0; - } - else if (m_startEvent) { - m_lastEvent = m_startEvent; - m_startEvent = 0; - } - return m_lastEvent; - } - - while (true) { - if (m_terminateEvent) - break; - - // Send pending elements and process the received ones - sendPending(); - if (m_terminateEvent) - break; - - // Process the received XML - XMLElement* xml = m_parser.extract(); - if (!xml) - break; - - // Print it - m_engine->printXml(*xml,this,false); - - // Check destination - bool respond = false; - if (!checkDestination(xml,respond)) { - String type = xml->getAttribute("type"); - Debug(m_engine,DebugNote, - "Stream. Received %s with unacceptable destination to=%s type=%s [%p]", - xml->name(),xml->getAttribute("to"),type.c_str(),this); - if (state() == Running) - if (respond) - switch (xml->type()) { - case XMLElement::Iq: - case XMLElement::Presence: - case XMLElement::Message: - if (type != "error" && type != "result") { - sendStanza(XMPPUtils::createError(xml,XMPPError::TypeModify, - XMPPError::HostUnknown,"Unknown destination")); - break; - } - default: - dropXML(xml); - } - else - dropXML(xml); - else if (respond || this->type() == JBEngine::Client) - invalidStreamXML(xml,XMPPError::HostUnknown,"Received invalid destination"); - break; - } - - // Check if stream end was received (end tag or error) - if (xml->type() == XMLElement::StreamEnd || - xml->type() == XMLElement::StreamError) { - Debug(m_engine,DebugAll,"Stream. Remote closed in state %s [%p]", - lookupState(state()),this); - terminate(false,xml,XMPPError::NoError,xml->getText(),false); - break; - } - - XDebug(m_engine,DebugAll,"Stream. Processing (%p,%s) in state %s [%p]", - xml,xml->name(),lookupState(state()),this); - - switch (state()) { - case Running: - processRunning(xml); - break; - case Auth: - processAuth(xml); - break; - case Securing: - processSecuring(xml); - break; - case Started: - // Set stream id if not already set - if (!m_id) { - if (xml->type() != XMLElement::StreamStart) { - dropXML(xml); - break; - } - m_id = xml->getAttribute("id"); - if (!m_id || m_engine->checkDupId(this)) { - invalidStreamXML(xml,XMPPError::InvalidId,"Duplicate stream id"); - break; - } - DDebug(m_engine,DebugAll,"Stream. Id set to '%s' [%p]", - m_id.c_str(),this); - } - processStarted(xml); - break; - case Register: - processRegister(xml); - break; - default: - Debug(m_engine,DebugStub,"Unhandled stream state %u '%s' [%p]", - state(),lookupState(state()),this); - TelEngine::destruct(xml); - } - break; - } - - // Return terminate event if set - // Get events from queue if not set to terminate - if (m_terminateEvent) { - m_lastEvent = m_terminateEvent; - m_terminateEvent = 0; - } - else if (m_startEvent) { - m_lastEvent = m_startEvent; - m_startEvent = 0; - } - else { - ObjList* obj = m_events.skipNull(); - m_lastEvent = obj ? static_cast(obj->get()) : 0; - if (m_lastEvent) - m_events.remove(m_lastEvent,false); - } - - // Check timers if no events - if (!m_lastEvent) { - if (m_idleTimeout && time > m_idleTimeout) { - if (startIdleTimer(time)) { - DDebug(m_engine,DebugAll,"Stream. Sending keep alive in state %s [%p]", - lookupState(state()),this); - const char* keepAlive = "\t"; - unsigned int l = 1; - if (!m_socket.send(keepAlive,l)) { - // Keep the reason: terminate() might override the last socket error - String reason = m_socket.m_error; - terminate(false,0,XMPPError::HostGone,reason,true); - } - } - } - else if (m_setupTimeout && time > m_setupTimeout) { - Debug(m_engine,DebugNote,"Stream. Setup timed out in state %s [%p]", - lookupState(state()),this); - terminate(true,0,XMPPError::ConnTimeout,"Connection timeout",true); - } - if (m_terminateEvent) { - m_lastEvent = m_terminateEvent; - m_terminateEvent = 0; - } - } - -#ifdef DEBUG - if (m_lastEvent) - Debug(m_engine,DebugAll,"Stream. Raising event (%p,%s) [%p]", - m_lastEvent,m_lastEvent->name(),this); -#endif - return m_lastEvent; -} - -// Terminate stream. Send stream end tag or error. Remove pending stanzas without id -// Deref stream if destroying -void JBStream::terminate(bool destroy, XMLElement* recvStanza, XMPPError::Type error, - const char* reason, bool send, bool final, bool sendError) -{ - XDebug(m_engine,DebugAll,"Stream::terminate(%u,%p,%u,%s,%u,%u) state=%s [%p]", - destroy,recvStanza,error,reason,send,final,lookupState(state()),this); - Lock2 lock(m_socket.m_streamMutex,m_socket.m_receiveMutex); - if (!flag(AutoRestart)) - destroy = true; - setRecvCount(-1); - m_nonceCount = 0; - m_setupTimeout = m_idleTimeout = 0; - TelEngine::destruct(m_startEvent); - if (m_streamXML) { - if (m_streamXML->dataCount()) - send = false; - TelEngine::destruct(m_streamXML); - } - if (state() == Destroy) { - resetStream(); - m_socket.terminate(true); - TelEngine::destruct(recvStanza); - return; - } - if (error == XMPPError::NoError && m_engine->exiting()) { - error = XMPPError::Shutdown; - reason = 0; - } - - Debug(m_engine,DebugAll, - "Stream. Terminate state=%s destroy=%u error=%s reason='%s' final=%u [%p]", - lookupState(state()),destroy,s_err[error],reason,final,this); - - // Send ending stream element - if (send && state() != Connecting && state() != Idle) { - XMLElement* e = 0; - if (sendError && error != XMPPError::NoError) { - e = XMPPUtils::createStreamError(error,reason); - // Add received element - // Preserve received element: we might generate an event - if (recvStanza) - e->addChild(new XMLElement(*recvStanza)); - } - bool ok = e ? sendStreamXML(e,m_state) : true; - if (ok) - sendStreamXML(new XMLElement(XMLElement::StreamEnd),m_state); - } - m_socket.terminate(state() == Connecting); - - // Done if called from destructor - if (final) { - changeState(Destroy); - resetStream(); - TelEngine::destruct(recvStanza); - return; - } - - // Cancel all outgoing elements without id - removePending(false,0,true); - // Always set termination event, except when exiting - if (!(m_terminateEvent || m_engine->exiting())) { - if (!recvStanza && error != XMPPError::NoError) - recvStanza = XMPPUtils::createStreamError(error,reason); - Debug(m_engine,DebugAll,"Stream. Set terminate error=%s reason=%s [%p]", - s_err[error],reason,this); - m_terminateEvent = new JBEvent(destroy?JBEvent::Destroy:JBEvent::Terminated, - this,recvStanza); - if (m_terminateEvent->m_text.null()) - m_terminateEvent->m_text = reason; - recvStanza = 0; - } - TelEngine::destruct(recvStanza); - - // Terminate - changeState(destroy ? Destroy : Idle); - resetStream(); - if (destroy) { - DDebug(m_engine,DebugAll,"Stream::terminate() deref() in state=%s [%p]", - lookupState(state()),this); - lock.drop(); - deref(); - } -} - -// Get an object from this stream -void* JBStream::getObject(const String& name) const -{ - if (name == "Socket*") - return state() == Securing ? (void*)&m_socket.m_socket : 0; - if (name == "JBStream") - return (void*)this; - return RefObject::getObject(name); -} - -// Get the name of a stream state -const char* JBStream::lookupState(int state) -{ - return lookup(state,s_streamState); -} - -// Get the starting stream element to be sent after stream connected -XMLElement* JBStream::getStreamStart() -{ - m_remoteFeatures.clear(); - m_parser.reset(); - m_waitState = WaitStart; - - XMLElement* start = XMPPUtils::createElement(XMLElement::StreamStart, - XMPPNamespace::Client); - start->setAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]); - start->setAttribute("to",remote()); - // Add version to notify the server we support RFC3920 TLS/SASL authentication - if (!flag(NoVersion1)) - start->setAttribute("version",s_version); - return start; -} - -// Get the authentication element to be sent when authentication starts -XMLElement* JBStream::getAuthStart() -{ - XMLElement* xml = 0; - // Deprecated XEP-0078 authentication - if (!flag(UseSasl)) { - xml = XMPPUtils::createIq(XMPPUtils::IqGet,0,0,"auth_1"); - xml->addChild(XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::IqAuth)); - m_waitState = WaitChallenge; - return xml; - } - // RFC 3920 SASL - if (m_authMech != JIDFeatureSasl::MechMD5 && - m_authMech != JIDFeatureSasl::MechPlain) - return 0; - String rsp; - // MD5: send auth element, wait challenge - // Plain auth: send auth element with credentials and wait response (success/failure) - if (m_authMech == JIDFeatureSasl::MechMD5) - m_waitState = WaitChallenge; - else { - buildSaslResponse(rsp); - m_waitState = WaitResponse; - } - xml = XMPPUtils::createElement(XMLElement::Auth,XMPPNamespace::Sasl,rsp); - xml->setAttribute("mechanism",lookup(m_authMech,JIDFeatureSasl::s_authMech)); - return xml; -} - -// Process received data while running -void JBStream::processRunning(XMLElement* xml) -{ - XDebug(m_engine,DebugAll,"JBStream::processRunning('%s') [%p]",xml->name(),this); - - switch (xml->type()) { - case XMLElement::Message: - m_events.append(new JBEvent(JBEvent::Message,this,xml)); - return; - case XMLElement::Presence: - m_events.append(new JBEvent(JBEvent::Presence,this,xml)); - return; - case XMLElement::Iq: - break; - default: - m_events.append(new JBEvent(JBEvent::Unhandled,this,xml)); - return; - } - - XMPPError::Type error = XMPPError::NoError; - int iq = XMPPUtils::iqType(xml->getAttribute("type")); - JBEvent* ev = getIqEvent(xml,iq,error); - if (ev) { - m_events.append(ev); - return; - } - if (error == XMPPError::NoError) { - m_events.append(new JBEvent(JBEvent::Unhandled,this,xml)); - return; - } - - // Don't respond to error or result - if (iq == XMPPUtils::IqError || iq == XMPPUtils::IqResult) { - dropXML(xml); - return; - } - - // Send error - sendStanza(XMPPUtils::createError(xml,XMPPError::TypeModify,error)); -} - -// Process a received element in Securing state -void JBStream::processSecuring(XMLElement* xml) -{ - Debug(m_engine,DebugInfo,"Stream. Received '%s' while securing the stream [%p]", - xml->name(),this); - dropXML(xml); -} - -// Process a received element in Register state -void JBStream::processRegister(XMLElement* xml) -{ - XDebug(m_engine,DebugAll,"JBStream::processRegister('%s') [%p]",xml->name(),this); - - XMPPUtils::IqType type = XMPPUtils::iqType(xml->getAttribute("type")); - String tmp = xml->getAttribute("id"); - unsigned int id = tmp.toInteger(); - if (xml->type() != XMLElement::Iq || - (type != XMPPUtils::IqResult && type != XMPPUtils::IqError) || - id != m_registerId) { - terminate(true,xml,XMPPError::UndefinedCondition,"Unacceptable response",true); - return; - } - - // Check for errors - if (type == XMPPUtils::IqError) { - String error, text; - XMPPUtils::decodeError(xml,error,text); - delete xml; - terminate(true,0,XMPPError::UndefinedCondition,text?text:error,false); - return; - } - - XMLElement* query = 0; - XMPPError::Type err = XMPPError::NoError; - const char* reason = "Invalid response"; -#define SETERR_BREAK(e,r) { err = e; reason = r; break; } - // Wait for register query response - if (m_registerId == 1) - while (true) { - XMLElement* query = xml->findFirstChild(XMLElement::Query); - if (!query) - SETERR_BREAK(XMPPError::UndefinedCondition,reason); - if (!XMPPUtils::hasXmlns(*query,XMPPNamespace::IqRegister)) - SETERR_BREAK(XMPPError::InvalidNamespace,reason); - // Check if already registered - XMLElement* r = query->findFirstChild(XMLElement::Registered); - if (r) { - m_register = false; - TelEngine::destruct(r); - break; - } - // Check if we have username/password - XMLElement* user = query->findFirstChild(XMLElement::Username); - XMLElement* pwd = query->findFirstChild(XMLElement::Password); - bool ok = (user && pwd); - TelEngine::destruct(user); - TelEngine::destruct(pwd); - if (!ok) - SETERR_BREAK(XMPPError::UndefinedCondition,"Unsupported method"); - // Send credential - m_registerId = 2; - XMLElement* q = XMPPUtils::createRegisterQuery(0,0, - String(m_registerId),m_local.node(),m_password); - sendStreamXML(q,state()); - break; - } - // Registered - else if (m_registerId == 2) - m_register = false; - else { - dropXML(xml); - xml = 0; - } -#undef SETERR_BREAK - - TelEngine::destruct(query); - if (xml) - TelEngine::destruct(xml); - - if (!m_register) - startAuth(); - else if (err != XMPPError::NoError) - terminate(true,0,err,reason,true); -} - -// Process a received element in Auth state -void JBStream::processAuth(XMLElement* xml) -{ - XDebug(m_engine,DebugAll,"JBStream::processAuth('%s') [%p]",xml->name(),this); - - // Waiting for abort to be confirmed - if (m_waitState == WaitAborted) { - if (xml->type() != XMLElement::Aborted) - DROP_AND_EXIT - if (!XMPPUtils::hasXmlns(*xml,XMPPNamespace::Sasl)) - INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0) - terminate(true,0,XMPPError::Aborted,"Authentication aborted",false); - TelEngine::destruct(xml); - return; - } - - while (true) { - // Sanity: check wait state - if (m_waitState != WaitChallenge && m_waitState != WaitResponse) - DROP_AND_EXIT - - // SASL: accept challenge, failure, success - if (flag(UseSasl)) { - if (xml->type() != XMLElement::Success && - xml->type() != XMLElement::Challenge && - xml->type() != XMLElement::Failure) - DROP_AND_EXIT - if (!XMPPUtils::hasXmlns(*xml,XMPPNamespace::Sasl)) - INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0) - // Waiting for response (sent auth or challenge response) - if (m_waitState == WaitResponse) - // MD5 auth - // Stream already authenticated: we received a challenge with valid rspauth - // and sent an empty response: wait for sucess/failure - // Stream not authenticated: we've sent auth or challenge response: - // wait for challenge or success with credentials - // Plain auth: accept success/failure - switch (m_authMech) { - case JIDFeatureSasl::MechMD5: - if (xml->type() == XMLElement::Failure) - break; - if (!flag(StreamAuthenticated)) { - String tmp = xml->getText(); - DataBlock rspauth; - Base64 base((void*)tmp.c_str(),tmp.length(),false); - bool ok = base.decode(rspauth); - base.clear(false); - if (!ok) - INVALIDXML_AND_EXIT(XMPPError::IncorrectEnc,0); - tmp.assign((const char*)rspauth.data(),rspauth.length()); - if (!tmp.startSkip("rspauth=",false)) - INVALIDXML_AND_EXIT(XMPPError::BadFormat,"Invalid response"); - String rspAuth; - buildDigestMD5Sasl(rspAuth,false); - if (rspAuth != tmp) - INVALIDXML_AND_EXIT(XMPPError::InvalidAuth,"Invalid response auth"); - DDebug(m_engine,DebugAll,"Stream. Stream authenticated [%p]",this); - m_flags |= StreamAuthenticated; - // Send empty response to challenge - if (xml->type() == XMLElement::Challenge) { - TelEngine::destruct(xml); - xml = XMPPUtils::createElement(XMLElement::Response,XMPPNamespace::Sasl); - sendStreamXML(xml,state()); - return; - } - break; - } - if (xml->type() != XMLElement::Success) - DROP_AND_EXIT - break; - case JIDFeatureSasl::MechPlain: - if (xml->type() == XMLElement::Challenge) - DROP_AND_EXIT - break; - default: - DDebug(m_engine,DebugStub, - "Stream. Unhandled SASL auth mechanism in Auth state [%p]",this); - DROP_AND_EXIT - } - // Waiting for challenge: sent auth element - else { - if (xml->type() != XMLElement::Challenge) - DROP_AND_EXIT - if (m_challengeCount) { - m_challengeCount--; - sendAuthResponse(xml); - } - else { - // Abort - m_waitState = WaitAborted; - TelEngine::destruct(xml); - xml = XMPPUtils::createElement(XMLElement::Abort,XMPPNamespace::Sasl); - sendStreamXML(xml,state()); - } - return; - } - - if (xml->type() == XMLElement::Success) { - TelEngine::destruct(xml); - break; - } - - // Failure - if (xml->type() != XMLElement::Failure) - DROP_AND_EXIT - XMLElement* e = xml->findFirstChild(); - XMPPError::Type err = XMPPError::NoError; - if (e) { - err = (XMPPError::Type)XMPPError::type(e->name()); - if (err == XMPPError::Count) - err = XMPPError::NoError; - TelEngine::destruct(e); - } - terminate(true,xml,err,"Authentication failed",true,false,false); - return; - } - - // XEP-0078: accept iq result or error - if (xml->type() != XMLElement::Iq) - DROP_AND_EXIT - // Check if received correct type - XMPPUtils::IqType t = XMPPUtils::iqType(xml->getAttribute("type")); - if (t != XMPPUtils::IqResult && t != XMPPUtils::IqError) - DROP_AND_EXIT - // Check if received correct id for the current waiting state - if (xml->hasAttribute("id","auth_1")) { - if (m_waitState != WaitChallenge) - DROP_AND_EXIT - } - else if (xml->hasAttribute("id","auth_2")) { - if (m_waitState != WaitResponse) - DROP_AND_EXIT - } - else - DROP_AND_EXIT - - // Terminate now on valid error - if (t == XMPPUtils::IqError) - ERRORXML_AND_EXIT - - // Result. - // WaitResponse: authenticated - if (m_waitState == WaitResponse) { - TelEngine::destruct(xml); - break; - } - - // WaitChallenge: Check child and its namespace. Send response - XMLElement* child = 0; - XMLElement* username = 0; - XMLElement* resource = 0; - XMPPError::Type err = XMPPError::NoError; - const char* reason = 0; - while (true) { - child = xml->findFirstChild(XMLElement::Query); - if (!(child && XMPPUtils::hasXmlns(*child,XMPPNamespace::IqAuth))) { - err = XMPPError::InvalidNamespace; - break; - } - // XEP-0078: username and resource children must be present - username = child->findFirstChild(XMLElement::Username); - resource = child->findFirstChild(XMLElement::Resource); - if (!(username && resource)) { - reason = "Username or resource child is missing"; - err = XMPPError::InvalidXml; - break; - } - // Get authentication methods - m_remoteFeatures.clear(); - if (child->hasChild(XMLElement::Digest)) - m_remoteFeatures.add(new JIDFeatureSasl(JIDFeatureSasl::MechSHA1)); - if (child->hasChild(XMLElement::Password)) - m_remoteFeatures.add(new JIDFeatureSasl(JIDFeatureSasl::MechPlain)); - break; - } - TelEngine::destruct(username); - TelEngine::destruct(resource); - TelEngine::destruct(child); - if (err != XMPPError::NoError) - INVALIDXML_AND_EXIT(err,reason) - setClientAuthMechanism(); - sendAuthResponse(xml); - return; - } - - // Authenticated - resetStream(); - if (flag(UseSasl)) - sendStreamStart(); - else { - Debug(m_engine,DebugInfo,"Stream. Authenticated [%p]",this); - changeState(Running); - } -} - -// Process a received element in Started state -void JBStream::processStarted(XMLElement* xml) -{ - XDebug(m_engine,DebugAll,"JBStream::processStarted('%s') [%p]",xml->name(),this); - - if (m_waitState == WaitStart) { - if (xml->type() != XMLElement::StreamStart) - DROP_AND_EXIT - // Check attributes: namespaces, from - if (!(xml->hasAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]) && - XMPPUtils::hasXmlns(*xml,XMPPNamespace::Client))) - INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0) - if (!(remote().domain() &= xml->getAttribute("from"))) - INVALIDXML_AND_EXIT(XMPPError::HostUnknown,0) - - // Get received version - String version = xml->getAttribute("version"); - if (version.null()) - m_flags |= NoRemoteVersion1; - else { - int pos = version.find('.'); - String majorStr = (pos != -1) ? version.substr(0,pos) : version; - int major = majorStr.toInteger(0); - if (major == 0) - m_flags |= NoRemoteVersion1; - else - m_flags &= ~NoRemoteVersion1; - } - - // Version 1: wait stream features - // Version 0: XEP-0078: start auth - // Start registering a new user if required - setRecvCount(-1); - if (flag(NoVersion1)) - if (!m_register) - startAuth(); - else - startRegister(); - else - m_waitState = WaitFeatures; - } - else if (m_waitState == WaitFeatures) { - if (xml->type() != XMLElement::StreamFeatures) - DROP_AND_EXIT - if (!getStreamFeatures(xml)) - return; - // Check TLS if not already secured - if (!flag(StreamSecured)) { - // Ignore all other features if TLS is started - // If missing: TLS shouldn't be used - // If present but not required check the local flag - JIDFeature* f = m_remoteFeatures.get(XMPPNamespace::Starttls); - if (f && (f->required() || flag(UseTls))) { - setRecvCount(1); - TelEngine::destruct(xml); - xml = XMPPUtils::createElement(XMLElement::Starttls,XMPPNamespace::Starttls); - sendStreamXML(xml,state()); - m_waitState = WaitTlsRsp; - return; - } - // Allow user register through unsecured streams ? - if (m_register && !flag(AllowUnsafeSetup)) { - TelEngine::destruct(xml); - terminate(true,0,XMPPError::Policy,"Can't register new user on unsecured stream",true); - return; - } - } - m_flags |= StreamSecured; - // Check if already authenticated - // Start registering a new user if required - if (!flag(StreamAuthenticated)) { - // RFC 3920 6.1: no mechanisms --> SASL not supported - XMLElement* e = xml->findFirstChild(XMLElement::Mechanisms); - if (!(e && e->hasChild(0))) - m_flags &= ~UseSasl; - TelEngine::destruct(e); - TelEngine::destruct(xml); - if (!m_register) - startAuth(); - else - startRegister(); - return; - } - m_flags |= StreamAuthenticated; - // Bind resource - XMLElement* bind = XMPPUtils::createElement(XMLElement::Bind,XMPPNamespace::Bind); - if (!m_local.resource().null()) - bind->addChild(new XMLElement(XMLElement::Resource,0,m_local.resource())); - XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"bind_1"); - iq->addChild(bind); - m_waitState = WaitBindRsp; - sendStreamXML(iq,state()); - } - else if (m_waitState == WaitTlsRsp) { - // Accept proceed and failure - bool ok = (xml->type() == XMLElement::Proceed); - if (!(ok || xml->type() == XMLElement::Failure) && - !XMPPUtils::hasXmlns(*xml,XMPPNamespace::Starttls)) - INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0) - if (ok) - startTls(); - else - terminate(true,0,XMPPError::NoError,"Server can't start TLS",false); - } - else if (m_waitState == WaitBindRsp) { - // Accept iq result or error - if (xml->type() != XMLElement::Iq) - DROP_AND_EXIT - // Check if received correct type - XMPPUtils::IqType t = XMPPUtils::iqType(xml->getAttribute("type")); - if (t != XMPPUtils::IqResult && t != XMPPUtils::IqError) - DROP_AND_EXIT - // Check if received correct id for the current waiting state - if (!xml->hasAttribute("id","bind_1")) - DROP_AND_EXIT - - // Terminate now on valid error - if (t == XMPPUtils::IqError) - ERRORXML_AND_EXIT - - // Result - XMLElement* bind = 0; - XMLElement* jidxml = 0; - XMPPError::Type err = XMPPError::NoError; - const char* reason = 0; - while (true) { - bind = xml->findFirstChild(XMLElement::Bind); - if (!bind) { - err = XMPPError::InvalidXml; - reason = "Bind child is missing"; - break; - } - if (!XMPPUtils::hasXmlns(*bind,XMPPNamespace::Bind)) { - err = XMPPError::InvalidNamespace; - break; - } - jidxml = bind->findFirstChild(XMLElement::Jid); - if (!jidxml) { - err = XMPPError::InvalidXml; - reason = "Jid child is misssing"; - break; - } - JabberID jid(jidxml->getText()); - if (!jid.isFull()) { - err = XMPPError::InvalidXml; - reason = "Jid is not full"; - break; - } - bool changed = !(local() == jid); - m_local.set(jid.node(),jid.domain(),jid.resource()); - if (changed) - Debug(m_engine,DebugInfo,"Stream. Local jid changed to '%s' [%p]", - local().c_str(),this); - break; - } - TelEngine::destruct(jidxml); - TelEngine::destruct(bind); - if (err == XMPPError::NoError) - // Create session if required - if (m_remoteFeatures.get(XMPPNamespace::Session)) { - XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"sess_1"); - iq->addChild(XMPPUtils::createElement(XMLElement::Session, - XMPPNamespace::Session)); - m_waitState = WaitSessionRsp; - sendStreamXML(iq,state()); - } - else - changeState(Running); - else - INVALIDXML_AND_EXIT(err,reason) - } - else if (m_waitState == WaitSessionRsp) { - // Accept iq result or error - if (xml->type() != XMLElement::Iq) - DROP_AND_EXIT - // Check if received correct type - XMPPUtils::IqType t = XMPPUtils::iqType(xml->getAttribute("type")); - if (t != XMPPUtils::IqResult && t != XMPPUtils::IqError) - DROP_AND_EXIT - // Check if received correct id for the current waiting state - if (!xml->hasAttribute("id","sess_1")) - DROP_AND_EXIT - // Terminate on error - if (t == XMPPUtils::IqError) - ERRORXML_AND_EXIT - // Result - changeState(Running); - } - else - DROP_AND_EXIT - TelEngine::destruct(xml); -} - -// Create an iq event from a received iq stanza -// Filter iq stanzas to generate an appropriate event -// Get iq type : set/get, error, result -// result: MAY have a first child with a response -// set/get: MUST have a first child -// error: MAY have a first child with the sent stanza -// MUST have an 'error' child -// Check type and the first child's namespace -JBEvent* JBStream::getIqEvent(XMLElement* xml, int iqType, XMPPError::Type& error) -{ -#define IQEVENT_SET_REQ(get,set) evType = (iqType == XMPPUtils::IqGet) ? get : set -#define IQEVENT_SET_RSP(res,err) evType = (iqType == XMPPUtils::IqResult) ? res : err - - JBEvent::Type evType; - switch (iqType) { - case XMPPUtils::IqGet: - case XMPPUtils::IqSet: - evType = JBEvent::Iq; - break; - case XMPPUtils::IqResult: - evType = JBEvent::IqResult; - break; - case XMPPUtils::IqError: - evType = JBEvent::IqError; - break; - default: - error = XMPPError::SFeatureNotImpl; - return 0; - } - error = XMPPError::NoError; - XMLElement* child = xml->findFirstChild(); - - // Request (type is set or get): check the child (MUST exists) - // Result: check it only if it has a child - // Error: check it only if it has an 'iq' child with a child - if (evType == JBEvent::Iq) { - // No child: request what ??? - if (!child) { - error = XMPPError::SBadRequest; - return 0; - } - switch (child->type()) { - case XMLElement::Jingle: - if (checkValidXmlns(child,XMPPNamespace::Jingle,error)) - IQEVENT_SET_REQ(JBEvent::IqJingleGet,JBEvent::IqJingleSet); - break; - case XMLElement::Session: - if (checkValidXmlns(child,XMPPNamespace::JingleSession,error)) - IQEVENT_SET_REQ(JBEvent::IqJingleGet,JBEvent::IqJingleSet); - break; - case XMLElement::Query: - if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoInfo)) - IQEVENT_SET_REQ(JBEvent::IqDiscoInfoGet,JBEvent::IqDiscoInfoSet); - else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoItems)) - IQEVENT_SET_REQ(JBEvent::IqDiscoItemsGet,JBEvent::IqDiscoItemsSet); - else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Roster)) { - if (iqType == XMPPUtils::IqGet) - error = XMPPError::SBadRequest; - else - evType = JBEvent::IqRosterSet; - } - break; - case XMLElement::Command: - if (checkValidXmlns(child,XMPPNamespace::Command,error)) - IQEVENT_SET_REQ(JBEvent::IqCommandGet,JBEvent::IqCommandSet); - break; - default: ; - } - } - else if (child) { - switch (child->type()) { - case XMLElement::Jingle: - if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Jingle)) - IQEVENT_SET_RSP(JBEvent::IqJingleRes,JBEvent::IqJingleErr); - break; - case XMLElement::Session: - if (XMPPUtils::hasXmlns(*child,XMPPNamespace::JingleSession)) - IQEVENT_SET_RSP(JBEvent::IqJingleRes,JBEvent::IqJingleErr); - break; - case XMLElement::Query: - if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoInfo)) - IQEVENT_SET_RSP(JBEvent::IqDiscoInfoRes,JBEvent::IqDiscoInfoErr); - else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::DiscoItems)) - IQEVENT_SET_RSP(JBEvent::IqDiscoItemsRes,JBEvent::IqDiscoItemsErr); - else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Roster)) - IQEVENT_SET_RSP(JBEvent::IqRosterRes,JBEvent::IqRosterErr); - break; - case XMLElement::Command: - if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Command)) - IQEVENT_SET_RSP(JBEvent::IqCommandRes,JBEvent::IqCommandErr); - break; - default: ; - } - } - - if (error == XMPPError::NoError) - return new JBEvent(evType,this,xml,child); - TelEngine::destruct(child); - return 0; -#undef IQEVENT_SET_REQ -#undef IQEVENT_SET_RSP -} - -// Send declaration and stream start -bool JBStream::sendStreamStart() -{ - m_id = ""; - m_declarationSent = 0; - return sendStreamXML(getStreamStart(),Started); -} - -// Send stream XML elements through the socket -bool JBStream::sendStreamXML(XMLElement* e, State newState) -{ - Lock lock(m_socket.m_streamMutex); - Error ret = ErrorContext; - while (e) { - if (state() == Idle || state() == Destroy) - break; - if (m_streamXML) { - ret = sendPending(); - if (ret != ErrorNone) { - TelEngine::destruct(e); - break; - } - } - bool unclose = (e->type() == XMLElement::StreamStart || - e->type() == XMLElement::StreamEnd); - m_streamXML = new XMLElementOut(e,0,unclose); - ret = sendPending(); - if (ret == ErrorPending) - ret = ErrorNone; - break; - } - if (ret == ErrorNone) - changeState(newState); - return (ret == ErrorNone); -} - -// Terminate stream on receiving invalid elements -void JBStream::invalidStreamXML(XMLElement* xml, XMPPError::Type error, const char* reason) -{ - if (!xml) - return; - Debug(m_engine,DebugNote, - "Stream. Invalid XML (%p,%s) state=%s error='%s' reason='%s' [%p]", - xml,xml->name(),lookupState(state()),s_err[error],reason,this); - terminate(true,xml,error,reason,true); -} - -// Terminate stream on receiving stanza errors -void JBStream::errorStreamXML(XMLElement* xml) -{ - String error, reason; - if (xml) { - XMLElement* tmp = xml->findFirstChild(XMLElement::Error); - XMPPUtils::decodeError(tmp,error,reason); - TelEngine::destruct(tmp); - TelEngine::destruct(xml); - } - Debug(m_engine,DebugNote,"Stream. Received error=%s reason='%s' state=%s [%p]", - error.c_str(),reason.c_str(),lookupState(state()),this); - terminate(false,0,XMPPError::NoError,reason?reason:error,true); -} - -// Drop an unexpected or unhandled element -void JBStream::dropXML(XMLElement* xml, bool unexpected) -{ - if (!xml) - return; - Debug(m_engine,unexpected?DebugNote:DebugInfo, - "Stream. Dropping %s element (%p,%s) in state %s [%p]", - unexpected?"unexpected":"unhandled",xml,xml->name(), - lookupState(state()),this); - TelEngine::destruct(xml); -} - -// Change stream state -void JBStream::changeState(State newState) -{ - if (m_state == newState) - return; - Debug(m_engine,DebugInfo,"Stream. Changing state from %s to %s [%p]", - lookupState(m_state),lookupState(newState),this); - m_state = newState; - if (newState == Running) { - m_setupTimeout = 0; - startIdleTimer(); - streamRunning(); - if (!m_startEvent) - m_startEvent = new JBEvent(JBEvent::Running,this,0); - } -} - -// Parse receive stream features -bool JBStream::getStreamFeatures(XMLElement* features) -{ -// Get xmlType child, check 'ns' namespace, add remote feature -#define GET_FEATURE(xmlType,ns) { \ - XMLElement* e = features->findFirstChild(xmlType); \ - if (e) { \ - if (!(XMPPUtils::hasXmlns(*e,ns))) { \ - TelEngine::destruct(e); \ - invalidStreamXML(features,XMPPError::InvalidNamespace,0); \ - return false; \ - } \ - m_remoteFeatures.add(ns,e->hasChild(XMLElement::Required)); \ - TelEngine::destruct(e); \ - } \ -} - m_remoteFeatures.clear(); - if (!features) - return true; - - // TLS - GET_FEATURE(XMLElement::Starttls,XMPPNamespace::Starttls) - // SASL - XMLElement* sasl = features->findFirstChild(XMLElement::Mechanisms); - if (sasl) { - if (!(XMPPUtils::hasXmlns(*sasl,XMPPNamespace::Sasl))) { - TelEngine::destruct(sasl); - invalidStreamXML(features,XMPPError::InvalidNamespace,0); - return false; - } - int auth = 0; - XMLElement* m = 0; - while (0 != (m = sasl->findNextChild(m,XMLElement::Mechanism))) - auth |= lookup(m->getText(),JIDFeatureSasl::s_authMech); - m_remoteFeatures.add(new JIDFeatureSasl(auth,sasl->hasChild(XMLElement::Required))); - TelEngine::destruct(sasl); - } - setClientAuthMechanism(); - // Old auth (older then version 1.0 SASL) - GET_FEATURE(XMLElement::Auth,XMPPNamespace::IqAuthFeature) - // Register new user - GET_FEATURE(XMLElement::Register,XMPPNamespace::Register) - // Bind resources - GET_FEATURE(XMLElement::Bind,XMPPNamespace::Bind) - // Sessions - GET_FEATURE(XMLElement::Session,XMPPNamespace::Session) - return true; -#undef GET_FEATURE -#undef REQUIRED -} - -// Start client TLS. Terminate the stream on error -bool JBStream::startTls() -{ - Debug(m_engine,DebugInfo,"Stream. Initiating TLS [%p]",this); - changeState(Securing); - if (m_engine->encryptStream(this)) { - m_flags |= StreamSecured; - setRecvCount(-1); - sendStreamStart(); - return true; - } - terminate(false,0,XMPPError::NoError,"Failed to start TLS",false); - return false; -} - -// Start client registration -bool JBStream::startRegister() -{ - // Allow user register through unsecured streams ? - if (!flag(StreamSecured | AllowUnsafeSetup)) { - terminate(true,0,XMPPError::Policy,"Can't register new user on unsecured stream",true); - return false; - } - - // Check if the server already told us it supports in-band register - // or query register support - XMLElement* xml = 0; - m_registerId = m_remoteFeatures.get(XMPPNamespace::Register) ? 2 : 1; - String id(m_registerId); - if (m_registerId == 2) - xml = XMPPUtils::createRegisterQuery(0,0,id,m_local.node(),m_password); - else - xml = XMPPUtils::createRegisterQuery(XMPPUtils::IqGet,0,0,id); - return sendStreamXML(xml,Register); -} - -// Start client authentication -bool JBStream::startAuth() -{ - XMLElement* xml = getAuthStart(); - if (xml) { - Debug(m_engine,DebugAll, - "Stream. Starting authentication type=%s mechanism=%s [%p]", - ((type()==JBEngine::Component)?"handshake":(flag(UseSasl)?"SASL":"IQ")), - lookup(m_authMech,JIDFeatureSasl::s_authMech),this); - return sendStreamXML(xml,Auth); - } - Debug(m_engine,DebugNote,"Stream. Failed to build auth start [%p]",this); - terminate(false,0,XMPPError::InvalidMechanism,"No mechanism available",true); - return false; -} - -// Send auth response to received challenge/iq -bool JBStream::sendAuthResponse(XMLElement* challenge) -{ - XMLElement* xml = 0; - String response; - XMPPError::Type code = XMPPError::NoError; - const char* error = 0; - - if (flag(UseSasl)) -#define SET_CODE_AND_BREAK(c,e) { code = c; error = e; break; } - while (true) { - if (m_authMech != JIDFeatureSasl::MechMD5 && - m_authMech != JIDFeatureSasl::MechPlain) - SET_CODE_AND_BREAK(XMPPError::InvalidMechanism,"No mechanism available") - // This should never happen - if (!(challenge && challenge->type() == XMLElement::Challenge)) - SET_CODE_AND_BREAK(XMPPError::Internal,"Unexpected element while expecting 'challenge'") - // TODO: implement challenge when using plain authentication - if (m_authMech == JIDFeatureSasl::MechPlain) { - const char* s = "Challenge not implemented for plain authentication"; - Debug(m_engine,DebugStub,"Stream. %s [%p]",s,this); - SET_CODE_AND_BREAK(XMPPError::UndefinedCondition,s) - } - const char* chgText = challenge->getText(); - if (!chgText) - SET_CODE_AND_BREAK(XMPPError::BadFormat,"Challenge is empty") - Base64 base64((void*)chgText,::strlen(chgText),false); - DataBlock chg; - bool ok = base64.decode(chg,false); - base64.clear(false); - if (!ok) - SET_CODE_AND_BREAK(XMPPError::IncorrectEnc,"Challenge with incorrect encoding") - String tmp((const char*)chg.data(),chg.length()); - if (tmp.null()) - SET_CODE_AND_BREAK(XMPPError::BadFormat,"Challenge is empty") - Debug(m_engine,DebugAll,"Stream(%s). Received challenge '%s' [%p]", - toString().c_str(),tmp.c_str(),this); - String nonce,realm; - ObjList* obj = tmp.split(',',false); - for (ObjList* o = obj->skipNull(); o; o = o->skipNext()) { - String* s = static_cast(o->get()); - if (s->startsWith("realm=")) - realm = s->substr(6); - else if (s->startsWith("nonce=")) - nonce = s->substr(6); - } - TelEngine::destruct(obj); - MimeHeaderLine::delQuotes(realm); - MimeHeaderLine::delQuotes(nonce); - if (nonce.null()) - SET_CODE_AND_BREAK(XMPPError::BadFormat,"Challenge is incomplete") - buildSaslResponse(response,&realm,&nonce); - xml = XMPPUtils::createElement(XMLElement::Response,XMPPNamespace::Sasl,response); - break; - } -#undef SET_CODE_AND_BREAK - else { - xml = XMPPUtils::createIq(XMPPUtils::IqSet,0,0,"auth_2"); - XMLElement* q = XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::IqAuth); - q->addChild(new XMLElement(XMLElement::Username,0,m_local.node())); - q->addChild(new XMLElement(XMLElement::Resource,0,m_local.resource())); - if (m_authMech == JIDFeatureSasl::MechSHA1) { - SHA1 sha; - sha << id() << m_password; - q->addChild(new XMLElement(XMLElement::Digest,0,sha.hexDigest())); - } - else if (m_authMech == JIDFeatureSasl::MechPlain) - q->addChild(new XMLElement(XMLElement::Password,0,m_password)); - else { - code = XMPPError::InvalidMechanism; - error = "No mechanism available"; - } - xml->addChild(q); - } - - if (!error) { - TelEngine::destruct(challenge); - m_waitState = WaitResponse; - return sendStreamXML(xml,state()); - } - TelEngine::destruct(xml); - Debug(m_engine,DebugNote,"Stream. Failed to respond error=%s reason='%s'. %s [%p]", - s_err[code],error,flag(UseSasl)?"Aborting":"Terminating",this); - if (flag(UseSasl)) { - TelEngine::destruct(challenge); - xml = XMPPUtils::createElement(XMLElement::Abort,XMPPNamespace::Sasl); - return sendStreamXML(xml,state()); - } - terminate(false,challenge,code,error,true); - return false; -} - -// Build SASL authentication response -// A valid mechanism must be previously set -void JBStream::buildSaslResponse(String& response, String* realm, String* nonce) -{ - // Digest MD5. See RFC 4616 Section 2 - // [authzid] UTF8NUL authcid UTF8NUL passwd - if (m_authMech == JIDFeatureSasl::MechPlain) { - DataBlock data; - unsigned char nul = 0; - data.append(&nul,1); - data += m_local.node(); - data.append(&nul,1); - data += m_password; - Base64 base64((void*)data.data(),data.length()); - base64.encode(response); - return; - } - - // Digest MD5. See RFC 2831 2.1.2.1 - MD5 md5(String((unsigned int)::random())); - m_cnonce = md5.hexDigest(); - appendQParam(response,"username",m_local.node(),true,true); - m_realm = realm ? *realm : ""; - if (m_realm) - appendQParam(response,"realm",m_realm,true); - m_nonce = nonce ? *nonce : ""; - appendQParam(response,"nonce",m_nonce,true); - m_nonceCount++; - char tmp[9]; - ::sprintf(tmp,"%08x",m_nonceCount); - m_nc = tmp; - appendQParam(response,"nc",m_nc,false); - appendQParam(response,"cnonce",m_cnonce,true); - appendQParam(response,"digest-uri",String("xmpp/")+m_local.domain(),true); - appendQParam(response,"qop",s_qop,true); - String rsp; - buildDigestMD5Sasl(rsp,true); - appendQParam(response,"response",rsp,false); - appendQParam(response,"charset","utf-8",false); - appendQParam(response,"algorithm","md5-sess",false); - Debug(m_engine,DebugAll,"Stream(%s). Built SASL response '%s' [%p]", - toString().c_str(),response.c_str(),this); - Base64 base64((void*)response.c_str(),response.length()); - base64.encode(response); -} - -// Parse remote's features and pick an authentication mechanism -// to be used when requesting authentication -void JBStream::setClientAuthMechanism() -{ - JIDFeature* f = m_remoteFeatures.get(XMPPNamespace::Sasl); - JIDFeatureSasl* sasl = static_cast(f); - m_authMech = JIDFeatureSasl::MechNone; - if (!sasl) - return; - // Component or not using SASL: accept SHA1 and plain - if (type() == JBEngine::Component || !flag(UseSasl)) { - if (sasl->mechanism(JIDFeatureSasl::MechSHA1)) - m_authMech = JIDFeatureSasl::MechSHA1; - else if (sasl->mechanism(JIDFeatureSasl::MechPlain) && flag(AllowPlainAuth)) - m_authMech = JIDFeatureSasl::MechPlain; - return; - } - // SASL: accept Digest MD5 - if (sasl->mechanism(JIDFeatureSasl::MechMD5)) - m_authMech = JIDFeatureSasl::MechMD5; - else if (sasl->mechanism(JIDFeatureSasl::MechPlain) && flag(AllowPlainAuth)) - m_authMech = JIDFeatureSasl::MechPlain; -} - -// Build a Digest MD5 SASL to be sent with authentication responses -// See RFC 2831 2.1.2.1 -// A1 = H(username:realm:passwd):nonce:cnonce:authzid -// A2 ="AUTHENTICATE:uri -// rsp = HEX(HEX(A1):nonce:nc:cnonce:qop:HEX(A2)) -void JBStream::buildDigestMD5Sasl(String& dest, bool authenticate) -{ - MD5 md5; - md5 << m_local.node() << ":" << m_realm << ":" << m_password; - MD5 md5A1(md5.rawDigest(),16); - if (m_nonce) - md5A1 << ":" << m_nonce; - md5A1 << ":" << m_cnonce; - MD5 md5A2; - if (authenticate) - md5A2 << "AUTHENTICATE"; - md5A2 << ":xmpp/" << m_local.domain(); - MD5 md5Rsp; - md5Rsp << md5A1.hexDigest(); - if (m_nonce) - md5Rsp << ":" << m_nonce << ":" << m_nc; - md5Rsp << ":" << m_cnonce << ":" << s_qop << ":" << md5A2.hexDigest(); - dest = md5Rsp.hexDigest(); -} - -// Event termination notification -void JBStream::eventTerminated(const JBEvent* event) -{ - if (event && event == m_lastEvent) { - m_lastEvent = 0; - DDebug(m_engine,DebugAll, - "Stream. Event (%p,%s) terminated [%p]",event,event->name(),this); - } -} - -// Try to send the first element in pending outgoing stanzas list -// Terminate stream on socket error -JBStream::Error JBStream::sendPending() -{ - XMLElementOut* eout = 0; - - if (state() == Destroy) - return ErrorContext; - - if (m_streamXML) { - m_idleTimeout = 0; - // Check if declaration was sent - if (m_declarationSent < s_declaration.length()) { - const char* data = s_declaration.c_str() + m_declarationSent; - unsigned int len = s_declaration.length() - m_declarationSent; - if (!m_socket.send(data,len)) { - Debug(m_engine,DebugNote,"Stream. Failed to send declaration [%p]",this); - terminate(false,0,XMPPError::HostGone,"Failed to send data",false); - return ErrorNoSocket; - } - m_declarationSent += len; - if (m_declarationSent < s_declaration.length()) - return ErrorPending; - DDebug(m_engine,DebugAll,"Stream. Sent declaration %s [%p]", - s_declaration.c_str(),this); - } - eout = m_streamXML; - } - else { - ObjList* obj = m_outXML.skipNull(); - if (!obj) { - if (!m_idleTimeout) - startIdleTimer(); - return ErrorNone; - } - if (state() != Running) { - m_idleTimeout = 0; - return ErrorPending; - } - eout = obj ? static_cast(obj->get()) : 0; - } - XMLElement* xml = eout->element(); - if (!xml) { - if (eout != m_streamXML) { - m_outXML.remove(eout,true); - if (!m_idleTimeout) - startIdleTimer(); - } - else - TelEngine::destruct(m_streamXML); - return ErrorNone; - } - - // Print the element only if it's the first time - if (!eout->sent()) - m_engine->printXml(*xml,this,true); - - Error ret = ErrorNone; - u_int32_t len; - const char* data = eout->getData(len); - unsigned int tmp = len; - if (m_socket.send(data,len)) { - if (len != tmp) - ret = ErrorPending; - eout->dataSent(len); - } - else - ret = ErrorNoSocket; - - if (ret == ErrorPending) { - m_idleTimeout = 0; - return ret; - } - - if (ret == ErrorNone) - DDebug(m_engine,DebugAll,"Stream. Sent element (%p,%s) id='%s [%p]", - xml,xml->name(),eout->id().c_str(),this); - else { - // Don't terminate if the element is stream error or stream end: - // stream is already terminating - bool bye = xml->type() != XMLElement::StreamError && - xml->type() != XMLElement::StreamEnd; - Debug(m_engine,DebugNote,"Stream. Failed to send (%p,%s) in state=%s [%p]", - xml,xml->name(),lookupState(state()),this); - if (eout->id()) { - JBEvent* ev = new JBEvent(JBEvent::WriteFail,this, - eout->release(),eout->id()); - m_events.append(ev); - } - if (bye) - terminate(false,0,XMPPError::HostGone,"Failed to send data",false); - } - if (eout != m_streamXML) - m_outXML.remove(eout,true); - else - TelEngine::destruct(m_streamXML); - startIdleTimer(); - return ret; -} - -// Remove: -// Pending elements with id if id is not 0 -// All elements without id if id is 0 -void JBStream::removePending(bool notify, const String* id, bool force) -{ - ListIterator iter(m_outXML); - bool first = true; - for (GenObject* o = 0; (o = iter.get());) { - XMLElementOut* eout = static_cast(o); - // Check if the first element will be removed if partially sent - if (first) { - first = false; - if (eout->dataCount() && !force) - continue; - } - if (id) { - if (*id != eout->id()) - continue; - } - else if (eout->id()) - continue; - if (notify) - m_events.append(new JBEvent(JBEvent::WriteFail,this,eout->release(),id)); - m_outXML.remove(eout,true); - } -} - -// Called when a setup state was completed -// Set/reset some stream flags and data -void JBStream::resetStream() -{ - // TLS: RFC 3920 - // SASL: RFC 3920 Section 7 page 38 - switch (state()) { - case Securing: - m_flags |= StreamSecured; - m_id = ""; - break; - case Auth: - m_flags |= StreamAuthenticated; - if (flag(UseSasl)) - m_id = ""; - break; - case Destroy: - case Idle: - m_flags &= ~(StreamAuthenticated | StreamSecured); - m_challengeCount = 2; - m_id = ""; - break; - default: - break; - } - m_flags &= ~NoRemoteVersion1; - m_nonce = ""; - m_cnonce = ""; - m_realm = ""; -} - -// Set receive count -void JBStream::setRecvCount(int value) -{ - Lock lock(m_socket.m_receiveMutex); - if (m_recvCount == value) - return; - DDebug(m_engine,DebugInfo,"Stream. recvCount changed from %d to %d [%p]", - m_recvCount,value,this); - m_recvCount = value; -} - -// Start idle timer if there are no pending stanzas -bool JBStream::startIdleTimer(u_int64_t time) -{ - if (state() != Running || !m_engine || m_outXML.skipNull() || - !m_engine->m_streamIdleInterval) { - m_idleTimeout = 0; - return false; - } - m_idleTimeout = time + m_engine->m_streamIdleInterval; - XDebug(m_engine,DebugInfo,"Stream. Started idle timer for " FMT64 "ms [%p]", - m_engine->m_streamIdleInterval,this); - return true; -} - - -/** - * JBComponentStream - */ - -JBComponentStream::JBComponentStream(JBEngine* engine, XMPPServerInfo& info, - const JabberID& localJid, const JabberID& remoteJid) - : JBStream(engine,JBEngine::Component,info,localJid,remoteJid) -{ - // Doesn't use SASL auth: just using this structure to set auth mechanism - JIDFeatureSasl* sasl = new JIDFeatureSasl(JIDFeatureSasl::MechMD5 | - JIDFeatureSasl::MechSHA1); - m_remoteFeatures.add(sasl); -} - -// Get an object from this stream -void* JBComponentStream::getObject(const String& name) const -{ - if (name == "JBComponentStream") - return (void*)this; - return JBStream::getObject(name); -} - -// Create stream start element -XMLElement* JBComponentStream::getStreamStart() -{ - XMLElement* start = XMPPUtils::createElement(XMLElement::StreamStart, - XMPPNamespace::ComponentAccept); - start->setAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]); - start->setAttribute("to",local()); - return start; -} - -// Get the authentication element to be sent when authentication starts -XMLElement* JBComponentStream::getAuthStart() -{ - setClientAuthMechanism(); - if (m_authMech == JIDFeatureSasl::MechSHA1) { - SHA1 auth; - auth << id() << m_password; - return new XMLElement(XMLElement::Handshake,0,auth.hexDigest()); - } - else if (m_authMech == JIDFeatureSasl::MechPlain) - return new XMLElement(XMLElement::Handshake,0,m_password); - return 0; -} - -// Process a received element in Started state -void JBComponentStream::processStarted(XMLElement* xml) -{ - // Expect stream start tag - setRecvCount(-1); - if (xml->type() != XMLElement::StreamStart) - DROP_AND_EXIT - // Check namespaces - if (!(xml->hasAttribute("xmlns:stream",s_ns[XMPPNamespace::Stream]) && - XMPPUtils::hasXmlns(*xml,XMPPNamespace::ComponentAccept))) - INVALIDXML_AND_EXIT(XMPPError::InvalidNamespace,0); - // Check the from attribute - if (!engine()->checkComponentFrom(this,xml->getAttribute("from"))) - INVALIDXML_AND_EXIT(XMPPError::HostUnknown,0); - TelEngine::destruct(xml); - startAuth(); -} - -// Process a received element in Auth state -void JBComponentStream::processAuth(XMLElement* xml) -{ - setRecvCount(-1); - if (xml->type() != XMLElement::Handshake) - DROP_AND_EXIT - TelEngine::destruct(xml); - changeState(Running); -} - - -/** - * JBClientStream - */ - -// Outgoing -JBClientStream::JBClientStream(JBEngine* engine, XMPPServerInfo& info, - const JabberID& localJid, const NamedList& params) - : JBStream(engine,JBEngine::Client,info,localJid,JabberID(0,localJid.domain(),0)) -{ - m_name = params.getValue("account"); - m_roster = new XMPPUserRoster(0,localJid.node(),localJid.domain()); - m_resource = new JIDResource(local().resource(),JIDResource::Available, - JIDResource::CapChat|JIDResource::CapAudio); - // Check if we should register this user - m_register = params.getBoolValue("register"); -} - -// Destructor -JBClientStream::~JBClientStream() -{ - TelEngine::destruct(m_roster); - TelEngine::destruct(m_resource); -} - -// Get an object from this stream -void* JBClientStream::getObject(const String& name) const -{ - if (name == "JBClientStream") - return (void*)this; - return JBStream::getObject(name); -} - -// Get a remote user from roster -XMPPUser* JBClientStream::getRemote(const JabberID& jid) -{ - return m_roster->getUser(jid,false); -} - -// Send a stanza -JBStream::Error JBClientStream::sendStanza(XMLElement* stanza, const char* senderId) -{ - if (!stanza) - return ErrorContext; - - Lock lock(streamMutex()); - - // Destroy: call parent's method to put the debug message - if (state() == Destroy) - return JBStream::sendStanza(stanza,senderId); - - // Check 'from' attribute - const char* from = stanza->getAttribute("from"); - if (from && *from) { - JabberID jid(from); - if (!local().match(jid)) { - Debug(engine(),DebugNote, - "Stream. Can't send stanza (%p,%s) with invalid from=%s [%p]", - stanza,stanza->name(),from,this); - TelEngine::destruct(stanza); - return ErrorContext; - } - } - -#if 0 - // TODO: Uncomment and implement. We'll need only 'subscribed' and 'unsubscribed' - // elements - if (stanza->type() == XMLElement::Presence) { - // Ignore if the presence is not sent to an actual user (node) - JabberID to(stanza->getAttribute("to")); - if (to.node()) { - Lock lock(m_roster); - // TODO: Update subscriptions for users in roster - } - } -#endif - - return JBStream::sendStanza(stanza,senderId); -} - -// Stream is running: get roster from server -void JBClientStream::streamRunning() -{ - XDebug(engine(),DebugAll,"JBClientStream::streamRunning() [%p]",this); - if (!m_rosterReqId.null()) - return; - m_roster->cleanup(); - m_rosterReqId = "roster-query"; - XMLElement* xml = XMPPUtils::createIq(XMPPUtils::IqGet,0,0,m_rosterReqId); - xml->addChild(XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::Roster)); - sendStanza(xml); -} - -// Process received data while running -void JBClientStream::processRunning(XMLElement* xml) -{ - XDebug(engine(),DebugAll,"JBClientStream::processRunning('%s') [%p]",xml->name(),this); - - JBStream::processRunning(xml); - - // Check last event for post processing - JBEvent* event = lastEvent(); - if (!event) - return; - bool sendPres = true; - switch (event->type()) { - case JBEvent::Presence: - break; - case JBEvent::IqRosterSet: - sendStanza(XMPPUtils::createIq(XMPPUtils::IqResult,event->to(), - event->from(),event->id())); - sendPres = false; - break; - case JBEvent::IqRosterRes: - case JBEvent::IqRosterErr: - if (m_rosterReqId == event->id()) { - // Cleanup roster only if received result or error - m_rosterReqId = ""; - m_roster->cleanup(); - if (event->type() == JBEvent::IqRosterRes) - break; - // Error - Debug(engine(),DebugNote,"Stream. Received error '%s' on roster request [%p]", - event->text().c_str(),this); - String err, txt; - XMPPUtils::decodeError(event->element(),err,txt); - m_events.remove(event,true); - String tmp; - tmp << "Unable to get roster from server"; - if (err) - tmp << " error=" << err; - if (txt) - tmp << " reason=" << txt; - TelEngine::destruct(event); - terminate(false,0,XMPPError::NoError,tmp,false); - } - return; - case JBEvent::IqDiscoInfoGet: - sendStanza(m_roster->createDiscoInfoResult(event->to(),event->from(),event->id())); - m_events.remove(event,true); - return; - case JBEvent::IqDiscoItemsGet: - case JBEvent::IqDiscoInfoSet: - case JBEvent::IqDiscoItemsSet: - sendStanza(event->createError(XMPPError::TypeCancel,XMPPError::SFeatureNotImpl)); - m_events.remove(event,true); - return; - case JBEvent::IqDiscoInfoRes: - case JBEvent::IqDiscoInfoErr: - case JBEvent::IqDiscoItemsRes: - case JBEvent::IqDiscoItemsErr: - return; - default: - return; - } - - // Presence: update roster and let the event to be processed by a service - // TODO: Presence None and Unavailable: check if we already know it - // If so, remove event to avoid sending too many massages - // or - // Don't do that: someone might rely on those presences (for timeout purposes?) - if (event->type() == JBEvent::Presence) { - JBPresence::Presence pres = JBPresence::presenceType(event->stanzaType()); - - // Check if it's the same user: update resource list - if (local().bare() &= event->from().bare()) { - Lock2 lock(m_roster,&m_roster->resources()); - bool avail = pres == JBPresence::None; - if (avail || pres == JBPresence::Unavailable) { - if (event->from().resource()) { - // Check for our own resource: we've seen that - if (event->from().resource() == local().resource()) { - Debug(engine(),DebugAll, - "Stream. Ignoring presence from the same resource [%p]",this); - m_events.remove(event,true); - return; - } - JIDResource* res = m_roster->resources().get(event->from().resource()); - if (pres == JBPresence::Unavailable) { - if (res) - m_roster->resources().remove(res,true); - } - else - if (res) - res->fromXML(event->element()); - else { - res = new JIDResource(event->from().resource()); - res->fromXML(event->element()); - m_roster->resources().add(res); - } - if (res) - Debug(engine(),DebugAll, - "Stream. %s own resource '%s' [%p]", - avail?"Added":"Removed",event->from().resource().c_str(),this); - } - else if (!avail && m_roster->resources().count()) { - m_roster->resources().clear(); - Debug(engine(),DebugAll,"Stream. Removed own resources [%p]",this); - } - } - return; - } - - XMPPUser* user = getRemote(event->from()); - bool error = false; - switch (pres) { - case JBPresence::None: - case JBPresence::Unavailable: - if (user) - user->processPresence(event,pres == JBPresence::None); - else - error = true; - break; - case JBPresence::Subscribed: - case JBPresence::Unsubscribed: - if (user) - user->processSubscribe(event,pres); - else - error = true; - break; - case JBPresence::Subscribe: - case JBPresence::Unsubscribe: - case JBPresence::Error: - break; - case JBPresence::Probe: - dropXML(event->releaseXML()); - m_events.remove(event,true); - break; - } - TelEngine::destruct(user); - -#ifdef DEBUG - // Don't show message if it's the same jid: it came from another resource - if (error && !(event->to().bare() &= event->from().bare())) - DDebug(engine(),DebugNote, - "Stream. Received presence=%s from=%s. User not in roster [%p]", - event->stanzaType().c_str(),event->from().c_str(),this); -#endif - return; - } - - // Roster event: update and change event type - event->m_type = JBEvent::IqClientRosterUpdate; - - // Add new resource if not added. Send initial presence - if (sendPres) { - XMLElement* pres = new XMLElement(XMLElement::Presence); - m_resource->setName(local().resource()); - m_resource->addTo(pres); - sendStanza(pres); - } - - // Process received roster update - XMLElement* item = event->child() ? event->child()->findFirstChild(XMLElement::Item) : 0; - for (; item; item = event->child()->findNextChild(item,XMLElement::Item)) { - JabberID jid = item->getAttribute("jid"); - String sub = item->getAttribute("subscription"); - XMPPUser* user = m_roster->getUser(jid,false); - bool removeUser = false; - if (sub != "remove") { - XMPPDirVal::Direction subType = (XMPPDirVal::Direction)XMPPDirVal::lookup(sub); - if (user) - user->subscription().replace(subType); - else { - user = new XMPPUser(m_roster,jid.node(),jid.domain(),subType,false,false); - user->ref(); - } - removeUser = !user->local(); - } - else - removeUser = true; - if (user) { - Debug(engine(),DebugAll,"Stream. Updated roster jid=%s subscription=%s [%p]", - jid.c_str(),sub.c_str(),this); - if (removeUser) { - Debug(engine(),DebugInfo, - "Stream. Removing jid=%s from roster [%p]",jid.c_str(),this); - // deref() the user since we've increased its reference counter - user->deref(); - } - } - TelEngine::destruct(user); - } -} - -// Check the 'to' attribute of a received element -// Accept empty or bare/full jid match. Set 'to' if empty -bool JBClientStream::checkDestination(XMLElement* xml, bool& respond) -{ - respond = false; - if (!xml) - return false; - const char* to = xml->getAttribute("to"); - if (to && *to) { - JabberID jid(to); - // Waiting for bid response: accept 'to' with resource if we don't have one - if (state() == Started && m_waitState == WaitBindRsp && !local().resource()) - return jid.match(local()); - return local().match(jid); - } - xml->setAttribute("to",local()); - return true; -} - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/jgengine.cpp b/libs/yjingle/jgengine.cpp deleted file mode 100644 index 17155e9d..00000000 --- a/libs/yjingle/jgengine.cpp +++ /dev/null @@ -1,377 +0,0 @@ -/** - * jgengine.cpp - * Yet Another Jingle Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include - -using namespace TelEngine; - -static XMPPError s_err; - -TokenDict JGEvent::s_typeName[] = { - {"Jingle", Jingle}, - {"ResultOk", ResultOk}, - {"ResultError", ResultError}, - {"ResultWriteFail", ResultWriteFail}, - {"ResultTimeout", ResultTimeout}, - {"Terminated", Terminated}, - {"Destroy", Destroy}, - {0,0} -}; - - -/** - * JGEngine - */ -JGEngine::JGEngine(JBEngine* engine, const NamedList* params, int prio) - : JBService(engine,"jgengine",params,prio), - m_sessionIdMutex(true,"JGEngine::sessionId"), - m_sessionId(1), m_stanzaTimeout(20000), m_pingInterval(300000) -{ - JBThreadList::setOwner(this); -} - -JGEngine::~JGEngine() -{ - cancelThreads(); -} - -// Create private stream(s) to get events from sessions -void JGEngine::initialize(const NamedList& params) -{ - int lvl = params.getIntValue("debug_level",-1); - if (lvl != -1) - debugLevel(lvl); - - int timeout = params.getIntValue("stanza_timeout",(int)m_stanzaTimeout); - m_stanzaTimeout = timeout > 10000 ? timeout : 10000; - - int ping = params.getIntValue("ping_interval",(int)m_pingInterval); - if (ping == 0) - m_pingInterval = 0; - else if (ping < 60000) - m_pingInterval = 60000; - else - m_pingInterval = ping; - // Make sure we don't ping before a ping times out - if (m_pingInterval && m_stanzaTimeout && m_pingInterval <= m_stanzaTimeout) - m_pingInterval = m_stanzaTimeout + 100; - - if (debugAt(DebugInfo)) { - String s; - s << " stanza_timeout=" << (unsigned int)m_stanzaTimeout; - s << " ping_interval=" << (unsigned int)m_pingInterval; - Debug(this,DebugInfo,"Jabber Jingle service initialized:%s [%p]", - s.c_str(),this); - } - - if (!m_initialized) { - m_initialized = true; - int c = params.getIntValue("private_process_threads",1); - for (int i = 0; i < c; i++) - JBThread::start(JBThread::Jingle,this,this); - } -} - -// Make an outgoing call -JGSession* JGEngine::call(JGSession::Version ver, const String& localJID, const String& remoteJID, - const ObjList& contents, XMLElement* extra, const char* message, - const char* subject) -{ - DDebug(this,DebugAll,"New outgoing call from '%s' to '%s'", - localJID.c_str(),remoteJID.c_str()); - - // Get a stream from the engine - JBStream* stream = 0; - if (engine()->protocol() == JBEngine::Component) - stream = engine()->getStream(); - else { - // Client: the stream must be already created - JabberID jid(localJID); - stream = engine()->getStream(&jid,false); - } - - // Create outgoing session - bool hasStream = (0 != stream); - if (hasStream) { - JGSession* session = 0; - switch (ver) { - case JGSession::Version1: - session = new JGSession1(this,stream,localJID,remoteJID,message); - break; - case JGSession::Version0: - session = new JGSession0(this,stream,localJID,remoteJID,message); - break; - case JGSession::VersionUnknown: - ; - } - TelEngine::destruct(stream); - if (session && session->initiate(contents,extra,subject)) { - Lock lock(this); - m_sessions.append(session); - return (session && session->ref()) ? session : 0; - } - TelEngine::destruct(session); - } - else - TelEngine::destruct(extra); - - Debug(this,DebugNote,"Outgoing call from '%s' to '%s' failed: %s", - localJID.c_str(),remoteJID.c_str(), - !hasStream? "can't create stream" : "failed to send data"); - return 0; -} - -// Get events from sessions -JGEvent* JGEngine::getEvent(u_int64_t time) -{ - JGEvent* event = 0; - lock(); - ListIterator iter(m_sessions); - for (;;) { - JGSession* session = static_cast(iter.get()); - // End of iteration? - if (!session) - break; - RefPointer s = session; - // Dead pointer? - if (!s) - continue; - unlock(); - if (0 != (event = s->getEvent(time))) { - if (event->type() == JGEvent::Destroy) { - DDebug(this,DebugAll,"Deleting internal event (%p,Destroy)",event); - delete event; - } - else - return event; - } - lock(); - } - unlock(); - return 0; -} - -// Default event processor -void JGEngine::defProcessEvent(JGEvent* event) -{ - if (!event) - return; - DDebug(this,DebugAll,"JGEngine::defprocessEvent. Deleting event (%p,%u)", - event,event->type()); - delete event; -} - -// Accept an event from the Jabber engine -bool JGEngine::accept(JBEvent* event, bool& processed, bool& insert) -{ - if (!(event && event->stream())) - return false; - XMLElement* child = event->child(); - XMPPError::Type error = XMPPError::NoError; - const char* errorText = 0; - bool respond = true; - JGSession::Version ver = JGSession::VersionUnknown; - String sid; - Lock lock(this); - switch (event->type()) { - case JBEvent::IqJingleGet: - // Jingle stanzas should never have type='get' - Debug(this,DebugNote,"Received iq jingle stanza with type='get'"); - error = XMPPError::SServiceUnavailable; - break; - case JBEvent::IqJingleSet: - if (!(event->element() && child)) { - Debug(this,DebugNote,"Received jingle event %s with no element or child",event->name()); - return false; - } - // Set version and session id - if (XMPPUtils::hasXmlns(*child,XMPPNamespace::Jingle)) { - ver = JGSession::Version1; - sid = child->getAttribute("sid"); - } - else if (XMPPUtils::hasXmlns(*child,XMPPNamespace::JingleSession)) { - ver = JGSession::Version0; - sid = child->getAttribute("id"); - } - DDebug(this,DebugAll,"Accepting event=%s child=%s sid=%s version=%d", - event->name(),child->name(),sid.c_str(),ver); - if (sid.null()) { - error = XMPPError::SBadRequest; - errorText = "Missing or empty session id"; - break; - } - // Check for a destination - for (ObjList* o = m_sessions.skipNull(); o; o = o->skipNext()) { - JGSession* session = static_cast(o->get()); - if (session->acceptEvent(event,sid)) { - processed = true; - return true; - } - } - // Check if this an incoming session request - if (event->type() == JBEvent::IqJingleSet) { - JGSession::Action action = JGSession::lookupAction(child->getAttribute("type"),ver); - if (action == JGSession::ActInitiate) { - DDebug(this,DebugAll,"New incoming call from=%s to=%s sid=%s version=%d", - event->from().c_str(),event->to().c_str(),sid.c_str(),ver); - if (!event->ref()) { - error = XMPPError::SInternal; - break; - } - switch (ver) { - case JGSession::Version1: - m_sessions.append(new JGSession1(this,event,sid)); - break; - case JGSession::Version0: - m_sessions.append(new JGSession0(this,event,sid)); - break; - default: - Debug(this,DebugStub, - "JGEngine::accept(): unhandled session version %d",ver); - } - processed = true; - return true; - } - } - error = XMPPError::SRequest; - errorText = "Unknown session"; - break; - case JBEvent::IqJingleRes: - case JBEvent::IqJingleErr: - case JBEvent::IqResult: - case JBEvent::IqError: - case JBEvent::WriteFail: - respond = false; - for (ObjList* o = m_sessions.skipNull(); o; o = o->skipNext()) { - JGSession* session = static_cast(o->get()); - if (session->acceptEvent(event)) { - processed = true; - return true; - } - } - break; - case JBEvent::Iq: - // Check file transfer - if (child && child->type() == XMLElement::Query && - XMPPUtils::hasXmlns(*child,XMPPNamespace::ByteStreams)) { - sid = child->getAttribute("sid"); - // Check for a destination - for (ObjList* o = m_sessions.skipNull(); o; o = o->skipNext()) { - JGSession* session = static_cast(o->get()); - if (session->acceptEvent(event,sid)) { - processed = true; - return true; - } - } - } - break; - case JBEvent::Terminated: - case JBEvent::Destroy: - for (ObjList* o = m_sessions.skipNull(); o; o = o->skipNext()) { - JGSession* session = static_cast(o->get()); - if (event->stream() == session->stream()) - session->enqueue(new JBEvent((JBEvent::Type)event->type(), - event->stream(),0)); - } - break; - default: - return false; - } - if (error == XMPPError::NoError) - return false; - - Debug(this,DebugNote,"Accepted event=%s child=%s. Invalid: error=%s text=%s", - event->name(),child?child->name():"",s_err[error],errorText); - - // Send error - if (respond) { - XMLElement* iq = XMPPUtils::createError(event->releaseXML(),XMPPError::TypeModify, - error,errorText); - event->stream()->sendStanza(iq); - } - processed = true; - return true; -} - -// Process generated events -void JGEngine::processEvent(JGEvent* event) -{ - Debug(this,DebugStub,"JGEngine::processEvent. Calling default processor"); - defProcessEvent(event); -} - -// Create a local session id -void JGEngine::createSessionId(String& id) -{ - Lock lock(m_sessionIdMutex); - id = "JG"; - id << (unsigned int)m_sessionId << "_" << (int)random(); - m_sessionId++; -} - - -/** - * JGEvent - */ -JGEvent::~JGEvent() -{ - if (m_session) { - if (!m_confirmed) - confirmElement(XMPPError::UndefinedCondition,"Unhandled"); - m_session->eventTerminated(this); - TelEngine::destruct(m_session); - } - TelEngine::destruct(releaseXML()); - XDebug(DebugAll,"JGEvent::~JGEvent [%p]",this); -} - -void JGEvent::init(JGSession* session) -{ - XDebug(DebugAll,"JGEvent::JGEvent [%p]",this); - if (session && session->ref()) - m_session = session; - if (m_element) { - m_id = m_element->getAttribute("id"); - if (m_session) - switch (m_session->version()) { - case JGSession::Version1: - m_jingle = m_element->findFirstChild(XMLElement::Jingle); - break; - case JGSession::Version0: - m_jingle = m_element->findFirstChild(XMLElement::Session); - break; - case JGSession::VersionUnknown: - ; - } - } -} - -// Set the jingle action as enumeration. Set confirmation flag if -// the element don't require it -void JGEvent::setAction(JGSession::Action act) -{ - m_action = act; - m_confirmed = !(m_element && act != JGSession::ActCount); -} - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/xmlparser.cpp b/libs/yjingle/xmlparser.cpp deleted file mode 100644 index 60f760f8..00000000 --- a/libs/yjingle/xmlparser.cpp +++ /dev/null @@ -1,487 +0,0 @@ -/** - * xmlparser.cpp - * Yet Another XMPP Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include - -using namespace TelEngine; - -/** - * XMLElement - */ -TokenDict XMLElement::s_names[] = { - {"stream:stream", StreamStart}, - {"/stream:stream", StreamEnd}, - {"stream:error", StreamError}, - {"stream:features", StreamFeatures}, - {"register", Register}, - {"starttls", Starttls}, - {"handshake", Handshake}, - {"auth", Auth}, - {"challenge", Challenge}, - {"abort", Abort}, - {"aborted", Aborted}, - {"response", Response}, - {"proceed", Proceed}, - {"success", Success}, - {"failure", Failure}, - {"mechanisms", Mechanisms}, - {"mechanism", Mechanism}, - {"session", Session}, - {"iq", Iq}, - {"message", Message}, - {"presence", Presence}, - {"error", Error}, - {"query", Query}, - {"vCard", VCard}, - {"jingle", Jingle}, - {"description", Description}, - {"payload-type", PayloadType}, - {"transport", Transport}, - {"candidate", Candidate}, - {"body", Body}, - {"subject", Subject}, - {"feature", Feature}, - {"bind", Bind}, - {"resource", Resource}, - {"transfer", Transfer}, - {"hold", Hold}, - {"active", Active}, - {"ringing", Ringing}, - {"mute", Mute}, - {"registered", Registered}, - {"remove", Remove}, - {"jid", Jid}, - {"username", Username}, - {"password", Password}, - {"digest", Digest}, - {"required", Required}, - {"dtmf", Dtmf}, - {"dtmf-method", DtmfMethod}, - {"command", Command}, - {"text", Text}, - {"item", Item}, - {"group", Group}, - {"reason", Reason}, - {"content", Content}, - {"parameter", Parameter}, - {"crypto", Crypto}, - {"crypto-required", CryptoRequired}, - {"trying", Trying}, - {"received", Received}, - {"file", File}, - {"offer", Offer}, - {"request", Request}, - {"streamhost", StreamHost}, - {"streamhost-used", StreamHostUsed}, - {0,0} -}; - -XMLElement::XMLElement() - : m_type(StreamEnd), m_owner(true), m_element(0) -{ - m_element = new TiXmlElement(typeName(m_type)); -// XDebug(DebugAll,"XMLElement::XMLElement(%s) [%p]",name(),this); -} - -XMLElement::XMLElement(const XMLElement& src) - : GenObject(), - m_type(Invalid), m_owner(true), m_element(0) -{ - TiXmlElement* e = src.get(); - if (!e) - return; - m_element = new TiXmlElement(*e); - setType(); -// XDebug(DebugAll,"XMLElement::XMLElement(%s) [%p]",name(),this); -} - -// Partially build this element from another one. -// Copy name and 'to', 'from', 'type', 'id' attributes -XMLElement::XMLElement(const XMLElement& src, bool response, bool result) - : m_type(src.type()), m_owner(true), m_element(0) -{ - m_element = new TiXmlElement(src.name()); - if (response) { - setAttributeValid("from",src.getAttribute("to")); - setAttributeValid("to",src.getAttribute("from")); - setAttribute("type",result?"result":"error"); - } - else { - setAttributeValid("from",src.getAttribute("from")); - setAttributeValid("to",src.getAttribute("to")); - setAttributeValid("type",src.getAttribute("type")); - } - setAttributeValid("id",src.getAttribute("id")); -// XDebug(DebugAll,"XMLElement::XMLElement(%s) [%p]",name(),this); -} - -XMLElement::XMLElement(const char* name, NamedList* attributes, - const char* text) - : m_type(Unknown), m_owner(true), m_element(0) -{ - m_element = new TiXmlElement(name); - // Set text - if (text) - m_element->LinkEndChild(new TiXmlText(text)); - // Add attributes - if (attributes) { - u_int32_t attr_count = attributes->length(); - for (u_int32_t i = 0; i < attr_count; i++) { - NamedString* ns = attributes->getParam(i); - if (!ns) - continue; - m_element->SetAttribute(ns->name().safe(),ns->safe()); - } - } - setType(); -// XDebug(DebugAll,"XMLElement::XMLElement(%s) [%p]",this->name(),this); -} - -XMLElement::XMLElement(Type type, NamedList* attributes, - const char* text) - : m_type(type), m_owner(true), m_element(0) -{ - m_element = new TiXmlElement(typeName(m_type)); - // Set text - if (text) - m_element->LinkEndChild(new TiXmlText(text)); - // Add attributes - if (attributes) { - u_int32_t attr_count = attributes->length(); - for (u_int32_t i = 0; i < attr_count; i++) { - NamedString* ns = attributes->getParam(i); - if (!ns) - continue; - m_element->SetAttribute(ns->name().safe(),ns->safe()); - } - } -// XDebug(DebugAll,"XMLElement::XMLElement(%s) [%p]",name(),this); -} - -XMLElement::XMLElement(TiXmlElement* element, bool owner) - : m_type(Unknown), m_owner(owner), m_element(element) -{ - setType(); -// XDebug(DebugAll,"XMLElement::XMLElement(%s) owner=%u [%p]", -// name(),m_owner,this); -} - -// Build this XML element from a list containing name, attributes and text. -XMLElement::XMLElement(NamedList& src, const char* prefix) - : m_type(Unknown), m_owner(true), m_element(0) -{ - m_element = new TiXmlElement(src.getValue(prefix)); - DDebug(DebugAll,"XMLElement(%s) src=%s prefix=%s [%p]", - m_name.c_str(),src.c_str(),prefix,this); - String pref(String(prefix) + "."); - // Set text - const char* text = src.getValue(pref); - if (text) - m_element->LinkEndChild(new TiXmlText(text)); - // Add attributes - unsigned int n = src.count(); - for (unsigned int i = 0; i < n; i++) { - NamedString* ns = src.getParam(i); - if (ns && ns->name().startsWith(pref)) - setAttribute(ns->name().substr(pref.length()),*ns); - } - setType(); -} - -XMLElement::~XMLElement() -{ - if (m_owner && m_element) - delete m_element; -// XDebug(DebugAll,"XMLElement::~XMLElement(%s) owner=%u [%p]", -// m_name.c_str(),m_owner,this); -} - -void XMLElement::toString(String& dest, bool unclose) const -{ - dest.clear(); - if (valid()) { - TIXML_OSTREAM xmlStr; - m_element->StreamOut(&xmlStr,unclose); - dest.assign(xmlStr.c_str(),xmlStr.length()); - } -} - -// Put this element's name, text and attributes to a list of parameters -void XMLElement::toList(NamedList& dest, const char* prefix) -{ - XDebug(DebugAll,"XMLElement(%s) to list=%s prefix=%s [%p]", - m_name.c_str(),dest.c_str(),prefix,this); - dest.addParam(prefix,name()); - String pref(String(prefix) + "."); - const char* tmp = getText(); - if (tmp) - dest.addParam(pref,tmp); - for (const TiXmlAttribute* a = firstAttribute(); a; a = a->Next()) - dest.addParam(pref + a->Name(),a->Value()); -} - -void XMLElement::setAttribute(const char* name, const char* value) -{ - if (!(valid() && name && value)) - return; - m_element->SetAttribute(name,value); -} - -const char* XMLElement::getAttribute(const char* name) const -{ - if (valid() && name) - return m_element->Attribute(name); - return 0; -} - -// Fill a list with element's attributes -void XMLElement::getAttributes(NamedList& dest) const -{ - if (!valid()) - return; - const TiXmlAttribute* attr = m_element->FirstAttribute(); - for (; attr; attr = attr->Next()) - dest.addParam(attr->Name(),attr->Value()); -} - -bool XMLElement::hasAttribute(const char* name, const char* value) const -{ - String tmp; - if (getAttribute(name,tmp)) - return tmp == value; - return false; -} - -const char* XMLElement::getText() const -{ - if (valid()) - return m_element->GetText(); - return 0; -} - -void XMLElement::addChild(XMLElement* element) -{ - if (valid() && element) { - TiXmlElement* tiElement = element->releaseOwnership(); - if (tiElement) - m_element->LinkEndChild(tiElement); - } - TelEngine::destruct(element); -} - -// Find the first child element of this one. -// Remove it from the children list. -// This element must own its TiXmlElement pointer. -XMLElement* XMLElement::removeChild(const char* name) -{ - if (!valid() && m_owner) - return 0; - TiXmlElement* element; - if (name && *name) - element = ((TiXmlNode*)m_element)->FirstChildElement(name); - else - element = ((TiXmlNode*)m_element)->FirstChildElement(); - if (!element) - return 0; - m_element->RemoveChild(element,false); - return new XMLElement(element,true); -} - -XMLElement* XMLElement::findFirstChild(const char* name) -{ - if (!valid()) - return 0; - TiXmlElement* element; - if (name && *name) - element = ((TiXmlNode*)m_element)->FirstChildElement(name); - else - element = ((TiXmlNode*)m_element)->FirstChildElement(); - if (element) - return new XMLElement(element,false); - return 0; -} - -XMLElement* XMLElement::findNextChild(XMLElement* element, const char* name) -{ - if (!valid()) { - TelEngine::destruct(element); - return 0; - } - TiXmlElement* tiElement = element ? element->get() : 0; - XMLElement* result = 0; - if (tiElement) { - if (name && *name) - tiElement = tiElement->NextSiblingElement(name); - else - tiElement = tiElement->NextSiblingElement(); - if (tiElement) - result = new XMLElement(tiElement,false); - } - else - result = findFirstChild(name); - TelEngine::destruct(element); - return result; -} - -// Get an xml element from a list's parameter -XMLElement* XMLElement::getXml(NamedList& list, bool stole, - const char* name, const char* value) -{ - NamedString* ns = list.getParam(name); - if (!ns) - return 0; - NamedPointer* np = static_cast(ns->getObject("NamedPointer")); - if (!(np && np->userObject("XMLElement")) || (value && *np != value)) - return 0; - if (stole) - return static_cast(np->takeData()); - return static_cast(np->userData()); -} - -TiXmlElement* XMLElement::releaseOwnership() -{ - if (!(m_owner && m_element)) - return 0; - TiXmlElement* tiElement = m_element; - m_element = 0; - m_owner = false; - return tiElement; -} - -/** - * XMLParser - */ -const char* skipBlanks(const char* p) -{ - for (; *p == ' ' || *p == '\r' || *p == '\n' || *p == '\t'; p++); - return p; -} - -u_int32_t XMLParser::s_maxDataBuffer = XMLPARSER_MAXDATABUFFER; -TiXmlEncoding XMLParser::s_xmlEncoding = TIXML_ENCODING_UTF8; - -bool XMLParser::consume(const char* data, u_int32_t len) -{ - // * Input serialization is assumed to be done by the source - // * Lock is necessary only when modifying TiXMLDocument - // 1. Add data to buffer. Adjust stream start tag - // 2. Call TiXMLDocument::Parse() - // 3. Check result - - // 1 - String tmp(data,len); - m_buffer << tmp; -// XDebug(DebugAll,"XMLParser::consume. Buffer: '%s'.",m_buffer.c_str()); - if (m_buffer.length() > s_maxDataBuffer) { - SetError(TIXML_ERROR_BUFFEROVERRUN,0,0,s_xmlEncoding); - return false; - } - // Check for start element. - // Adjust termination tag in order to pass correct data to the parser - if (m_findstart) { - int start = m_buffer.find("stream:stream"); - // Don't process until found: 'stream:stream' ... '>' - if (start == -1) - return true; - int end = m_buffer.find(">",start); - if (end == -1) - return true; - // Check if we received an end stream - // Search for a '/' before 'stream:stream' - int i_end = m_buffer.find("/"); - bool b_end = false; - if (i_end != -1 && i_end < start) { - const char* pEnd = m_buffer.c_str() + i_end + 1; - const char* pStart = m_buffer.c_str() + start; - if (pStart == skipBlanks(pEnd)) - b_end = true; - } - if (!b_end) { - m_findstart = false; - if (i_end > start && i_end < end) { - // We found a '/' between 'stream:stream' and '>' - // Do nothing: The tag is already closed or the element - // is invalid and the parser will fail - } - // Found: insert '/' - String tmp = m_buffer.substr(end,m_buffer.length() - end); - m_buffer = m_buffer.substr(0,end) << " /" << tmp; - } - // If we received end stream tag before start stream tag - // The element will be parsed: the upper layer will deal with it - } - if (!m_buffer) - return true; - lock(); - const char* src = m_buffer.c_str(); - const char* ret = Parse(src,0,s_xmlEncoding); - unlock(); - // Remove processed data from bufer - if (ret > src && ret <= src + m_buffer.length()) - m_buffer = m_buffer.substr(ret - src); -// XDebug(DebugAll, -// "XMLParser::consume. Data parsed. Buffer: '%s'. Error: '%s'.", -// m_buffer.c_str(),ErrorDesc()); - // 3 - if (ErrorId() && ErrorId() != TIXML_ERROR_INCOMPLETE) - return false; - return true; -} - -XMLElement* XMLParser::extract() -{ - // Find the first TiXMLElement - // Check if we received stream end - // Remove any other object type - for (;;) { - TiXmlNode* node = FirstChild(); - if (!node) - break; - // Check for XML elements - if (node->ToElement()) { - RemoveChild(node,false); - return new XMLElement(node->ToElement(),true); - } - // Check for end stream - // For Tiny XML '' is an unknown element - if (node->ToUnknown() && - XMLElement::isType(node->Value(),XMLElement::StreamEnd)) { - RemoveChild(node,true); - return new XMLElement(); - } - // Remove non-element - RemoveChild(node,true); - } - return 0; -} - -void XMLParser::reset() -{ - Lock lock(this); - TiXmlDocument::Clear(); - m_buffer.clear(); - m_findstart = true; -} - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/xmlparser.h b/libs/yjingle/xmlparser.h deleted file mode 100644 index 13edb50a..00000000 --- a/libs/yjingle/xmlparser.h +++ /dev/null @@ -1,704 +0,0 @@ -/** - * xmlparser.h - * Yet Another XMPP Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#ifndef __XMLPARSER_H -#define __XMLPARSER_H - -#include -#include - -#ifdef _WINDOWS - -#ifdef LIBYJINGLE_EXPORTS -#define YJINGLE_API __declspec(dllexport) -#else -#ifndef LIBYJINGLE_STATIC -#define YJINGLE_API __declspec(dllimport) -#endif -#endif - -#endif /* _WINDOWS */ - -#ifndef YJINGLE_API -#define YJINGLE_API -#endif - -/** - * Holds all Telephony Engine related classes. - */ -namespace TelEngine { - -#define XMLPARSER_MAXDATABUFFER 8192 // Default max data buffer - -class XMLElement; -class XMLParser; -class XMLElementOut; - -/** - * This class holds an XML element - * @short An XML element - */ -class YJINGLE_API XMLElement : public GenObject -{ - friend class XMLParser; -public: - /** - * Element type as enumeration - */ - enum Type { - // *** Stream related elements - StreamStart, // stream:stream - StreamEnd, // /stream:stream - StreamError, // stream::error - StreamFeatures, // stream::features - Register, // register - Starttls, // starttls - Handshake, // handshake - Auth, // auth - Challenge, // challenge - Abort, // abort - Aborted, // aborted - Response, // response - Proceed, // proceed - Success, // success - Failure, // failure - Mechanisms, // mechanisms - Mechanism, // mechanism - Session, // session - // *** Stanzas - Iq, // iq - Message, // message - Presence, // presence - // *** Stanza children - Error, // error - Query, // query - VCard, // vCard - Jingle, // session - // Description - Description, // description - PayloadType, // payload-type - // Transport - Transport, // transport - Candidate, // candidate - // Message - Body, // body - Subject, // subject - // Resources - Feature, // feature - Bind, // bind - Resource, // resource - // Jingle session info - Transfer, // transfer - Hold, // hold - Active, // active - Ringing, // ringing - Mute, // mute - // Miscellaneous - Registered, // registered - Remove, // remove - Jid, // jid - Username, // username - Password, // password - Digest, // digest - Required, // required - Dtmf, // dtmf - DtmfMethod, // dtmf-method - Command, // command - Text, // text - Item, // item - Group, // group - Reason, // reason - Content, // content - Parameter, // parameter - Crypto, // crypto - CryptoRequired, // crypto-required - Trying, // trying - Received, // received - File, // file - Offer, // offer - Request, // request - StreamHost, // streamhost - StreamHostUsed, // streamhost-used - Unknown, // Any text - Invalid, // m_element is 0 - }; - - /** - * Constructor. - * Constructs a StreamEnd element - */ - XMLElement(); - - /** - * Copy constructor - * @param src Source element - */ - XMLElement(const XMLElement& src); - - /** - * Constructor. Partially build this element from another one. - * Copy name and 'to', 'from', 'type', 'id' attributes - * @param src Source element - * @param response True to reverse 'to' and 'from' attributes - * @param result True to set type to "result", false to set it to "error". - * Ignored if response is false - */ - XMLElement(const XMLElement& src, bool response, bool result); - - /** - * Constructor. - * Constructs an XML element with a TiXmlElement element with the given name. - * Used for outgoing elements - * @param name The element's name - * @param attributes Optional list of attributes - * @param text Optional text for the XML element - */ - XMLElement(const char* name, NamedList* attributes = 0, const char* text = 0); - - /** - * Constructor. - * Constructs an XML element with a TiXmlElement element with the given type's name. - * Used for outgoing elements - * @param type The element's type - * @param attributes Optional list of attributes - * @param text Optional text for the XML element - */ - XMLElement(Type type, NamedList* attributes = 0, const char* text = 0); - - /** - * Constructor. - * Build this XML element from a list containing name, attributes and text. - * Element's name must be a parameter whose name must be equal to prefix. - * Element's text must be a parameter whose name is prefix followed by a dot. - * The list of attributes will be built from parameters starting with prefix.attributename - * @param src The list containing data used to build this XML element - * @param prefix The prefix used to search the list of parameters - */ - XMLElement(NamedList& src, const char* prefix); - - /** - * Destructor. Deletes the underlying TiXmlElement if owned - */ - virtual ~XMLElement(); - - /** - * Get the type of this object - * @return The type of this object as enumeration - */ - inline Type type() const - { return m_type; } - - /** - * Get the TiXmlElement's name - * @return The name of the TiXmlElement object or 0 - */ - inline const char* name() const - { return valid() ? m_element->Value() : 0; } - - /** - * Check if the TiXmlElement's name is the given text - * @param text Text to compare with - * @return False if text is 0 or not equal to name - */ - inline bool nameIs(const char* text) const - { return (text && name() && (0 == ::strcmp(name(),text))); } - - /** - * Get the validity of this object - * @return True if m_element is non null - */ - inline bool valid() const - { return m_element != 0; } - - /** - * Change the type of this object - * @param t The new type of this object - */ - inline void changeType(Type t) - { m_type = t; } - - /** - * Put the element in a buffer - * @param dest Destination string - * @param unclose True to leave the tag unclosed - */ - void toString(String& dest, bool unclose = false) const; - - /** - * Put this element's name, text and attributes to a list of parameters - * @param dest Destination list - * @param prefix Prefix to add to parameters - */ - void toList(NamedList& dest, const char* prefix); - - /** - * Set the value of an existing attribute or adds a new one - * @param name Attribute's name - * @param value Attribute's value - */ - void setAttribute(const char* name, const char* value); - - /** - * Set the value of an existing attribute or adds a new one if the value's length is not 0 - * @param name Attribute's name - * @param value Attribute's value - */ - inline void setAttributeValid(const char* name, const String& value) { - if (value) - setAttribute(name,value); - } - - /** - * Set the value of an existing attribute or adds a new one from an integer - * @param name Attribute's name - * @param value Attribute's value - */ - inline void setAttribute(const char* name, int value) { - String s(value); - setAttribute(name,s); - } - - /** - * Get the value of an attribute - * @param name Attribute's name - * @return Attribute's value. May be 0 if doesn't exists or empty - */ - const char* getAttribute(const char* name) const; - - /** - * Get the value of an attribute - * @param name Attribute's name - * @param value Destination string - * @return True if attribute with the given name exists and is not empty - */ - inline bool getAttribute(const char* name, String& value) const { - value = getAttribute(name); - return 0 != value.length(); - } - - /** - * Fill a list with element's attributes - * @param dest The destination list - */ - void getAttributes(NamedList& dest) const; - - /** - * Check if an attribute with the given name and value exists - * @param name Attribute's name - * @param value Attribute's value - * @return True/False - */ - bool hasAttribute(const char* name, const char* value) const; - - /** - * Get the text of this XML element - * @return Pointer to the text of this XML element or 0 - */ - const char* getText() const; - - /** - * Add a child to this object. Release the received element - * @param element XMLElement to add - */ - void addChild(XMLElement* element); - - /** - * Find the first child element of this one. - * Remove it from the children list. - * If an element is returned, it owns the TiXmlElement pointer. - * This element must own its TiXmlElement pointer. - * @param name Optional name of the child - * @return Pointer to an XMLElement or 0 if not found - */ - XMLElement* removeChild(const char* name = 0); - - /** - * Find the first child element of this one. - * Remove it from the children list. - * If an element is returned, it owns the TiXmlElement pointer. - * This element must own its TiXmlElement pointer. - * @param type Child's type to find - * @return Pointer to an XMLElement or 0 if not found - */ - inline XMLElement* removeChild(Type type) - { return removeChild(typeName(type)); } - - /** - * Find the first child element of this one. - * If an element is returned, it is a newly allocated one, not owning its TiXmlElement pointer - * @param name Optional name of the child - * @return Pointer to an XMLElement or 0 if not found - */ - XMLElement* findFirstChild(const char* name = 0); - - /** - * Find the first child element of the given type. - * If an element is returned, it is a newly allocated one, not owning its TiXmlElement pointer - * @param type Child's type to find - * @return Pointer to an XMLElement or 0 if not found - */ - inline XMLElement* findFirstChild(Type type) - { return findFirstChild(typeName(type)); } - - /** - * Check if this element has a given child - * @param name Optional name of the child (check for the first one if 0) - * @return True if this element has the desired child - */ - inline bool hasChild(const char* name) { - XMLElement* tmp = findFirstChild(name); - bool ok = (0 != tmp); - TelEngine::destruct(tmp); - return ok; - } - - /** - * Check if this element has a given child - * @param type Child's type to find - * @return True if this element has the desired child - */ - inline bool hasChild(Type type) - { return hasChild(typeName(type)); } - - /** - * Find the next child element. Delete the starting element if not 0. - * If an element is returned, it is a newly allocated one, not owning its TiXmlElement pointer - * @param element Starting XMLElement. O to find from the beginning - * @param name Optional name of the child - * @return Pointer to an XMLElement or 0 if not found - */ - XMLElement* findNextChild(XMLElement* element, const char* name = 0); - - /** - * Find the next child element of the given type. Delete the starting element if not 0. - * If an element is returned, it is a newly allocated one, not owning its TiXmlElement pointer - * @param element Starting XMLElement. O to find from the beginning - * @param type Child's type to find - * @return Pointer to an XMLElement or 0 if not found - */ - inline XMLElement* findNextChild(XMLElement* element, Type type) - { return findNextChild(element,typeName(type)); } - - /** - * Find the first attribute - * @return Pointer to the first attribute or 0 - */ - inline const TiXmlAttribute* firstAttribute() const - { return valid() ? m_element->FirstAttribute() : 0; } - - /** - * Get the name associated with the given type - * @param type Element type as enumeration - * @return Pointer to the name or 0 - */ - static inline const char* typeName(Type type) - { return lookup(type,s_names); } - - /** - * Check if the given text is equal to the one associated with the given type - * @param txt Text to compare - * @param type Element type as enumeration - * @return True if txt equals the text associated with the given type - */ - static inline bool isType(const char* txt, Type type) { - const char* s = typeName(type); - return (txt && s && (0 == ::strcmp(txt,s))); - } - - /** - * Get a pointer to this object - */ - virtual void* getObject(const String& name) const { - if (name == "XMLElement") - return (void*)this; - return GenObject::getObject(name); - } - - /** - * Release memory - */ - virtual const String& toString() const - { return m_name; } - - /** - * Release memory - */ - virtual void destruct() { - if (m_owner && m_element) - delete m_element; - m_element = 0; - GenObject::destruct(); - } - - /** - * Get an xml element from a list's parameter - * @param list The list to be searched for the given parameter - * @param stole True to release parameter ownership (defaults to false) - * @param name Parameter name (defaults to 'xml') - * @param value Optional parameter value to check - * @return XMLElement pointer or 0. If a valid pointer is returned and - * stole is true the caller will own the pointer - */ - static XMLElement* getXml(NamedList& list, bool stole = false, - const char* name = "xml", const char* value = 0); - - /** - * Associations between XML element name and type - */ - static TokenDict s_names[]; - -protected: - /** - * Constructor. - * Constructs an XML element from a TiXmlElement. - * Used to extract elements from parser and access the children. - * When extracting elements from parser the object will own the TiXmlElement. - * When accessing the children, the object will not own the TiXmlElement - * @param element Pointer to a valid TiXmlElement - * @param owner Owner flag - */ - XMLElement(TiXmlElement* element, bool owner); - - /** - * Get the underlying TiXmlElement - * @return The underlying TiXmlElement object or 0 - */ - inline TiXmlElement* get() const - { return m_element; } - - /** - * Release the ownership of the underlying TiXmlElement - * and returns it if the object owns it - * @return The underlying TiXmlElement object or 0 if not owned or 0 - */ - TiXmlElement* releaseOwnership(); - -private: - // Set this object's type from m_element's name - inline void setType() { - m_name = name(); - m_type = (Type)lookup(name(),s_names,Unknown); - } - - Type m_type; // Element's type - bool m_owner; // Owner flag. If true, this object owns the XML element - String m_name; // The name of this element - TiXmlElement* m_element; // The underlying XML element -}; - -/** - * This class is responsable of parsing incoming data. - * Keeps the resulting XML elements and the input buffer - * @short An XML parser - */ -class YJINGLE_API XMLParser : public TiXmlDocument, public Mutex -{ -public: - /** - * Constructor. - * Constructs an XML parser - */ - inline XMLParser() - : TiXmlDocument(), Mutex(true), m_findstart(true) - {} - - /** - * Destructor - */ - virtual ~XMLParser() - {} - - /** - * Add data to buffer. Parse the buffer. - * On success, the already parsed data is removed from buffer. - * This method is thread safe - * @param data Pointer to the data to consume - * @param len Data length - * @return True on successfully parsed - */ - bool consume(const char* data, u_int32_t len); - - /** - * Extract the first XML element from document. - * Remove non-element children of the document (e.g. declaration). - * This method is thread safe - * @return Pointer to an XMLElement or 0 if the document is empty - */ - XMLElement* extract(); - - /** - * Get the buffer length (incomplete data) - * @return The number of bytes belonging to an incomplete XML element - */ - inline unsigned int bufLen() const - { return m_buffer.length(); } - - /** - * Get a copy of the parser's buffer - * @param dest Destination string - */ - inline void getBuffer(String& dest) const - { dest = m_buffer; } - - /** - * Clear the parser's input buffer and already parsed elements. Reset data - */ - void reset(); - - /** - * The maximum allowed buffer length - */ - static u_int32_t s_maxDataBuffer; - - /** - * The XML encoding - */ - static TiXmlEncoding s_xmlEncoding; - -private: - String m_buffer; // Input data buffer - bool m_findstart; // Search for stream start tag or not -}; - -/** - * This class holds an XML element to be sent through a stream - * @short An outgoing XML element - */ -class YJINGLE_API XMLElementOut : public RefObject -{ -public: - /** - * Constructor - * @param element The XML element - * @param senderID Optional sender id - * @param unclose True to not close the tag when building the buffer - */ - inline XMLElementOut(XMLElement* element, const char* senderID = 0, - bool unclose = false) - : m_element(element), m_offset(0), m_id(senderID), m_unclose(unclose), - m_sent(false) - {} - - /** - * Destructor - * Delete m_element if not 0 - */ - virtual ~XMLElementOut() - { TelEngine::destruct(m_element); } - - /** - * Get the underlying element - * @return The underlying element - */ - inline XMLElement* element() const - { return m_element; } - - /** - * Check if this element was (partially) sent - * @return True if an attempt to send this element was already done - */ - inline bool sent() const - { return m_sent; } - - /** - * Get the data buffer - * @return The data buffer - */ - inline String& buffer() - { return m_buffer; } - - /** - * Get the id member - * @return The id member - */ - inline const String& id() const - { return m_id; } - - /** - * Get the remainig byte count to send - * @return The unsent number of bytes - */ - inline u_int32_t dataCount() - { return m_buffer.length() - m_offset; } - - /** - * Get the remainig data to send. Set the buffer if not already set - * @param nCount The number of unsent bytes - * @return Pointer to the remaining data or 0 - */ - inline const char* getData(u_int32_t& nCount) { - if (!m_buffer) - prepareToSend(); - nCount = dataCount(); - return m_buffer.c_str() + m_offset; - } - - /** - * Increase the offset with nCount bytes. Set the sent flag - * @param nCount The number of bytes sent - */ - inline void dataSent(u_int32_t nCount) { - m_sent = true; - m_offset += nCount; - if (m_offset > m_buffer.length()) - m_offset = m_buffer.length(); - } - - /** - * Release the ownership of m_element - * The caller is responsable of returned pointer - * @return XMLElement pointer or 0 - */ - inline XMLElement* release() { - XMLElement* e = m_element; - m_element = 0; - return e; - } - - /** - * Fill a buffer with the XML element to send - * @param buffer The buffer to fill - */ - inline void toBuffer(String& buffer) - { if (m_element) m_element->toString(buffer,m_unclose); } - - /** - * Fill the buffer with the XML element to send - */ - inline void prepareToSend() - { toBuffer(m_buffer); } - -private: - XMLElement* m_element; // The XML element - String m_buffer; // Data to send - u_int32_t m_offset; // Offset to send - String m_id; // Sender's id - bool m_unclose; // Close or not the element's tag - bool m_sent; // Sent flag (true if an attempt to send was done) -}; - -}; - -#endif /* __XMLPARSER_H */ - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/xmpputils.cpp b/libs/yjingle/xmpputils.cpp deleted file mode 100644 index 511fb685..00000000 --- a/libs/yjingle/xmpputils.cpp +++ /dev/null @@ -1,886 +0,0 @@ -/** - * utils.cpp - * Yet Another Jabber Component Protocol Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include - -using namespace TelEngine; - -#include - -static XMPPNamespace s_ns; -static XMPPError s_err; - -TokenDict XMPPServerInfo::s_flagName[] = { - {"noautorestart", NoAutoRestart}, - {"keeproster", KeepRoster}, - {"tlsrequired", TlsRequired}, - {"oldstyleauth", OldStyleAuth}, - {"allowplainauth", AllowPlainAuth}, - {"allowunsafesetup", AllowUnsafeSetup}, - {0,0} -}; - -TokenDict XMPPDirVal::s_names[] = { - {"none", None}, - {"to", To}, - {"from", From}, - {"both", Both}, - {0,0}, -}; - -/** - * XMPPNamespace - */ -TokenDict XMPPNamespace::s_value[] = { - {"http://etherx.jabber.org/streams", Stream}, - {"jabber:client", Client}, - {"jabber:server", Server}, - {"jabber:component:accept", ComponentAccept}, - {"jabber:component:connect", ComponentConnect}, - {"urn:ietf:params:xml:ns:xmpp-streams", StreamError}, - {"urn:ietf:params:xml:ns:xmpp-stanzas", StanzaError}, - {"http://jabber.org/features/iq-register", Register}, - {"jabber:iq:register", IqRegister}, - {"jabber:iq:private", IqPrivate}, - {"jabber:iq:auth", IqAuth}, - {"http://jabber.org/features/iq-auth", IqAuthFeature}, - {"urn:ietf:params:xml:ns:xmpp-tls", Starttls}, - {"urn:ietf:params:xml:ns:xmpp-sasl", Sasl}, - {"urn:ietf:params:xml:ns:xmpp-session", Session}, - {"urn:ietf:params:xml:ns:xmpp-bind", Bind}, - {"jabber:iq:roster", Roster}, - {"jabber:iq:roster-dynamic", DynamicRoster}, - {"http://jabber.org/protocol/disco#info", DiscoInfo}, - {"http://jabber.org/protocol/disco#items", DiscoItems}, - {"vcard-temp", VCard}, - {"http://jabber.org/protocol/si/profile/file-transfer",SIProfileFileTransfer}, - {"http://jabber.org/protocol/bytestreams", ByteStreams}, - {"urn:xmpp:jingle:0", Jingle}, - {"urn:xmpp:jingle:errors:0", JingleError}, - {"urn:xmpp:jingle:apps:rtp:0", JingleAppsRtp}, - {"urn:xmpp:jingle:apps:rtp:info:0", JingleAppsRtpInfo}, - {"urn:xmpp:jingle:apps:rtp:audio", JingleAppsRtpAudio}, - {"urn:xmpp:jingle:apps:file-transfer:0", JingleAppsFileTransfer}, - {"urn:xmpp:jingle:transports:ice-udp:0", JingleTransportIceUdp}, - {"urn:xmpp:jingle:transports:raw-udp:0", JingleTransportRawUdp}, - {"urn:xmpp:jingle:transports:raw-udp:info:0", JingleTransportRawUdpInfo}, - {"urn:xmpp:jingle:transports:bytestreams:0", JingleTransportByteStreams}, - {"urn:xmpp:jingle:transfer:0", JingleTransfer}, - {"urn:xmpp:jingle:dtmf:0", Dtmf}, - {"http://www.google.com/session", JingleSession}, - {"http://www.google.com/session/phone", JingleAudio}, - {"http://www.google.com/transport/p2p", JingleTransport}, - {"urn:xmpp:jingle:apps:rtp:info", JingleRtpInfoOld}, - {"http://jabber.org/protocol/jingle/info/dtmf", DtmfOld}, - {"http://jabber.org/protocol/commands", Command}, - {"http://www.google.com/xmpp/protocol/voice/v1", CapVoiceV1}, - {0,0} -}; - -bool XMPPNamespace::isText(Type index, const char* txt) -{ - const char* tmp = lookup(index,s_value,0); - if (!(txt && tmp)) - return false; - return 0 == strcmp(tmp,txt); -} - -/** - * XMPPError - */ -TokenDict XMPPError::s_value[] = { - {"cancel", TypeCancel}, - {"continue", TypeContinue}, - {"modify", TypeModify}, - {"auth", TypeAuth}, - {"wait", TypeWait}, - {"bad-format", BadFormat}, - {"bad-namespace-prefix", BadNamespace}, - {"connection-timeout", ConnTimeout}, - {"host-gone", HostGone}, - {"host-unknown", HostUnknown}, - {"improper-addressing", BadAddressing}, - {"internal-server-error", Internal}, - {"invalid-from", InvalidFrom}, - {"invalid-id", InvalidId}, - {"invalid-namespace", InvalidNamespace}, - {"invalid-xml", InvalidXml}, - {"not-authorized", NotAuth}, - {"policy-violation", Policy}, - {"remote-connection-failed", RemoteConn}, - {"resource-constraint", ResConstraint}, - {"restricted-xml", RestrictedXml}, - {"see-other-host", SeeOther}, - {"system-shutdown", Shutdown}, - {"undefined-condition", UndefinedCondition}, - {"unsupported-encoding", UnsupportedEnc}, - {"unsupported-stanza-type", UnsupportedStanza}, - {"unsupported-version", UnsupportedVersion}, - {"xml-not-well-formed", Xml}, - // Auth failures - {"aborted", Aborted}, - {"incorrect-encoding", IncorrectEnc}, - {"invalid-authzid", InvalidAuth}, - {"invalid-mechanism", InvalidMechanism}, - {"mechanism-too-weak", MechanismTooWeak}, - {"not-authorized", NotAuthorized}, - {"temporary-auth-failure", TempAuthFailure}, - // Stanza errors - {"bad-request", SBadRequest}, - {"conflict", SConflict}, - {"feature-not-implemented", SFeatureNotImpl}, - {"forbidden", SForbidden}, - {"gone", SGone}, - {"internal-server-error", SInternal}, - {"item-not-found", SItemNotFound}, - {"jid-malformed", SBadJid}, - {"not-acceptable", SNotAcceptable}, - {"not-allowed", SNotAllowed}, - {"payment-required", SPayment}, - {"recipient-unavailable", SUnavailable}, - {"redirect", SRedirect}, - {"registration-required", SReg}, - {"remote-server-not-found", SNoRemote}, - {"remote-server-timeout", SRemoteTimeout}, - {"resource-constraint", SResource}, - {"service-unavailable", SServiceUnavailable}, - {"subscription-required", SSubscription}, - {"undefined-condition", SUndefinedCondition}, - {"unexpected-request", SRequest}, - {"unsupported-dtmf-method", DtmfNoMethod}, - {"item-not-found", ItemNotFound}, - {0,0} -}; - -bool XMPPError::isText(int index, const char* txt) -{ - const char* tmp = lookup(index,s_value,0); - if (!(txt && tmp)) - return false; - return 0 == strcmp(tmp,txt); -} - -/** - * JabberID - */ -void JabberID::set(const char* jid) -{ - this->assign(jid); - parse(); -} - -void JabberID::set(const char* node, const char* domain, const char* resource) -{ - if (node != m_node.c_str()) - m_node = node; - if (domain != m_domain.c_str()) - m_domain = domain; - if (resource != m_resource.c_str()) - m_resource = resource; - clear(); - if (m_node) - *this << m_node << "@"; - *this << m_domain; - m_bare = *this; - if (m_node && m_resource) - *this << "/" << m_resource; -} - -bool JabberID::valid(const String& value) -{ - if (value.null()) - return true; - return s_regExpValid.matches(value); -} - -Regexp JabberID::s_regExpValid("^\\([[:alnum:]]*\\)"); - -#if 0 -~`!#$%^*_-+=()[]{}|\;?. -#endif - -void JabberID::parse() -{ - String tmp = *this; - int i = tmp.find('@'); - if (i == -1) - m_node = ""; - else { - m_node = tmp.substr(0,i); - tmp = tmp.substr(i+1,tmp.length()-i-1); - } - i = tmp.find('/'); - if (i == -1) { - m_domain = tmp; - m_resource = ""; - } - else { - m_domain = tmp.substr(0,i); - m_resource = tmp.substr(i+1,tmp.length()-i-1); - } - // Set bare JID - m_bare = ""; - if (m_node) - m_bare << m_node << "@"; - m_bare << m_domain; -} - -/** - * JIDIdentity - */ -TokenDict JIDIdentity::s_category[] = { - {"account", Account}, - {"client", Client}, - {"component", Component}, - {"gateway", Gateway}, - {0,0}, - }; - -TokenDict JIDIdentity::s_type[] = { - {"registered", AccountRegistered}, - {"phone", ClientPhone}, - {"generic", ComponentGeneric}, - {"presence", ComponentPresence}, - {"generic", GatewayGeneric}, - {0,0}, - }; - -XMLElement* JIDIdentity::toXML() -{ - return XMPPUtils::createIdentity(categoryText(m_category),typeText(m_type),m_name); -} - -bool JIDIdentity::fromXML(const XMLElement* element) -{ - if (!element) - return false; - XMLElement* id = ((XMLElement*)element)->findFirstChild("identity"); - if (!id) - return false; - m_category = categoryValue(id->getAttribute("category")); - m_type = typeValue(id->getAttribute("type")); - id->getAttribute("name",m_name); - TelEngine::destruct(id); - return true; -} - - -/** - * JIDFeatureSasl - */ -TokenDict JIDFeatureSasl::s_authMech[] = { - {"DIGEST-MD5", MechMD5}, - {"DIGEST-SHA1", MechSHA1}, - {"PLAIN", MechPlain}, - {0,0} -}; - -/** - * JIDFeatureList - */ - -// Find a specific feature -JIDFeature* JIDFeatureList::get(XMPPNamespace::Type feature) -{ - ObjList* obj = m_features.skipNull(); - for (; obj; obj = obj->skipNext()) { - JIDFeature* f = static_cast(obj->get()); - if (*f == feature) - return f; - } - return 0; -} - -// Build an XML element and add it to the destination -XMLElement* JIDFeatureList::addTo(XMLElement* element) -{ - if (!element) - return 0; - ObjList* obj = m_features.skipNull(); - for (; obj; obj = obj->skipNext()) { - JIDFeature* f = static_cast(obj->get()); - XMLElement* feature = new XMLElement(XMLElement::Feature); - feature->setAttribute("var",s_ns[*f]); - element->addChild(feature); - } - return element; -} - -// Update the list from 'feature' children of the given element -void JIDFeatureList::fromXml(XMLElement* element, bool reset) -{ - if (reset) - clear(); - if (!element) - return; - - XMLElement* x = 0; - while (0 != (x = element->findNextChild(x,XMLElement::Feature))) { - XMPPNamespace::Type t = XMPPNamespace::type(x->getAttribute("var")); - if (t != XMPPNamespace::Count && !get(t)) - add(t); - } -} - - -/** - * XMPPUtils - */ -TokenDict XMPPUtils::s_iq[] = { - {"set", IqSet}, - {"get", IqGet}, - {"result", IqResult}, - {"error", IqError}, - {0,0} - }; - -TokenDict XMPPUtils::s_commandAction[] = { - {"execute", CommExecute}, - {"cancel", CommCancel}, - {"prev", CommPrev}, - {"next", CommNext}, - {"complete", CommComplete}, - {0,0} - }; - -TokenDict XMPPUtils::s_commandStatus[] = { - {"executing", CommExecuting}, - {"completed", CommCompleted}, - {"cancelled", CommCancelled}, - {0,0} - }; - -XMLElement* XMPPUtils::createElement(const char* name, XMPPNamespace::Type ns, - const char* text) -{ - XMLElement* element = new XMLElement(name,0,text); - element->setAttribute("xmlns",s_ns[ns]); - return element; -} - -XMLElement* XMPPUtils::createElement(XMLElement::Type type, XMPPNamespace::Type ns, - const char* text) -{ - XMLElement* element = new XMLElement(type,0,text); - element->setAttribute("xmlns",s_ns[ns]); - return element; -} - -XMLElement* XMPPUtils::createIq(IqType type, const char* from, - const char* to, const char* id) -{ - XMLElement* iq = new XMLElement(XMLElement::Iq); - iq->setAttribute("type",lookup(type,s_iq,"")); - iq->setAttributeValid("from",from); - iq->setAttributeValid("to",to); - iq->setAttributeValid("id",id); - return iq; -} - -XMLElement* XMPPUtils::createIqBind( const char* from, - const char* to, const char* id, const ObjList& resources) -{ - XMLElement* iq = createIq(IqSet,from,to,id); - XMLElement* bind = createElement(XMLElement::Bind,XMPPNamespace::Bind); - ObjList* obj = resources.skipNull(); - for (; obj; obj = resources.skipNext()) { - String* s = static_cast(obj->get()); - if (!(s && s->length())) - continue; - XMLElement* res = new XMLElement(XMLElement::Resource,0,*s); - bind->addChild(res); - } - iq->addChild(bind); - return iq; -} - -// Create an 'iq' element of type 'get' with a 'vcard' child -XMLElement* XMPPUtils::createVCard(bool get, const char* from, const char* to, const char* id) -{ - XMLElement* xml = createIq(get ? IqGet : IqSet,from,to,id); - xml->addChild(createElement(XMLElement::VCard,XMPPNamespace::VCard)); - return xml; -} - -XMLElement* XMPPUtils::createCommand(CommandAction action, const char* node, - const char* sessionId) -{ - XMLElement* command = createElement(XMLElement::Command,XMPPNamespace::Command); - if (sessionId) - command->setAttribute("sessionid",sessionId); - command->setAttribute("node",node); - command->setAttribute("action",lookup(action,s_commandAction)); - return command; -} - -XMLElement* XMPPUtils::createIdentity(const char* category, const char* type, - const char* name) -{ - XMLElement* id = new XMLElement("identity"); - id->setAttribute("category",category); - id->setAttribute("type",type); - id->setAttribute("name",name); - return id; -} - -XMLElement* XMPPUtils::createIqDisco(const char* from, const char* to, - const char* id, bool info) -{ - XMLElement* xml = createIq(IqGet,from,to,id); - xml->addChild(createElement(XMLElement::Query, - info ? XMPPNamespace::DiscoInfo : XMPPNamespace::DiscoItems)); - return xml; -} - -XMLElement* XMPPUtils::createDiscoInfoRes(const char* from, const char* to, - const char* id, JIDFeatureList* features, JIDIdentity* identity) -{ - XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqResult,from,to,id); - XMLElement* query = XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::DiscoInfo); - if (identity) - query->addChild(identity->toXML()); - if (features) - features->addTo(query); - iq->addChild(query); - return iq; -} - -XMLElement* XMPPUtils::createError(XMPPError::ErrorType type, - XMPPError::Type condition, const char* text) -{ - XMLElement* err = new XMLElement("error"); - err->setAttribute("type",s_err[type]); - XMLElement* tmp = createElement(s_err[condition],XMPPNamespace::StanzaError); - err->addChild(tmp); - if (text) { - tmp = createElement(XMLElement::Text,XMPPNamespace::StanzaError,text); - err->addChild(tmp); - } - return err; -} - -// Create an error from a received element. Consume the received element -XMLElement* XMPPUtils::createError(XMLElement* xml, XMPPError::ErrorType type, - XMPPError::Type error, const char* text) -{ - if (!xml) - return 0; - XMLElement* err = new XMLElement(*xml,true,false); - // Copy children from xml to the error element - XMLElement* child = 0; - while (0 != (child = xml->removeChild())) - err->addChild(child); - TelEngine::destruct(xml); - // Create the error - err->addChild(createError(type,error,text)); - return err; -} - -XMLElement* XMPPUtils::createStreamError(XMPPError::Type error, const char* text) -{ - XMLElement* element = new XMLElement(XMLElement::StreamError); - XMLElement* err = createElement(s_err[error],XMPPNamespace::StreamError); - element->addChild(err); - if (text) { - XMLElement* txt = createElement(XMLElement::Text,XMPPNamespace::StreamError,text); - element->addChild(txt); - } - return element; -} - -// Build a register query element -XMLElement* XMPPUtils::createRegisterQuery(IqType type, const char* from, - const char* to, const char* id, - XMLElement* child1, XMLElement* child2, XMLElement* child3) -{ - XMLElement* iq = createIq(type,from,to,id); - XMLElement* q = XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::IqRegister); - if (child1) - q->addChild(child1); - if (child2) - q->addChild(child2); - if (child3) - q->addChild(child3); - iq->addChild(q); - return iq; -} - -// Check if the given element has an attribute 'xmlns' equal to a given value -bool XMPPUtils::hasXmlns(XMLElement& element, XMPPNamespace::Type ns) -{ - return element.hasAttribute("xmlns",s_ns[ns]); -} - -// Decode an 'error' XML element -void XMPPUtils::decodeError(XMLElement* element, String& error, String& text) -{ - if (!element) - return; - - XMPPNamespace::Type nsErr; - error = ""; - text = ""; - XMLElement* child = 0; - switch (element->type()) { - case XMLElement::StreamError: - nsErr = XMPPNamespace::StreamError; - break; - case XMLElement::Error: - nsErr = XMPPNamespace::StanzaError; - break; - case XMLElement::Iq: - case XMLElement::Presence: - case XMLElement::Message: - child = element->findFirstChild(XMLElement::Error); - decodeError(child,error,text); - TelEngine::destruct(child); - default: - return; - } - - while (0 != (child = element->findNextChild(child))) - if (hasXmlns(*child,nsErr)) { - error = child->name(); - TelEngine::destruct(child); - break; - } - child = element->findFirstChild(XMLElement::Text); - if (child) { - text = child->getText(); - TelEngine::destruct(child); - } -} - -inline void addPaddedVal(String& buf, int val, const char* sep) -{ - if (val < 10) - buf << "0"; - buf << val << sep; -} - -// Encode EPOCH time given in seconds to a date/time profile as defined in -// XEP-0082 -void XMPPUtils::encodeDateTimeSec(String& buf, unsigned int timeSec, - unsigned int fractions) -{ - int y; - unsigned int m,d,hh,mm,ss; - if (!Time::toDateTime(timeSec,y,m,d,hh,mm,ss)) - return; - buf << y << "-"; - addPaddedVal(buf,m,"-"); - addPaddedVal(buf,d,"T"); - addPaddedVal(buf,hh,":"); - addPaddedVal(buf,mm,":"); - addPaddedVal(buf,ss,""); - if (fractions) - buf << "." << fractions; - buf << "Z"; -} - -// Decode a date/time profile as defined in XEP-0082 and -// XML Schema Part 2: Datatypes Second Edition to EPOCH time -unsigned int XMPPUtils::decodeDateTimeSec(const String& time, unsigned int* fractions) -{ - // XML Schema Part 2: Datatypes Second Edition - // (see http://www.w3.org/TR/xmlschema-2/#dateTime) - // Section 3.2.7: dateTime - // Format: [-]yyyy[y+]-mm-ddThh:mm:ss[.s+][Z|[+|-]hh:mm] - // NOTE: The document specify that yyyy may be negative and may have more then 4 digits: - // for now we only accept positive years greater then 1970 - - unsigned int ret = (unsigned int)-1; - unsigned int timeFractions = 0; - while (true) { - // Split date/time - int pos = time.find('T'); - if (pos == -1) - return (unsigned int)-1; - // Decode date - if (time.at(0) == '-') - break; - int year = 0; - unsigned int month = 0; - unsigned int day = 0; - String date = time.substr(0,pos); - ObjList* list = date.split('-'); - bool valid = (list->length() == 3 && list->count() == 3); - if (valid) { - year = (*list)[0]->toString().toInteger(-1,10); - month = (unsigned int)(*list)[1]->toString().toInteger(-1,10); - day = (unsigned int)(*list)[2]->toString().toInteger(-1,10); - valid = year >= 1970 && month && month <= 12 && day && day <= 31; - } - TelEngine::destruct(list); - if (valid) - DDebug(DebugAll, - "XMPPUtils::decodeDateTimeSec() decoded year=%d month=%u day=%u from '%s'", - year,month,day,time.c_str()); - else { - DDebug(DebugNote, - "XMPPUtils::decodeDateTimeSec() incorrect date=%s in '%s'", - date.c_str(),time.c_str()); - break; - } - // Decode Time - String t = time.substr(pos + 1,8); - if (t.length() != 8) - break; - unsigned int hh = 0; - unsigned int mm = 0; - unsigned int ss = 0; - int offsetSec = 0; - list = t.split(':'); - valid = (list->length() == 3 && list->count() == 3); - if (valid) { - hh = (unsigned int)(*list)[0]->toString().toInteger(-1,10); - mm = (unsigned int)(*list)[1]->toString().toInteger(-1,10); - ss = (unsigned int)(*list)[2]->toString().toInteger(-1,10); - valid = (hh <= 23 && mm <= 59 && ss <= 59) || - (hh == 24 && mm == 0 && ss == 0); - } - TelEngine::destruct(list); - if (!valid) { - DDebug(DebugNote, - "XMPPUtils::decodeDateTimeSec() incorrect time=%s in '%s'", - t.c_str(),time.c_str()); - break; - } -#ifdef DEBUG - else - Debug(DebugAll, - "XMPPUtils::decodeDateTimeSec() decoded hour=%u minute=%u sec=%u from '%s'", - hh,mm,ss,time.c_str()); -#endif - // Get the rest - unsigned int parsed = date.length() + t.length() + 1; - unsigned int len = time.length() - parsed; - const char* buf = time.c_str() + parsed; - if (len > 1) { - // Get time fractions - if (buf[0] == '.') { - unsigned int i = 1; - // FIXME: Trailing 0s are not allowed in fractions - for (; i < len && buf[i] >= '0' && buf[i] <= '9'; i++) - ; - String fr(buf + 1,i - 1); - if (i > 2) - timeFractions = (unsigned int)fr.toInteger(-1); - else - timeFractions = (unsigned int)-1; - if (timeFractions != (unsigned int)-1) - DDebug(DebugAll, - "XMPPUtils::decodeDateTimeSec() decoded fractions=%u from '%s'", - timeFractions,time.c_str()); - else { - DDebug(DebugNote, - "XMPPUtils::decodeDateTimeSec() incorrect fractions=%s in '%s'", - fr.c_str(),time.c_str()); - break; - } - len -= i; - buf += i; - } - // Get offset - if (len > 1) { - int sign = 1; - if (*buf == '-' || *buf == '+') { - if (*buf == '-') - sign = -1; - buf++; - len--; - } - String offs(buf,5); - // We should have at least 5 bytes: hh:ss - if (len < 5 || buf[2] != ':') { - DDebug(DebugNote, - "XMPPUtils::decodeDateTimeSec() incorrect time offset=%s in '%s'", - offs.c_str(),time.c_str()); - break; - } - unsigned int hhOffs = (unsigned int)offs.substr(0,2).toInteger(-1,10); - unsigned int mmOffs = (unsigned int)offs.substr(3,2).toInteger(-1,10); - // XML Schema Part 2 3.2.7.3: the hour may be 0..13. It can be 14 if minute is 0 - if (mmOffs > 59 || (hhOffs > 13 && !mmOffs)) { - DDebug(DebugNote, - "XMPPUtils::decodeDateTimeSec() incorrect time offset values hour=%u minute=%u in '%s'", - hhOffs,mmOffs,time.c_str()); - break; - } - DDebug(DebugAll, - "XMPPUtils::decodeDateTimeSec() decoded time offset '%c' hour=%u minute=%u from '%s'", - sign > 0 ? '+' : '-',hhOffs,mmOffs,time.c_str()); - offsetSec = sign * (hhOffs * 3600 + mmOffs * 60); - buf += 5; - len -= 5; - } - } - // Check termination markup - if (len && (len != 1 || *buf != 'Z')) { - DDebug(DebugNote, - "XMPPUtils::decodeDateTimeSec() '%s' is incorrectly terminated '%s'", - time.c_str(),buf); - break; - } - ret = Time::toEpoch(year,month,day,hh,mm,ss,offsetSec); -#ifdef DEBUG - if (ret == (unsigned int)-1) - Debug(DebugNote, - "XMPPUtils::decodeDateTimeSec() failed to convert '%s'", - time.c_str()); -#endif - break; - } - - if (ret != (unsigned int)-1) { - if (fractions) - *fractions = timeFractions; - } - return ret; -} - -// Check if an element or attribute name restricts value output -static const char* check(const String& name, const char* ok) -{ -#define RESTRICT_LEN 2 - static String restrict[RESTRICT_LEN] = {"auth", "password"}; - static const char* pwd = "********"; - for (unsigned int i = 0; i < RESTRICT_LEN; i++) - if (restrict[i] == name) - return pwd; - return ok; -#undef RESTRICT_LEN -} - -void XMPPUtils::print(String& xmlStr, XMLElement& element, const char* indent) -{ -#define STARTLINE(indent) "\r\n" << indent - const char* enclose = "-----"; - bool hasAttr = (0 != element.firstAttribute()); - bool hasChild = element.hasChild(0); - const char* txt = element.getText(); - bool root = false; - if (!(indent && *indent)) { - indent = ""; - root = true; - } - if (root) - xmlStr << STARTLINE(indent) << enclose; - // Name - if (!(hasAttr || hasChild || txt)) { - // indent - xmlStr << STARTLINE(indent) << '<' << element.name(); - if ((element.name())[0] != '/') - xmlStr << '/'; - xmlStr << '>'; - if (root) - xmlStr << STARTLINE(indent) << enclose; - return; - } - // or '; - String sindent = indent; - sindent << " "; - // Attributes - const TiXmlAttribute* attr = element.firstAttribute(); - for (; attr; attr = attr->Next()) - xmlStr << STARTLINE(sindent) << attr->Name() << "=\"" - << check(attr->Name(),attr->Value()) << '"'; - // Text. Filter some known elements to avoid output of passwords - if (txt) - xmlStr << STARTLINE(sindent) << check(element.name(),txt); - // Children - XMLElement* child = element.findFirstChild(); - String si = sindent; - for (; child; child = element.findNextChild(child)) - print(xmlStr,*child,si); - // End tag - if (hasChild || txt) - xmlStr << STARTLINE(indent) << "'; - else - xmlStr << STARTLINE(indent) << "/>"; - if (root) - xmlStr << STARTLINE(indent) << enclose; -#undef STARTLINE -} - -bool XMPPUtils::split(NamedList& dest, const char* src, const char sep, - bool nameFirst) -{ - if (!src) - return false; - unsigned int index = 1; - String s = src; - ObjList* obj = s.split(sep,false); - for (ObjList* o = obj->skipNull(); o; o = o->skipNext(), index++) { - String* tmp = static_cast(o->get()); - if (nameFirst) - dest.addParam(*tmp,String(index)); - else - dest.addParam(String(index),*tmp); - } - TelEngine::destruct(obj); - return true; -} - -// Decode a comma separated list of flags and put them into an integr mask -int XMPPUtils::decodeFlags(const String& src, const TokenDict* dict) -{ - if (!dict) - return 0; - int mask = 0; - ObjList* obj = src.split(',',false); - for (ObjList* o = obj->skipNull(); o; o = o->skipNext()) - mask |= lookup(static_cast(o->get())->c_str(),dict); - TelEngine::destruct(obj); - return mask; -} - -// Encode a mask of flags to a comma separated list. -void XMPPUtils::buildFlags(String& dest, int src, const TokenDict* dict) -{ - if (!dict) - return; - for (; dict->token; dict++) - if (0 != (src & dict->value)) - dest.append(dict->token,","); -} - -// Add child elements from a list to a destination element -bool XMPPUtils::addChidren(XMLElement* dest, ObjList& list) -{ - if (!dest) - return false; - ObjList* o = list.skipNull(); - bool added = (0 != o); - for (; o; o = o->skipNext()) { - XMLElement* xml = static_cast(o->get()); - dest->addChild(new XMLElement(*xml)); - } - return added; -} - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/xmpputils.h b/libs/yjingle/xmpputils.h deleted file mode 100644 index fff68d69..00000000 --- a/libs/yjingle/xmpputils.h +++ /dev/null @@ -1,1220 +0,0 @@ -/** - * xmpputils.h - * Yet Another Jabber Component Protocol Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#ifndef __XMPPUTILS_H -#define __XMPPUTILS_H - -#include -#include - -#ifdef _WINDOWS - -#ifdef LIBYJINGLE_EXPORTS -#define YJINGLE_API __declspec(dllexport) -#else -#ifndef LIBYJINGLE_STATIC -#define YJINGLE_API __declspec(dllimport) -#endif -#endif - -#endif /* _WINDOWS */ - -#ifndef YJINGLE_API -#define YJINGLE_API -#endif - -/** - * Holds all Telephony Engine related classes. - */ -namespace TelEngine { - -class XMPPServerInfo; // Server info class -class XMPPNamespace; // XMPP namespaces -class XMPPError; // XMPP errors -class JabberID; // A Jabber ID (JID) -class JIDIdentity; // A JID's identity -class JIDFeature; // A JID's feature -class JIDFeatureSasl; // A JID's SASL feature -class JIDFeatureList; // Feature list -class XMPPUtils; // Utilities -class XMPPDirVal; // Direction flags - -/** - * This class holds informations about a server - * @short Server info class - */ -class YJINGLE_API XMPPServerInfo : public RefObject -{ -public: - /** - * Server flags - */ - enum ServerFlag { - NoAutoRestart = 0x0001, // Don't auto restart streams when down - KeepRoster = 0x0002, // Tell the presence service to keep the roster for this server - TlsRequired = 0x0004, // The server always requires connection encryption - OldStyleAuth = 0x0008, // The server doesn't support RFC 3920 TLS/SASL ... - AllowPlainAuth = 0x0020, // Allow plain password authentication - AllowUnsafeSetup = 0x0040, // Allow user account setup on unenchrypted streams - }; - - /** - * Constructor. Construct a full server info object - * @param name Server domain name - * @param address IP address - * @param port IP port - * @param password Component only: Password used for authentication - * @param identity Component only: The stream identity used when connecting - * @param fullidentity Component only: The user identity - * @param flags Server flags - */ - inline XMPPServerInfo(const char* name, const char* address, int port, - const char* password, const char* identity, const char* fullidentity, - int flags) - : m_name(name), m_address(address), m_port(port), m_password(password), - m_identity(identity), m_fullIdentity(fullidentity), m_flags(flags) - {} - - /** - * Constructor. Construct a partial server info object - * @param name Server domain name - * @param port IP port - */ - inline XMPPServerInfo(const char* name, int port) - : m_name(name), m_port(port) - {} - - /** - * Get the server's address - * @return The server's address - */ - inline const String& address() const - { return m_address; } - - /** - * Get the server's domain name - * @return The server's domain name - */ - inline const String& name() const - { return m_name; } - - /** - * Get the server's port used to connect to - * @return The server's port used to connect to - */ - inline const int port() const - { return m_port; } - - /** - * Get the server's port used to connect to - * @return The server's port used to connect to - */ - inline const String& password() const - { return m_password; } - - /** - * Get the server's identity - * @return The server's identity - */ - inline const String& identity() const - { return m_identity; } - - /** - * Get the server's full identity - * @return The server's full identity - */ - inline const String& fullIdentity() const - { return m_fullIdentity; } - - /** - * Check if a given flag (or mask) is set - * @return True if the flag is set - */ - inline bool flag(int mask) const - { return 0 != (m_flags & mask); } - - /** - * Flag names dictionary - */ - static TokenDict s_flagName[]; - -private: - String m_name; // Domain name - String m_address; // IP address - int m_port; // Port - String m_password; // Authentication data - String m_identity; // Identity. Used for Jabber Component protocol - String m_fullIdentity; // Full identity for this server - int m_flags; // Server flags -}; - - -/** - * This class holds the XMPP/JabberComponent/Jingle namespace enumerations and the associated strings - * @short XMPP namespaces - */ -class YJINGLE_API XMPPNamespace -{ -public: - enum Type { - Stream = 1, // http://etherx.jabber.org/streams - Client, // jabber:client - Server, // jabber:server - ComponentAccept, // jabber:component:accept - ComponentConnect, // jabber:component:connect - StreamError, // urn:ietf:params:xml:ns:xmpp-streams - StanzaError, // urn:ietf:params:xml:ns:xmpp-stanzas - Register, // http://jabber.org/features/iq-register - IqRegister, // jabber:iq:register - IqPrivate, // jabber:iq:private - IqAuth, // jabber:iq:auth - IqAuthFeature, // http://jabber.org/features/iq-auth - Starttls, // urn:ietf:params:xml:ns:xmpp-tls - Sasl, // urn:ietf:params:xml:ns:xmpp-sasl - Session, // urn:ietf:params:xml:ns:xmpp-session - Bind, // urn:ietf:params:xml:ns:xmpp-bind - Roster, // jabber:iq:roster - DynamicRoster, // jabber:iq:roster-dynamic - DiscoInfo, // http://jabber.org/protocol/disco#info - DiscoItems, // http://jabber.org/protocol/disco#items - VCard, // vcard-temp - SIProfileFileTransfer, // http://jabber.org/protocol/si/profile/file-transfer - ByteStreams, // http://jabber.org/protocol/bytestreams - Jingle, // xmlns='urn:xmpp:jingle:0 - JingleError, // urn:xmpp:jingle:errors:0 - JingleAppsRtp, // urn:xmpp:jingle:apps:rtp:0 - JingleAppsRtpInfo, // urn:xmpp:jingle:apps:rtp:info:0 - JingleAppsRtpAudio, // urn:xmpp:jingle:apps:rtp:audio - JingleAppsFileTransfer, // urn:xmpp:jingle:apps:file-transfer:0 - JingleTransportIceUdp, // urn:xmpp:jingle:transports:ice-udp:0 - JingleTransportRawUdp, // urn:xmpp:jingle:transports:raw-udp:0 - JingleTransportRawUdpInfo, // urn:xmpp:jingle:transports:raw-udp:info:0 - JingleTransportByteStreams, // urn:xmpp:jingle:transports:bytestreams:0 - JingleTransfer, // urn:xmpp:jingle:transfer:0 - Dtmf, // urn:xmpp:jingle:dtmf:0 - JingleSession, // http://www.google.com/session - JingleAudio, // http://www.google.com/session/phone - JingleTransport, // http://www.google.com/transport/p2p - JingleRtpInfoOld, // urn:xmpp:jingle:apps:rtp:info - DtmfOld, // http://jabber.org/protocol/jingle/info/dtmf - Command, // http://jabber.org/protocol/command - CapVoiceV1, // http://www.google.com/xmpp/protocol/voice/v1 - Count, - }; - - /** - * Get the string representation of a namespace value - */ - inline const char* operator[](Type index) - { return lookup(index,s_value); } - - /** - * Check if a text is a known namespace - */ - static bool isText(Type index, const char* txt); - - /** - * Get the type associated with a given namespace text - */ - static inline Type type(const char* txt) { - int tmp = lookup(txt,s_value,Count); - return tmp ? (Type)tmp : Count; - } - -private: - static TokenDict s_value[]; // Namespace list -}; - - -/** - * This class holds the XMPP error type, error enumerations and associated strings - * @short XMPP errors. - */ -class YJINGLE_API XMPPError -{ -public: - /** - * Error condition enumeration - */ - enum Type { - NoError = 0, - // Stream errors - BadFormat, // bad-format - BadNamespace, // bad-namespace-prefix - ConnTimeout, // connection-timeout - HostGone, // host-gone - HostUnknown, // host-unknown - BadAddressing, // improper-addressing - Internal, // internal-server-error - InvalidFrom, // invalid-from - InvalidId, // invalid-id - InvalidNamespace, // invalid-namespace - InvalidXml, // invalid-xml - NotAuth, // not-authorized - Policy, // policy-violation - RemoteConn, // remote-connection-failed - ResConstraint, // resource-constraint - RestrictedXml, // restricted-xml - SeeOther, // see-other-host - Shutdown, // system-shutdown - UndefinedCondition, // undefined-condition - UnsupportedEnc, // unsupported-encoding - UnsupportedStanza, // unsupported-stanza-type - UnsupportedVersion, // unsupported-version - Xml, // xml-not-well-formed - // Auth failures - Aborted, // aborted - IncorrectEnc, // incorrect-encoding - InvalidAuth, // invalid-authzid - InvalidMechanism, // invalid-mechanism - MechanismTooWeak, // mechanism-too-weak - NotAuthorized, // not-authorized - TempAuthFailure, // temporary-auth-failure - // Stanza errors - SBadRequest, // bad-request - SConflict, // conflict - SFeatureNotImpl, // feature-not-implemented - SForbidden, // forbidden - SGone, // gone - SInternal, // internal-server-error - SItemNotFound, // item-not-found - SBadJid, // jid-malformed - SNotAcceptable, // not-acceptable - SNotAllowed, // not-allowed - SPayment, // payment-required - SUnavailable, // recipient-unavailable - SRedirect, // redirect - SReg, // registration-required - SNoRemote, // remote-server-not-found - SRemoteTimeout, // remote-server-timeout - SResource, // resource-constraint - SServiceUnavailable, // service-unavailable - SSubscription, // subscription-required - SUndefinedCondition, // undefined-condition - SRequest, // unexpected-request - // Misc - DtmfNoMethod, // unsupported-dtmf-method - ItemNotFound, // item-not-found - Count, - }; - - /** - * Error type enumeration - */ - enum ErrorType { - TypeCancel = 1000, // do not retry (the error is unrecoverable) - TypeContinue, // proceed (the condition was only a warning) - TypeModify, // retry after changing the data sent - TypeAuth, // retry after providing credentials - TypeWait, // retry after waiting (the error is temporary) - TypeCount, - }; - - /** - * Get the text representation of a given error value - */ - inline const char* operator[](int index) - { return lookup(index,s_value); } - - /** - * Check if a given text is a valid error - */ - static bool isText(int index, const char* txt); - - /** - * Get the type associated with a given error text - */ - static inline int type(const char* txt) - { return lookup(txt,s_value,Count); } - -private: - static TokenDict s_value[]; // Error list -}; - - -/** - * This class holds a Jabber ID in form "node@domain/resource" or "node@domain" - * @short A Jabber ID - */ -class YJINGLE_API JabberID : public String -{ -public: - /** - * Constructor - */ - inline JabberID() {} - - /** - * Constructor. Constructs a JID from a given string - * @param jid The JID string - */ - inline JabberID(const char* jid) - { set(jid); } - - /** - * Constructor. Constructs a JID from user, domain, resource - * @param node The node - * @param domain The domain - * @param resource The resource - */ - JabberID(const char* node, const char* domain, const char* resource = 0) - { set(node,domain,resource); } - - /** - * Get the node part of the JID - * @return The node part of the JID - */ - inline const String& node() const - { return m_node; } - - /** - * Get the bare JID: "node@domain" - * @return The bare JID - */ - inline const String& bare() const - { return m_bare; } - - /** - * Get the domain part of the JID - * @return The domain part of the JID - */ - inline const String& domain() const - { return m_domain; } - - /** - * Set the domain part of the JID. - * @param d The new domain part of the JID. - */ - inline void domain(const char* d) - { set(m_node.c_str(),d,m_resource.c_str()); } - - /** - * Get the resource part of the JID - * @return The resource part of the JID - */ - inline const String& resource() const - { return m_resource; } - - /** - * Check if this is a full JID - * @return True if this is a full JID - */ - inline bool isFull() const - { return m_node && m_domain && m_resource; } - - /** - * Try to match another JID to this one. If src has a resource compare it too - * (case sensitive). Otherwise compare just the bare JID (case insensitive) - * @param src The JID to match - * @return True if matched - */ - inline bool match(const JabberID& src) const - { return (src.resource().null() || (resource() == src.resource())) && (bare() &= src.bare()); } - - /** - * Equality operator. Do a case senitive resource comparison and a case insensitive bare jid comparison - * @param src The JID to compare with - * @return True if equal - */ - inline bool operator==(const JabberID& src) const - { return (resource() == src.resource()) && (bare() &= src.bare()); } - - /** - * Equality operator. Build a temporary JID and compare with it - * @param src The string to compare with - * @return True if equal - */ - inline bool operator==(const String& src) const - { JabberID tmp(src); return operator==(tmp); } - - /** - * Inequality operator - * @param src The JID to compare with - * @return True if not equal - */ - inline bool operator!=(const JabberID& src) const - { return !operator==(src); } - - /** - * Inequality operator - * @param src The string to compare with - * @return True if not equal - */ - inline bool operator!=(const String& src) const - { return !operator==(src); } - - /** - * Set the resource part of the JID - * @param res The new resource part of the JID - */ - inline void resource(const char* res) - { set(m_node.c_str(),m_domain.c_str(),res); } - - /** - * Set the data - * @param jid The JID string to assign - */ - void set(const char* jid); - - /** - * Set the data - * @param node The node - * @param domain The domain - * @param resource The resource - */ - void set(const char* node, const char* domain, const char* resource = 0); - - /** - * Check if the given string contains valid characters - * @param value The string to check - * @return True if value is valid or 0. False if value is a non empty invalid string - */ - static bool valid(const String& value); - - /** - * Keep the regexp used to check the validity of a string - */ - static Regexp s_regExpValid; - -private: - void parse(); // Parse the string. Set the data - - String m_node; // The node part - String m_domain; // The domain part - String m_resource; // The resource part - String m_bare; // The bare JID: node@domain -}; - - -/** - * This class holds an identity for a JID - * @short A JID identity - */ -class YJINGLE_API JIDIdentity : public RefObject -{ -public: - /** - * JID category enumeration - */ - enum Category { - Account, // account - Client, // client - Component, // component - Gateway, // gateway - CategoryUnknown - }; - - /** - * JID type enumeration - */ - enum Type { - AccountRegistered, // registered - ClientPhone, // phone - ComponentGeneric, // generic - ComponentPresence, // presence - GatewayGeneric, // generic - TypeUnknown - }; - - /** - * Constructor. Build a JID identity - * @param c The JID's category - * @param t The JID's type - * @param name The name of this identity - */ - inline JIDIdentity(Category c, Type t, const char* name = 0) - : m_name(name), m_category(c), m_type(t) - {} - - /** - * Destructor - */ - virtual ~JIDIdentity() - {} - - /** - * Build an XML element from this identity - * @return A valid XML element - */ - XMLElement* toXML(); - - /** - * Build this identity from an XML element - * @return True on succes - */ - bool fromXML(const XMLElement* element); - - /** - * Get a string representation of this object - * @return This object's name - */ - virtual const String& toString() const - { return m_name; } - - /** - * Get a pointer from this object - * @param name The requested pointer's name - * @return Requested pointer or 0 - */ - virtual void* getObject(const String& name) const { - if (name == "JIDIdentity") - return (void*)this; - return RefObject::getObject(name); - } - - /** - * Set the name of this identity - * @param name New identity name - */ - inline void setName(const char* name) - { if (name) m_name = name; } - - /** - * Lookup for a text associated with a given category - * @return The category's name - */ - static inline const char* categoryText(Category c) - { return lookup(c,s_category); } - - /** - * Lookup for a value associated with a given category name - * @return The category's value - */ - static inline Category categoryValue(const char* c) - { return (Category)lookup(c,s_category,CategoryUnknown); } - - /** - * Lookup for a text associated with a given category type - * @return The category's type name - */ - static inline const char* typeText(Type t) - { return lookup(t,s_type); } - - /** - * Lookup for a value associated with a given category type - * @return The category's type value - */ - static inline Type typeValue(const char* t) - { return (Type)lookup(t,s_category,TypeUnknown); } - -private: - static TokenDict s_category[]; - static TokenDict s_type[]; - - String m_name; - Category m_category; // Category - Type m_type; // Type -}; - - -/** - * This class holds a JID feature - * @short A JID feature - */ -class YJINGLE_API JIDFeature : public RefObject -{ -public: - /** - * Constructor - * @param feature The feature to add - * @param required True if this feature is required - */ - inline JIDFeature(XMPPNamespace::Type feature, bool required = false) - : m_feature(feature), - m_required(required) - {} - - /** - * Destructor - */ - virtual ~JIDFeature() - {} - - /** - * Check if this feature is a required one - * @return True if this feature is a required one - */ - inline bool required() const - { return m_required; } - - /** - * XMPPNamespace::Type conversion operator - */ - inline operator XMPPNamespace::Type() - { return m_feature; } - -private: - XMPPNamespace::Type m_feature; // The feature - bool m_required; // Required flag -}; - - -/** - * This class holds a JID SASL feature (authentication methods) - * @short A JID's SASL feature - */ -class YJINGLE_API JIDFeatureSasl : public JIDFeature -{ -public: - /** - * Mechanisms used to authenticate a stream - */ - enum Mechanism { - MechNone = 0x00, // No authentication mechanism - MechMD5 = 0x01, // MD5 digest - MechSHA1 = 0x02, // SHA1 digest - MechPlain = 0x04, // Plain text password - }; - - /** - * Constructor - * @param mech Authentication mechanisms used by the JID - * @param required Required flag - */ - inline JIDFeatureSasl(int mech, bool required = false) - : JIDFeature(XMPPNamespace::Sasl,required), - m_mechanism(mech) - {} - - /** - * Get the authentication mechanisms used by the JID - * @return The authentication mechanisms used by the JID - */ - inline int mechanism() const - { return m_mechanism; } - - /** - * Check if a given mechanism is allowed - * @return True if the given mechanism is allowed - */ - inline bool mechanism(Mechanism mech) const - { return 0 != (m_mechanism & mech); } - - /** - * XMPPNamespace::Type conversion operator - */ - inline operator XMPPNamespace::Type() - { return JIDFeature::operator XMPPNamespace::Type(); } - - /** - * Authentication mechanism names - */ - static TokenDict s_authMech[]; - -private: - int m_mechanism; // Authentication mechanisms -}; - - -/** - * This class holds a list of JID features - * @short JID feature list - */ -class YJINGLE_API JIDFeatureList -{ -public: - /** - * Add a feature to the list - * @param feature The feature to add - * @param required True if this feature is required - * @return False if the given feature already exists - */ - inline bool add(XMPPNamespace::Type feature, bool required = false) { - if (get(feature)) - return false; - m_features.append(new JIDFeature(feature,required)); - return true; - } - - /** - * Add a feature to the list. Destroy the received parameter if already in the list - * @param feature The feature to add - * @return False if the given feature already exists - */ - inline bool add(JIDFeature* feature) { - if (!feature || get(*feature)) { - TelEngine::destruct(feature); - return false; - } - m_features.append(feature); - return true; - } - - /** - * Remove a feature from the list - * @param feature The feature to remove - */ - inline void remove(XMPPNamespace::Type feature) - { m_features.remove(get(feature),true); } - - /** - * Get a feature from the list - * @param feature The feature to get - * @return Pointer to the feature or 0 if it doesn't exists - */ - JIDFeature* get(XMPPNamespace::Type feature); - - /** - * Add 'feature' children to the given element - * @param element The target XMLElement - * @return The given element - */ - XMLElement* addTo(XMLElement* element); - - /** - * Update the list from 'feature' children of the given element - * @param element The source XMLElement - * @param reset True to clear the list before updating - * @return The given element - */ - void fromXml(XMLElement* element, bool reset = true); - - /** - * Clear the feature list - */ - inline void clear() - { m_features.clear(); } - -private: - ObjList m_features; // The features -}; - - -/** - * This class is a general XMPP utilities - * @short General XMPP utilities - */ -class YJINGLE_API XMPPUtils -{ -public: - /** - * Iq type enumeration - */ - enum IqType { - IqSet, // set - IqGet, // get - IqResult, // result - IqError, // error - IqCount, - }; - - /** - * Command action enumeration - */ - enum CommandAction { - CommExecute, - CommCancel, - CommPrev, - CommNext, - CommComplete, - }; - - /** - * Command status enumeration - */ - enum CommandStatus { - CommExecuting, - CommCompleted, - CommCancelled, - }; - - /** - * Create an XML element with an 'xmlns' attribute - * @param name Element's name - * @param ns 'xmlns' attribute - * @param text Optional text for the element - * @return A valid XMLElement pointer - */ - static XMLElement* createElement(const char* name, XMPPNamespace::Type ns, - const char* text = 0); - - /** - * Create an XML element with an 'xmlns' attribute - * @param type Element's type - * @param ns 'xmlns' attribute - * @param text Optional text for the element - * @return A valid XMLElement pointer - */ - static XMLElement* createElement(XMLElement::Type type, XMPPNamespace::Type ns, - const char* text = 0); - - /** - * Create an 'iq' element - * @param type Iq type as enumeration - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @return A valid XMLElement pointer - */ - static XMLElement* createIq(IqType type, const char* from, - const char* to, const char* id); - - /** - * Create an 'iq' element with a 'bind' child containing the resources - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @param resources The resources to bind (strings) - * @return A valid XMLElement pointer - */ - static XMLElement* createIqBind(const char* from, - const char* to, const char* id, const ObjList& resources); - - /** - * Create an 'iq' element with a 'vcard' child - * @param get True to set the iq's type to 'get', false to set it to 'set' - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @return A valid XMLElement pointer - */ - static XMLElement* createVCard(bool get, const char* from, const char* to, const char* id); - - /** - * Create a 'command' element - * @param action The command action - * @param node The command - * @param sessionId Optional session ID for the command - * @return A valid XMLElement pointer - */ - static XMLElement* createCommand(CommandAction action, const char* node, - const char* sessionId = 0); - - /** - * Create an 'identity' element - * @param category The 'category' attribute - * @param type The 'type' attribute - * @param name The 'name' attribute - * @return A valid XMLElement pointer - */ - static XMLElement* createIdentity(const char* category, - const char* type, const char* name); - - /** - * Create an 'iq' of type 'get' element with a 'query' child - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @param info True to create a query info request. False to create a query items request - * @return A valid XMLElement pointer - */ - static XMLElement* createIqDisco(const char* from, const char* to, - const char* id, bool info = true); - - /** - * Create an 'iq' of type 'result' element with a 'query' child in response to - * a disco info request - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @param features Features to be added to response - * @param identity The identity of the entity sending the response - * @return A valid XMLElement pointer - */ - static XMLElement* createDiscoInfoRes(const char* from, const char* to, - const char* id, JIDFeatureList* features, JIDIdentity* identity); - - /** - * Create a 'error' element - * @param type Error type - * @param error The error - * @param text Optional text to add to the error element - * @return A valid XMLElement pointer - */ - static XMLElement* createError(XMPPError::ErrorType type, - XMPPError::Type error, const char* text = 0); - - /** - * Create an error from a received element. Consume the received element - * Reverse 'to' and 'from' attributes - * @param xml Received element - * @param type Error type - * @param error The error - * @param text Optional text to add to the error element - * @return A valid XMLElement pointer or 0 if xml is 0 - */ - static XMLElement* createError(XMLElement* xml, XMPPError::ErrorType type, - XMPPError::Type error, const char* text = 0); - - /** - * Create a 'stream:error' element - * @param error The XMPP defined condition - * @param text Optional text to add to the error - * @return A valid XMLElement pointer - */ - static XMLElement* createStreamError(XMPPError::Type error, - const char* text = 0); - - /** - * Build a register query element - * @param type Iq type as enumeration - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @param child1 Optional child of query element - * @param child2 Optional child of query element - * @param child3 Optional child of query element - * @return Valid XMLElement pointer - */ - static XMLElement* createRegisterQuery(IqType type, const char* from, - const char* to, const char* id, - XMLElement* child1 = 0, XMLElement* child2 = 0, XMLElement* child3 = 0); - - /** - * Build an register query element used to create/set username/password - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @param username The username - * @param password The password - * @return Valid XMLElement pointer - */ - static inline XMLElement* createRegisterQuery(const char* from, - const char* to, const char* id, - const char* username, const char* password) { - return createRegisterQuery(XMPPUtils::IqSet,from,to,id, - new XMLElement(XMLElement::Username,0,username), - new XMLElement(XMLElement::Password,0,password)); - } - - /** - * Check if the given element has an attribute 'xmlns' equal to a given value - * @param element Element to check - * @param ns Namespace value to check - * @return True if the given element has the requested namespace - */ - static bool hasXmlns(XMLElement& element, XMPPNamespace::Type ns); - - /** - * Decode a received stream error or stanza error - * @param element The received element - * @param error The error condition - * @param text The stanza's error or error text - */ - static void decodeError(XMLElement* element, String& error, String& text); - - /** - * Encode EPOCH time given in seconds to a date/time profile as defined in - * XEP-0082 and XML Schema Part 2: Datatypes Second Edition - * @param buf Destination string - * @param timeSec The time to encode (in seconds) - * @param fractions Optional second fractions - */ - static void encodeDateTimeSec(String& buf, unsigned int timeSec, - unsigned int fractions = 0); - - /** - * Decode a date/time profile as defined in XEP-0082 - * and XML Schema Part 2: Datatypes Second Edition to EPOCH time - * @param time The date/time string - * @param fractions Pointer to integer to be filled with second fractions, if present - * @return The decoded time in seconds, -1 on error - */ - static unsigned int decodeDateTimeSec(const String& time, unsigned int* fractions = 0); - - /** - * Print an XMLElement to a string - * @param xmlStr The destination string - * @param element The element to print - * @param indent The indent. 0 if it is the root element - */ - static void print(String& xmlStr, XMLElement& element, const char* indent = 0); - - /** - * Split a string at a delimiter character and fills a named list with its parts - * Skip empty parts - * @param dest The destination NamedList - * @param src Pointer to the string - * @param sep The delimiter - * @param nameFirst True to add the parts as name and index as value. - * False to do the other way - */ - static bool split(NamedList& dest, const char* src, const char sep, - bool nameFirst); - - /** - * Decode a comma separated list of flags and put them into an integer mask - * @param src Source string - * @param dict Dictionary containing flag names and values - * @return The mask of found flags - */ - static int decodeFlags(const String& src, const TokenDict* dict); - - /** - * Encode a mask of flags to a comma separated list of names - * @param dest Destination string - * @param src Source mask - * @param dict Dictionary containing flag names and values - */ - static void buildFlags(String& dest, int src, const TokenDict* dict); - - /** - * Add child elements from a list to a destination element - * @param dest Destination XMLElement - * @param list A list containing XML elements - * @return True if at least one child was added - */ - static bool addChidren(XMLElement* dest, ObjList& list); - - /** - * Get the type of an 'iq' stanza as enumeration - * @param text The text to check - * @return Iq type as enumeration - */ - static inline IqType iqType(const char* text) - { return (IqType)lookup(text,s_iq,IqCount); } - - /** - * Keep the types of 'iq' stanzas - */ - static TokenDict s_iq[]; - - /** - * Keep the command actions - */ - static TokenDict s_commandAction[]; - - /** - * Keep the command status - */ - static TokenDict s_commandStatus[]; -}; - -/** - * This class holds a 4-state direction value (such as subscription states) - * @short Direction flags - */ -class YJINGLE_API XMPPDirVal -{ -public: - enum Direction { - None = 0, - To = 1, - From = 2, - Both = 3, - }; - - /** - * Constructor - * @param flags Flag(s) to set - */ - inline XMPPDirVal(int flags = None) - : m_value(flags) - {} - - /** - * Constructor - * @param name The name of the flag used to initialize this object - */ - inline XMPPDirVal(const char* name) - : m_value(lookup(name,None)) - {} - - /** - * Replace all flags - * @param flag The new value of the flags - */ - inline void replace(int flag) - { m_value = flag; } - - /** - * Replace all flags from a value's name - * @param name The name of the flag used to replace this value - */ - inline void replace(const char* name) - { m_value = lookup(name,None); } - - /** - * Set one or more flags - * @param flag Flag(s) to set - */ - inline void set(int flag) - { m_value |= flag; } - - /** - * Reset one or more flags - * @param flag Flag(s) to reset - */ - inline void reset(int flag) - { m_value &= ~flag; } - - /** - * Check if a given bit mask is set - * @param mask Bit mask to check - * @return True if the given bit mask is set - */ - inline bool flag(int mask) const - { return (m_value & mask) != 0; } - - /** - * Check if the 'To' flag is set - * @return True if the 'To' flag is set - */ - inline bool to() const - { return flag(To); } - - /** - * Check if the 'From' flag is set - * @return True if the 'From' flag is set - */ - inline bool from() const - { return flag(From); } - - /** - * Cast operator - */ - inline operator int() - { return m_value; } - - /** - * Get the name of a flag - * @param flag The flag - * @param defVal Value to return if not found - * @return The name of the requested flag - */ - static inline const char* lookup(int flag, const char* defVal = "") - { return TelEngine::lookup(flag,s_names,defVal); } - - /** - * Get the value associated with a flag name - * @param name The flag name - * @param defVal Value to return if not found - * @return The value of the requested flag name - */ - static inline int lookup(const char* name, int defVal = None) - { return TelEngine::lookup(name,s_names,defVal); } - - /** - * Keep the flag names - */ - static TokenDict s_names[]; - -private: - int m_value; // The value -}; - -}; - -#endif /* __XMPPUTILS_H */ - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yjingle/yatejabber.h b/libs/yjingle/yatejabber.h deleted file mode 100644 index 1c4191a0..00000000 --- a/libs/yjingle/yatejabber.h +++ /dev/null @@ -1,2663 +0,0 @@ -/** - * yatejabber.h - * Yet Another Jabber Component Protocol Stack - * This file is part of the YATE Project http://YATE.null.ro - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#ifndef __YATEJABBER_H -#define __YATEJABBER_H - -#include -#include - -/** - * Holds all Telephony Engine related classes. - */ -namespace TelEngine { - -class JBEvent; // A Jabber event -class JBStream; // A Jabber stream -class JBComponentStream; // A Jabber Component stream -class JBClientStream; // A Jabber client to server stream -class JBThread; // Base class for private threads -class JBThreadList; // A list of threads -class JBEngine; // A Jabber engine -class JBService; // A Jabber service -class JBPresence; // A Jabber presence service -class JIDResource; // A JID resource -class JIDResourceList; // Resource list -class XMPPUser; // An XMPP user (JID, resources, capabilities etc) -class XMPPUserRoster; // An XMPP user's roster - -/** - * This class holds a Jabber stream event. Stream events are raised by streams - * and sent by the engine to the proper service - * @short A Jabber stream event - */ -class YJINGLE_API JBEvent : public RefObject -{ - friend class JBStream; - friend class JBClientStream; -public: - /** - * Event type enumeration - */ - enum Type { - // Stream events - Terminated = 1, // Stream terminated. Try to connect - Destroy = 2, // Stream is destroying - Running = 3, // Stream is running (stable state: can send/recv stanzas) - // Result events - WriteFail = 10, // Write failed. m_element is the element, m_id is the id set by the sender - // Stanza events: m_element is always valid - Presence = 20, // m_element is a 'presence' stanza - Message = 30, // m_element is a 'message' stanza - Iq = 50, // m_element is an 'iq' set/get, m_child is it's first child - IqError = 51, // m_element is an 'iq' error, m_child is the 'iq' child if any - IqResult = 52, // m_element is an 'iq' result, m_child is it's first child if any - // Disco: m_child is a 'query' element qualified by DiscoInfo/DiscoItems namespaces - IqDiscoInfoGet = 60, - IqDiscoInfoSet = 61, - IqDiscoInfoRes = 62, - IqDiscoInfoErr = 63, - IqDiscoItemsGet = 64, - IqDiscoItemsSet = 65, - IqDiscoItemsRes = 66, - IqDiscoItemsErr = 67, - // Command: m_child is a 'command' element qualified by Command namespace - IqCommandGet = 70, - IqCommandSet = 71, - IqCommandRes = 72, - IqCommandErr = 73, - // Jingle: m_child is a 'jingle' element qualified by Jingle namespace - IqJingleGet = 80, - IqJingleSet = 81, - IqJingleRes = 82, - IqJingleErr = 83, - // Roster: m_child is a 'query' element qualified by Roster namespace - IqRosterSet = 91, - IqRosterRes = 92, - IqRosterErr = 93, - // Roster update (set or result) received by client streams: m_child is a 'query' element - // qualified by Roster namespace - IqClientRosterUpdate = 150, - // Invalid - Unhandled = 200, // m_element is an unhandled element - Invalid = 500, // m_element is 0 - }; - - /** - * Constructor. Constructs an event from a stream - * @param type Type of this event - * @param stream The stream that generated the event - * @param element Element that generated the event - * @param child Optional type depending element's child - */ - JBEvent(Type type, JBStream* stream, XMLElement* element, XMLElement* child = 0); - - /** - * Constructor. Constructs a WriteSuccess/WriteFail event from a stream - * @param type Type of this event - * @param stream The stream that generated the event - * @param element Element that generated the event - * @param senderID Sender's id - */ - JBEvent(Type type, JBStream* stream, XMLElement* element, const String& senderID); - - /** - * Destructor. Delete the XML element if valid - */ - virtual ~JBEvent(); - - /** - * Get the event type - * @return The type of this event as enumeration - */ - inline int type() const - { return m_type; } - - /** - * Get the event name - * @return The name of this event - */ - inline const char* name() const - { return lookup(type()); } - - /** - * Get the element's 'type' attribute if any - * @return The element's 'type' attribute - */ - inline const String& stanzaType() const - { return m_stanzaType; } - - /** - * Get the 'from' attribute of a received stanza - * @return The 'from' attribute - */ - inline const JabberID& from() const - { return m_from; } - - /** - * Get the 'to' attribute of a received stanza - * @return The 'to' attribute - */ - inline const JabberID& to() const - { return m_to; } - - /** - * Get the sender's id for Write... events or the 'id' attribute if the - * event carries a received stanza - * @return The event id - */ - inline const String& id() const - { return m_id; } - - /** - * The stanza's text or termination reason for Terminated/Destroy events - * @return The event's text - */ - inline const String& text() const - { return m_text; } - - /** - * Get the stream that generated this event - * @return The stream that generated this event - */ - inline JBStream* stream() const - { return m_stream; } - - /** - * Get the underlying XMLElement - * @return XMLElement pointer or 0 - */ - inline XMLElement* element() const - { return m_element; } - - /** - * Get the first child of the underlying element if any - * @return XMLElement pointer or 0 - */ - inline XMLElement* child() const - { return m_child; } - - /** - * Delete the underlying XMLElement(s). Release the ownership. - * The caller is responsable of the returned pointer - * @param del True to delete all xml elements owned by this event - * @return XMLElement pointer if not deleted or 0 - */ - inline XMLElement* releaseXML(bool del = false) { - TelEngine::destruct(m_child); - if (del) { - TelEngine::destruct(m_element); - return 0; - } - XMLElement* tmp = m_element; - m_element = 0; - return tmp; - } - - /** - * Release the link with the stream to let the stream continue with events - */ - void releaseStream(); - - /** - * Create an error response from this event if it contains a known type. - * Don't create the error response if this event is carrying a response - * @param type Error type - * @param error The error condition - * @param text Optional text to add to the error element - * @return A valid XMLElement pointer - */ - XMLElement* createError(XMPPError::ErrorType type, XMPPError::Type error, const char* text = 0); - - /** - * Get the name of an event type - * @return The name an event type - */ - inline static const char* lookup(int type) - { return TelEngine::lookup(type,s_type); } - -private: - static TokenDict s_type[]; // Event names - JBEvent() {} // Don't use it! - bool init(JBStream* stream, XMLElement* element); - - Type m_type; // Type of this event - JBStream* m_stream; // The stream that generated this event - bool m_link; // Stream link state - XMLElement* m_element; // Received XML element, if any - XMLElement* m_child; // The first child element for 'iq' elements - String m_stanzaType; // Stanza's 'type' attribute - JabberID m_from; // Stanza's 'from' attribute - JabberID m_to; // Stanza's 'to' attribute - String m_id; // Sender's id for Write... events - // 'id' attribute if the received stanza has one - String m_text; // The stanza's text or termination reason for - // Terminated/Destroy events -}; - -/** - * A socket used used to transport data for a Jabber stream - * @short A Jabber streams's socket - */ -class YJINGLE_API JBSocket -{ - friend class JBStream; -public: - /** - * Constructor. Build socket for an outgoing stream - * @param engine The Jabber engine - * @param stream The stream owning this socket - * @param address The address used to connect to - * @param port Port used to connect to remote server - */ - JBSocket(JBEngine* engine, JBStream* stream, - const char* address, int port); - - /** - * Destructor. Close the socket - */ - inline ~JBSocket() - { terminate(); } - - /** - * Check if the socket is valid - * @return True if the socket is valid. - */ - inline bool valid() const - { return m_socket && m_socket->valid(); } - - /** - * Get the remote peer's address - * @return The remote peer's address - */ - inline const SocketAddr& addr() const - { return m_address; } - - /** - * Get last connect/send/receive error text - * @return Last error text - */ - inline const String& error() const - { return m_error; } - - /** - * Connect the socket - * @param terminated True if false is returned and the socket was terminated - * while connecting - * @param newAddr Optional address to connect to - * @param newPort Optional port to connect to - * @return False on failure - */ - bool connect(bool& terminated, const char* newAddr, int newPort = 0); - - /** - * Terminate the socket - * @param shutdown True to shut down, false to asynchronously terminate the socket - */ - void terminate(bool shutdown = false); - - /** - * Read data from socket - * @param buffer Destination buffer - * @param len The number of bytes to read. On exit contains the number of - * bytes actually read - * @return False on socket error - */ - bool recv(char* buffer, unsigned int& len); - - /** - * Write data to socket - * @param buffer Source buffer - * @param len The number of bytes to send - * @return False on socket error - */ - bool send(const char* buffer, unsigned int& len); - -private: - JBEngine* m_engine; // The Jabber engine - JBStream* m_stream; // Stream owning this socket - Socket* m_socket; // The socket - String m_remoteDomain; // Remote domain used to resolve before connecting - SocketAddr m_address; // Remote address - Mutex m_streamMutex; // Lock stream - Mutex m_receiveMutex; // Lock receive - String m_error; // Keep error string from send/receive/connect -}; - -/** - * Base class for all Jabber streams. Basic stream data processing: send/receive - * XML elements, keep stream state, generate events - * @short A Jabber stream - */ -class YJINGLE_API JBStream : public RefObject -{ - friend class JBEngine; - friend class JBEvent; -public: - /** - * Stream state enumeration. - */ - enum State { - Idle = 0, // Stream is waiting to be connected or destroyed - Connecting = 1, // Stream is waiting for the socket to connect - Started = 2, // Stream start tag sent - Securing = 3, // Stream is currently negotiating the TLS - Register = 4, // A new user is currently registering - Auth = 5, // Stream is currently authenticating - Running = 6, // Established. Allow XML stanzas to pass over the stream - Destroy = 7, // Stream is destroying. No more traffic allowed - }; - - /** - * Values returned by send() methods. - */ - enum Error { - ErrorNone = 0, // No error (stanza enqueued/sent) - ErrorContext, // Invalid stream context (state) or parameters - ErrorPending, // The operation is pending in the stream's queue - ErrorNoSocket, // Unrecoverable socket error. The stream will be terminated - }; - - /** - * Stream behaviour options - */ - enum Flags { - AutoRestart = 0x0001, // Auto restart stream when down - AllowPlainAuth = 0x0002, // Allow plain password authentication - // If not allowed and this is the only method - // offered by server the stream will be terminated - NoVersion1 = 0x0004, // Don't support RFC 3920 TLS/SASL ... - UseTls = 0x0008, // Use TLS if offered. Internally set if the remote server - // always require encryption - UseSasl = 0x0010, // Use SASL as authentication mechanism (RFC 3920) - // If not set, the deprecated XEP-0078 will be used for authentication - AllowUnsafeSetup = 0x0020, // Allow in-band user account setup on unsecured streams - StreamSecured = 0x0100, // Stream already secured - StreamAuthenticated = 0x0200, // Stream already authenticated - NoRemoteVersion1 = 0x0400, // Remote doesn't support RFC 3920 TLS/SASL ... - }; - - /** - * Destructor. - * Gracefully close the stream and the socket - */ - virtual ~JBStream(); - - /** - * Get the type of this stream. See the protocol enumeration of the engine - * @return The type of this stream - */ - inline int type() const - { return m_type; } - - /** - * Get the stream state - * @return The stream state as enumeration. - */ - inline State state() const - { return m_state; } - - /** - * Get the stream direction - * @return True if the stream is an outgoing one - */ - inline bool outgoing() const - { return m_outgoing; } - - /** - * Get the stream's name - * @return The stream's name - */ - inline const String& name() const - { return m_name; } - - /** - * Get the stream id - * @return The stream id - */ - inline const String& id() const - { return m_id; } - - /** - * Get the stream's owner - * @return Pointer to the engine owning this stream - */ - inline JBEngine* engine() const - { return m_engine; } - - /** - * Get the JID of the local side of this stream - * @return The JID of the local side of this stream - */ - inline const JabberID& local() const - { return m_local; } - - /** - * Get the JID of the remote side of this stream - * @return The JID of the remote side of this stream - */ - inline const JabberID& remote() const - { return m_remote; } - - /** - * Get the remote peer's address - * @return The remote peer's address - */ - inline const SocketAddr& addr() const - { return m_socket.addr(); } - - /** - * Check if a given option (or option mask) is set - * @param mask The flag(s) to check - * @return True if set - */ - inline bool flag(int mask) const - { return 0 != (m_flags & mask); } - - /** - * Get the stream mutex - * @return The stream mutex - */ - inline Mutex* streamMutex() - { return &m_socket.m_streamMutex; } - - /** - * Connect the stream. Send stream start tag on success - * This method is thread safe - */ - void connect(); - - /** - * Read data from socket and pass it to the parser. Terminate stream on - * socket or parser error. - * This method is thread safe - * @return True if data was received - */ - bool receive(); - - /** - * Send a stanza. - * This method is thread safe - * @param stanza Element to send - * @param senderId Optional sender's id. Used for notification events - * @return The result of posting the stanza - */ - virtual Error sendStanza(XMLElement* stanza, const char* senderId = 0); - - /** - * Stream state and data processor. Increase restart counter. - * Restart stream if idle and auto restart. - * Extract an element from parser and construct an event. - * This method is thread safe - * @param time Current time - * @return JBEvent pointer or 0 - */ - JBEvent* getEvent(u_int64_t time); - - /** - * Terminate stream. Send stream end tag or error. - * Remove pending stanzas without id. Deref stream if destroying. - * This method is thread safe - * @param destroy True to destroy. False to terminate - * @param recvStanza Received stanza, if any - * @param error Termination reason. Set it to NoError to send stream end tag - * @param reason Optional text to be added to the error stanza - * @param send True to send the stream end element - * @param final True if called from destructor - * @param sendError True to send the error element (ignored if error is NoError) - */ - void terminate(bool destroy, XMLElement* recvStanza, XMPPError::Type error, const char* reason, - bool send, bool final = false, bool sendError = true); - - /** - * Remove pending stanzas with a given id. - * This method is thread safe - * @param id The id of stanzas to remove - * @param notify True to raise an event for each removed stanza - */ - inline void removePending(const String& id, bool notify = false) { - Lock lock(m_socket.m_streamMutex); - removePending(notify,&id,false); - } - - /** - * Get the string representation of this stream - * @return The string representation of this stream - */ - virtual const String& toString() const - { return name(); } - - /** - * Get an object from this stream - * @param name The name of the object to get - */ - virtual void* getObject(const String& name) const; - - /** - * Get the name of a stream state - * @param state The requested state number - * @return The name of the requested state - */ - static const char* lookupState(int state); - - /** - * Dictionary keeping the flag names - */ - static TokenDict s_flagName[]; - -protected: - /** - * Internal wait states enumeration. Defines what kind of XML is expected - */ - enum WaitState { // Main state Wait - WaitIdle, // Idle nothing - WaitStart, // Started stream start response - WaitFeatures, // Started stream features - WaitBindRsp, // Started wait for bind response - WaitSessionRsp, // Started wait for session response - WaitTlsRsp, // Started wait response to starttls - WaitChallenge, // Auth iq auth query with auth data sent, wait response - WaitResponse, // Auth iq auth query sent, wait response - WaitAborted, // Auth abort sent, wait confirmation to terminate stream - }; - - /** - * Constructor. Build an outgoing stream - * @param engine The engine that owns this stream - * @param type Stream type - * @param info Structure containing data used to connect to remote server - * @param localJid Local party's JID - * @param remoteJid Remote party's JID - */ - JBStream(JBEngine* engine, int type, XMPPServerInfo& info, - const JabberID& localJid, const JabberID& remoteJid); - - /** - * Default constructor - */ - inline JBStream() - : m_socket(0,0,0,0) - {} - - /** - * Close the stream. Release memory - */ - virtual void destroyed(); - - /** - * Check the 'to' attribute of a received element - * @param xml The received element - * @param respond Action to be taken when if not accepted. - * True to respond with an error, false to just drop it - * @return False to reject it. If the stream is not in Running state, - * it will be terminated - */ - virtual bool checkDestination(XMLElement* xml, bool& respond); - - /** - * Get the starting stream element to be sent after stream connected - * @return XMLElement pointer - */ - virtual XMLElement* getStreamStart(); - - /** - * Get the authentication element to be sent when authentication starts - * @return XMLElement pointer or 0 on failure - */ - virtual XMLElement* getAuthStart(); - - /** - * Process a received stanza in Running state - * @param xml Valid XMLElement pointer - */ - virtual void processRunning(XMLElement* xml); - - /** - * Process a received element in Register state. Descendants MUST consume the data - * @param xml Valid XMLElement pointer - */ - virtual void processRegister(XMLElement* xml); - - /** - * Process a received element in Auth state. Descendants MUST consume the data - * @param xml Valid XMLElement pointer - */ - virtual void processAuth(XMLElement* xml); - - /** - * Process a received element in Securing state. Descendants MUST consume the data. - * Drop the received element - * @param xml Valid XMLElement pointer - */ - virtual void processSecuring(XMLElement* xml); - - /** - * Process a received element in Started state. Descendants MUST consume the data - * @param xml Valid XMLElement pointer - */ - virtual void processStarted(XMLElement* xml); - - /** - * Notify descendants when stream state changed to Running - */ - virtual void streamRunning() - {} - - /** - * Create an iq event from a received iq stanza - * @param xml Received element - * @param iqType The iq type - * @param error Error type if 0 is returned - * @return JBEvent pointer or 0 - */ - JBEvent* getIqEvent(XMLElement* xml, int iqType, XMPPError::Type& error); - - /** - * Send declaration and stream start - * @return True on success - */ - bool sendStreamStart(); - - /** - * Send stream XML elements through the socket - * @param e The element to send - * @param newState The new stream state on success - * @return False if send failed (stream termination was initiated) - */ - bool sendStreamXML(XMLElement* e, State newState); - - /** - * Terminate stream on receiving invalid elements - * @param xml Received element - * @param error Termination reason - * @param reason Optional text to be added to the error stanza - */ - void invalidStreamXML(XMLElement* xml, XMPPError::Type error, const char* reason); - - /** - * Terminate stream on receiving stanza errors while not running - * @param xml Received element - */ - void errorStreamXML(XMLElement* xml); - - /** - * Drop an unexpected or unhandled element - * @param xml Received element - * @param unexpected True if unexpected - */ - void dropXML(XMLElement* xml, bool unexpected = true); - - /** - * Change stream's state. Raise a Running event when apropriate - * @param newState the new stream state - */ - void changeState(State newState); - - /** - * Clear the remote feature list. Parse the received element to fill it up. - * Terminate the stream on error (such as invalid namespace). - * If false is returned, don't re-use the received element - * @param features Features element to parse - * @return False if the stream is terminated - */ - bool getStreamFeatures(XMLElement* features); - - /** - * Start client TLS. Terminate the stream on error - * @return True if TLS was initiated. False on failure: stream termination was initiated - */ - bool startTls(); - - /** - * Start client registration - * Terminate the stream on error - * @return False if the stream is terminated - */ - bool startRegister(); - - /** - * Start client authentication. Send first request to authenticate with the server. - * Terminate the stream on error - * @return False if the stream is terminated - */ - bool startAuth(); - - /** - * Send authentication response. Terminate the stream on error - * @param challenge Received challenge. If non 0 a SASL response is built and sent. - * If 0, a non-SASL response is sent (using handshaking for component and - * XEP-0078 for client streams) - * @return False if the stream is terminated - */ - bool sendAuthResponse(XMLElement* challenge = 0); - - /** - * Build SASL authentication response (Plain or Digest MD5 SASL). - * A valid mechanism must be previously set - * @param response Destination string - * @param realm Received realm or 0 to use local jid. If 0, nonce param is ignored - * @param nonce Server nonce if available - */ - void buildSaslResponse(String& response, String* realm = 0, - String* nonce = 0); - - /** - * Parse remote's features and pick an authentication mechanism - * to be used when requesting authentication - */ - void setClientAuthMechanism(); - - /** - * Build a Digest MD5 SASL (RFC 2831) to be sent with authentication responses - * @param dest Destination string - * @param authenticate True if building a Digest MD5 challenge response, false if - * building a Digest MD5 to check a 'success' response - */ - void buildDigestMD5Sasl(String& dest, bool authenticate = true); - - /** - * Safely set receive count - * @param value The new value of the receive count - */ - void setRecvCount(int value); - - /** - * Start the idle timer if there are no pending stanzas - * @param time The current time in miliseconds - * @return True if started - */ - bool startIdleTimer(u_int64_t time = Time::msecNow()); - - /** - * Get last event from queue - * @return JBEvent pointer or 0 - */ - inline JBEvent* lastEvent() { - ObjList* o = m_events.last(); - return o ? static_cast(o->get()) : 0; - } - - /** - * Stream's name - */ - String m_name; - - /** - * The password used for authentication - */ - String m_password; - - /** - * Local party feature list - */ - JIDFeatureList m_localFeatures; - - /** - * Remote party feature list - */ - JIDFeatureList m_remoteFeatures; - - /** - * Stream flags - */ - int m_flags; - - /** - * The number of challenge/response exchanges allowed before ending the stream - */ - unsigned int m_challengeCount; - - /** - * Internal states - */ - WaitState m_waitState; - - /** - * Chosen authentication mechanism - */ - JIDFeatureSasl::Mechanism m_authMech; - - /** - * Events queue - */ - ObjList m_events; - - /** - * Register a new account - */ - bool m_register; - -private: - // Event termination notification - // @param event The notifier. Ignored if it's not m_lastEvent - void eventTerminated(const JBEvent* event); - // Try sending pending stream element if any - // Try to send the first element in pending outgoing stanzas list - // If ErrorNoSocket is returned, stream termination was initiated - Error sendPending(); - // Remove pending elements with id if id is not 0 - // Remove all elements without id if id is 0 - // Set force to true to remove the first element even if partially sent - void removePending(bool notify, const String* id, bool force); - // Called when a setup state was completed - // Set/reset some stream flags and data - void resetStream(); - - int m_type; // Stream type - State m_state; // Stream state - bool m_outgoing; // Stream direction - unsigned int m_restart; // Remaining restart attempts - unsigned int m_restartMax; // Max restart attempts - u_int64_t m_timeToFillRestart; // Next time to increase the restart counter - u_int64_t m_setupTimeout; // Stream setup timeout (interval allowed between Idle and Running states) - u_int64_t m_idleTimeout; // Connection idle in state Running (send keep alive packet) - String m_id; // Stream id - JabberID m_local; // Local peer's jid - JabberID m_remote; // Remote peer's jid - JBEngine* m_engine; // The owner of this stream - JBSocket m_socket; // The socket used by this stream - XMLParser m_parser; // XML parser - ObjList m_outXML; // Outgoing XML elements - JBEvent* m_lastEvent; // Last generated event - JBEvent* m_terminateEvent; // Destroy/Terminate event - JBEvent* m_startEvent; // Running event - int m_recvCount; // The number of bytes to read: -1: all, 0: nothing 1: 1 byte - XMLElementOut* m_streamXML; // Pending (incomplete) stream element - unsigned int m_declarationSent; // The number of declaration bytes sent - // Auth data - unsigned int m_nonceCount; // Nonce count - String m_nc; // Nonce count string - String m_nonce; // Server nonce - String m_cnonce; // Client nonce - String m_realm; // Client realm - // Register data - unsigned int m_registerId; // Id used when registering a new account -}; - -/** - * This class holds a Jabber Component stream (implements the Jabber Component Protocol). - * @short A Jabber Component stream - */ -class YJINGLE_API JBComponentStream : public JBStream -{ - friend class JBEngine; -public: - /** - * Destructor - */ - virtual ~JBComponentStream() - {} - - /** - * Get an object from this stream - * @param name The name of the object to get - * @return Pointer to the object or 0 if not found - */ - virtual void* getObject(const String& name) const; - -protected: - /** - * Constructor. Build an outgoing stream - * @param engine The engine that owns this stream - * @param info Structure containing data used to connect to remote server - * @param localJid Local party's JID - * @param remoteJid Remote party's JID - */ - JBComponentStream(JBEngine* engine, XMPPServerInfo& info, - const JabberID& localJid, const JabberID& remoteJid); - - /** - * Get the starting stream element to be sent after stream connected - * @return XMLElement pointer - */ - virtual XMLElement* getStreamStart(); - - /** - * Get the authentication element to be sent when authentication starts - * @return XMLElement pointer - */ - virtual XMLElement* getAuthStart(); - - /** - * Process a received element in Auth state - * @param xml Valid XMLElement pointer - */ - virtual void processAuth(XMLElement* xml); - - /** - * Process a received element in Started state - * @param xml Valid XMLElement pointer - */ - virtual void processStarted(XMLElement* xml); - -private: - // Default constructor is private to avoid unwanted use - JBComponentStream() {} -}; - -/** - * This class holds a Jabber client stream used to connect an user to its server - * @short A Jabber client to server stream - */ -class YJINGLE_API JBClientStream : public JBStream -{ - friend class JBEngine; -public: - /** - * Destructor - */ - virtual ~JBClientStream(); - - /** - * Get the roster of this stream's client - * @return Valid XMPPUserRoster - */ - inline XMPPUserRoster* roster() - { return m_roster; } - - /** - * Get the client's resource - * @return The client's resource - */ - inline JIDResource* getResource() - { return m_resource; } - - /** - * Get an object from this stream - * @param name The name of the object to get - * @return Pointer to the object or 0 if not found - */ - virtual void* getObject(const String& name) const; - - /** - * Get a remote user from roster - * @param jid The user's bare jid - * @return Referenced XMPPUser object or 0 if not found - */ - XMPPUser* getRemote(const JabberID& jid); - - /** - * Send a stanza. This method is thread safe - * @param stanza Element to send - * @param senderId Optional sender's id. Used for notification events - * @return The result of posting the stanza - */ - virtual Error sendStanza(XMLElement* stanza, const char* senderId = 0); - -protected: - /** - * Constructor. Build an outgoing stream - * @param engine The engine that owns this stream - * @param info Structure containing data used to connect to remote server - * @param jid Client's full Jabber ID - * @param params Other stream parameters - */ - JBClientStream(JBEngine* engine, XMPPServerInfo& info, const JabberID& jid, - const NamedList& params); - - /** - * Constructor - * @param engine The engine that owns this stream - * @param jid User's JID - * @param password Password used for authentication - * @param address The remote address to connect to - * @param autoRestart True to auto restart the stream - * @param maxRestart The maximum restart attempts allowed - * @param incRestartInterval The interval to increase the restart counter - * @param allowPlainAuth Allow plain text password authentication - * @param outgoing Stream direction - */ - JBClientStream(JBEngine* engine, const JabberID& jid, - const String& password, const SocketAddr& address, - bool autoRestart, unsigned int maxRestart, u_int64_t incRestartInterval, - bool allowPlainAuth = false, bool outgoing = true); - - /** - * Notification from parent when steam is authenticated: get roster from server - */ - virtual void streamRunning(); - - /** - * Process a received stanza in Running state - * @param xml Valid XMLElement pointer - */ - virtual void processRunning(XMLElement* xml); - - /** - * Check the 'to' attribute of a received element against the local jid. - * Accept empty or bare/full jid match. Set the 'to' attribute to local jid if empty - * @param xml The received element - * @param respond Action to be taken if not accepted. Always false on exit - * @return False to reject it - */ - virtual bool checkDestination(XMLElement* xml, bool& respond); - -private: - // Default constructor is private to avoid unwanted use - JBClientStream() {} - - XMPPUserRoster* m_roster; // Client's roster - JIDResource* m_resource; // Client's resource - String m_rosterReqId; // Roster request id -}; - - -/** - * This class holds encapsulates a private library thread - * @short A Jabber thread that can be added to a list of threads - */ -class YJINGLE_API JBThread : public GenObject -{ -public: - /** - * Thread type enumeration. Used to do a specific client processing - */ - enum Type { - StreamConnect, // Asynchronously connect a stream's socket - EngineReceive, // Read all streams sockets - EngineProcess, // Get events from sockets and send them to - // registered services - Presence, // Presence service processor - Jingle, // Jingle service processor - Message // Message service processor - }; - - /** - * Destructor. Remove itself from the owner's list - */ - virtual ~JBThread(); - - /** - * Get the type of this thread - * @return Thread type as enumeration - */ - inline Type type() const - { return m_type; } - - /** - * Cancel (terminate) this thread - * @param hard Kill the thread the hard way rather than just setting - * an exit check marker - */ - virtual void cancelThread(bool hard = false) = 0; - - /** - * Create and start a private thread - * @param type Thread type - * @param list The list owning this thread - * @param client The client to process - * @param sleep Time to sleep if there is nothing to do, zero to use platform default - * @param prio Thread priority, defaults to Normal - * @return False if failed to start the requested thread - */ - static bool start(Type type, JBThreadList* list, void* client, int sleep = 0, int prio = Thread::Normal); - -protected: - /** - * Constructor. Append itself to the owner's list - * @param type Thread type - * @param owner The list owning this thread - * @param client The client to process - * @param sleep Time to sleep if there is nothing to do - */ - JBThread(Type type, JBThreadList* owner, void* client, int sleep = 2); - - /** - * Process the client - */ - void runClient(); - - /** - * Get the stream's client - * @return The stream's client - */ - inline void* client() - { return m_client; } - -private: - Type m_type; // Thread type - JBThreadList* m_owner; // List owning this thread - void* m_client; // The client to process - int m_sleep; // Time to sleep if there is nothing to do -}; - - -/** - * This class holds a list of private threads for an object that wants to terminate them on destroy - * @short A list of private threads - */ -class YJINGLE_API JBThreadList -{ - friend class JBThread; -public: - /** - * Get the enabler owning this list - * @return The owner of this list - */ - inline DebugEnabler* owner() const - { return m_owner; } - - /** - * Cancel all threads - * This method is thread safe - * @param wait True to wait for the threads to terminate - * @param hard Kill the threads the hard way rather than just setting an exit check marker - */ - void cancelThreads(bool wait = true, bool hard = false); - -protected: - /** - * Constructor - * @param owner The owner of this list - */ - JBThreadList(DebugEnabler* owner = 0) - : m_mutex(true,"JBThreadList"), - m_owner(owner), m_cancelling(false) - { m_threads.setDelete(false); } - - /** - * Set the enabler owning this list - * @param dbg The new owner of this list - */ - inline void setOwner(DebugEnabler* dbg) - { m_owner = dbg; } - -private: - Mutex m_mutex; // Lock list operations - DebugEnabler* m_owner; // The owner of this list - ObjList m_threads; // Private threads list - bool m_cancelling; // Cancelling threads operation in progress -}; - - -/** - * This class holds a Jabber engine - * @short A Jabber engine - */ -class YJINGLE_API JBEngine : public DebugEnabler, public Mutex, - public GenObject, public JBThreadList -{ - friend class JBStream; -public: - /** - * Jabber protocol type - */ - enum Protocol { - Component = 1, // Use Jabber Component protocol - Client = 2, // Use client streams - }; - - /** - * Service type enumeration - */ - enum Service { - ServiceJingle = 0, // Receive Jingle events - ServiceIq = 1, // Receive generic Iq events - ServiceMessage = 2, // Receive Message events - ServicePresence = 3, // Receive Presence events - ServiceCommand = 4, // Receive Command events - ServiceDisco = 5, // Receive Disco events - ServiceStream = 6, // Receive stream Terminated or Destroy events - ServiceWriteFail = 7, // Receive WriteFail events - ServiceRoster = 8, // Receive roster events - ServiceCount = 9 - }; - - /** - * Constructor - * @param proto The protocol used by the streams belonging to this engine - */ - JBEngine(Protocol proto); - - /** - * Destructor - */ - virtual ~JBEngine(); - - /** - * Get the Jabber protocol this engine is using - * @return The Jabber protocol as enumeration - */ - inline Protocol protocol() const - { return m_protocol; } - - /** - * Get the default component server - * @return The default component server - */ - inline const JabberID& componentServer() const - { return m_componentDomain; } - - /** - * Set the alternate domain name - * @param domain Name of an acceptable alternate domain - */ - inline void setAlternateDomain(const char* domain = 0) - { m_alternateDomain = domain; } - - /** - * Get the alternate domain name - * @return the alternate domain name - */ - inline const JabberID& getAlternateDomain() const - { return m_alternateDomain; } - - /** - * Get the default resource name. - * @return The default resource name. - */ - inline const String& defaultResource() const - { return m_defaultResource; } - - /** - * Get the stream list - * @return The list of streams belonging to this engine - */ - inline const ObjList& streams() const - { return m_streams; } - - /** - * Cleanup streams. Stop all threads owned by this engine. Release memory - */ - virtual void destruct(); - - /** - * Initialize the engine's parameters. Start private streams if requested - * @param params Engine's parameters - */ - virtual void initialize(const NamedList& params); - - /** - * Terminate all streams - */ - void cleanup(); - - /** - * Set the default component server to use. The domain must be in the server list. - * Choose the first one from the server list if the given one doesn't exists. - * Do nothing if the protocol is not Component - * @param domain Domain name of the server - */ - void setComponentServer(const char* domain); - - /** - * Find a stream by its name. This method is thread safe - * @param name The name of the stream to find - * @return Referenced JBStream pointer or 0 - */ - JBStream* findStream(const String& name); - - /** - * Get a stream. Create it not found and requested. - * For the component protocol, the jid parameter may contain the domain to find, - * otherwise, the default component will be used. - * This method won't create a client stream. Use @ref createClientStream(). - * This method is thread safe - * @param jid Optional jid to use to find or create the stream - * @param create True to create a stream if don't exist. Ignored if the engine's protocol is Client - * @return Referenced JBStream pointer or 0 - */ - JBStream* getStream(const JabberID* jid = 0, bool create = true); - - /** - * Try to get a stream if stream parameter is 0 - * @param stream Stream to check - * @param release Set to true on exit if the caller must deref the stream - * @return True if stream is valid - */ - bool getStream(JBStream*& stream, bool& release); - - /** - * Create a new client stream. This method is thread safe - * @param params Stream parameters - * @return Referenced JBClientStream pointer or 0 - */ - JBClientStream* createClientStream(NamedList& params); - - /** - * Keep calling receive() for each stream until no data is received or the - * thread is terminated - * @return True if data was received - */ - bool receive(); - - /** - * Get events from the streams owned by this engine and send them to a service. - * Delete them if not processed by a service - * @param time Current time - * @return True if an event was generated by any stream - */ - bool process(u_int64_t time); - - /** - * Check if an outgoing stream exists with the same id and remote peer - * @param stream The calling stream - * @return True if found - */ - bool checkDupId(const JBStream* stream); - - /** - * Check the 'from' attribute received by a Component stream at startup - * @param stream The calling stream - * @param from The from attribute to check - * @return True if valid - */ - bool checkComponentFrom(JBComponentStream* stream, const char* from); - - /** - * Asynchronously call the connect method of the given stream if the stream is idle - * @param stream The stream to connect - */ - virtual void connect(JBStream* stream); - - /** - * Check if this engine is exiting - * @return True is terminating - */ - virtual bool exiting() const - { return false; } - - /** - * Setup the transport layer security for a stream - * @param stream The stream requesting the operation - * @return True if stream securing started, false on failure. - */ - virtual bool encryptStream(JBStream* stream); - - /** - * Append a server info element to the list - * @param server The object to add - * @param open True to open the stream, if in component mode - */ - void appendServer(XMPPServerInfo* server, bool open); - - /** - * Get the identity of the given server - * @param destination The destination buffer - * @param full True to get the full identity - * @param token The search string. If 0 and the component protocol is used, - * the default server will be used - * @param domain True to find by domain name. False to find by address - * @return False if server doesn't exists - */ - bool getServerIdentity(String& destination, bool full, const char* token = 0, - bool domain = true); - - /** - * Find server info object - * @param token The search string. If 0 and the Component protocol is used, - * the default component server will be used - * @param domain True to find by domain name. False to find by address - * @return XMPPServerInfo pointer or 0 if not found - */ - XMPPServerInfo* findServerInfo(const char* token, bool domain); - - /** - * Attach a service to this engine. - * This method is thread safe - * @param service The service to attach - * @param type Service type - * @param prio Service priority. Set to -1 to use the givent service's priority. - * A lower values indicates a service with higher priority - */ - void attachService(JBService* service, Service type, int prio = -1); - - /** - * Remove a service from all event handlers of this engine. - * This method is thread safe - * @param service The service to detach - */ - void detachService(JBService* service); - - /** - * Print an XML element to output - * @param xml Element to print - * @param stream Stream requesting the operation - * @param send True if sending, false if receiving - */ - void printXml(const XMLElement& xml, const JBStream* stream, bool send) const; - - /** - * Get the name of a protocol - * @return The name of the requested protocol or the default value - */ - inline static const char* lookupProto(int proto, const char* def = 0) - { return lookup(proto,s_protoName,def); } - - /** - * Get the value associated with a protocol name - * @return The value associated with a protocol name - */ - inline static int lookupProto(const char* proto, int def = 0) - { return lookup(proto,s_protoName,def); } - -private: - // Process a Disco... events - bool processDisco(JBEvent* event); - // Process a Command events - bool processCommand(JBEvent* event); - // Pass events to services - bool received(Service service, JBEvent* event); - static TokenDict s_protoName[]; // Protocol names - - Protocol m_protocol; // The protocol to use - u_int32_t m_restartUpdateInterval; // Update interval for restart counter of all streams - u_int32_t m_restartCount; // The default restart counter value - u_int64_t m_streamSetupInterval; // Timeout for stream setup - u_int64_t m_streamIdleInterval; // Timeout for stream idle (nothing sent/received in Running state) - int m_printXml; // Print XML data to output - ObjList m_streams; // Streams belonging to this engine - JIDIdentity* m_identity; // Engine's identity - JIDFeatureList m_features; // Engine's features - JabberID m_componentDomain; // Default server domain name - String m_componentAddr; // Default server address - int m_componentCheckFrom; // The behaviour when checking the 'from' attribute for a component stream - // 0: no check 1: local identity 2: remote identity - JabberID m_alternateDomain; // Alternate acceptable domain - String m_defaultResource; // Default name for missing resources - Mutex m_serverMutex; // Lock server info list - ObjList m_server; // Server info list - Mutex m_servicesMutex; // Lock service list - ObjList m_services[ServiceCount]; // Services list - bool m_initialized; // True if already initialized -}; - - -/** - * This class is the base class for a Jabber service who wants - * to get specific protocol data from the Jabber engine - * @short A Jabber service - */ -class YJINGLE_API JBService : public DebugEnabler, public Mutex, public GenObject -{ -public: - /** - * Constructor - * @param engine The Jabber engine - * @param name This service's name - * @param params Service's parameters - * @param prio The priority of this service - */ - JBService(JBEngine* engine, const char* name, const NamedList* params, int prio); - - /** - * Destructor. Remove from engine - */ - virtual ~JBService(); - - /** - * Get the Jabber engine - * @return The Jabber engine - */ - inline JBEngine* engine() - { return m_engine; } - - /** - * Get the Jabber engine - * @return The Jabber engine - */ - inline int priority() const - { return m_priority; } - - /** - * Accept an event from the engine. If accepted, the event is enqueued - * and the stream that generated the event is notified on event - * terminated to allow it to process other data. - * This method is thread safe - * @param event The event to accept - * @return False if not accepted, let the engine try another service - */ - bool received(JBEvent* event); - - /** - * Initialize the service - * @param params Service's parameters - */ - virtual void initialize(const NamedList& params) - {} - - /** - * Remove from engine. Release memory - */ - virtual void destruct(); - -protected: - /** - * Accept an event from the engine - * @param event The event to accept - * @param processed Set to true on exit to signal that the event was - * already processed - * @param insert Set to true if accepted to insert on top of the event queue - * @return False if not accepted, let the engine try another service - */ - virtual bool accept(JBEvent* event, bool& processed, bool& insert); - - /** - * Get an event from queue - * @return JBEvent pointer or 0 if queue is empty - */ - JBEvent* deque(); - - /** - * True if already initialized - */ - bool m_initialized; - -private: - inline JBService() {} // Don't use it ! - JBEngine* m_engine; // The Jabber Component engine - int m_priority; // Service priority - ObjList m_events; // Events received from engine -}; - - -/** - * This class is a message receiver service for the Jabber engine - * @short A Jabber message service - */ -class YJINGLE_API JBMessage : public JBService, public JBThreadList -{ -public: - /** - * Message type enumeration - */ - enum MsgType { - Chat, // chat - GroupChat, // groupchat - HeadLine, // headline - Normal, // normal - Error, // error - None, - }; - - /** - * Constructor. Constructs a Jabber message service - * @param engine The Jabber engine - * @param params Service's parameters - * @param prio The priority of this service - */ - inline JBMessage(JBEngine* engine, const NamedList* params, int prio = 0) - : JBService(engine,"jbmsgrecv",params,prio), m_syncProcess(true) - { JBThreadList::setOwner(this); } - - /** - * Destructor. Cancel private thread(s) - */ - virtual ~JBMessage() - { cancelThreads(); } - - /** - * Initialize the service - * @param params Service's parameters - */ - virtual void initialize(const NamedList& params); - - /** - * Get a message from queue - * @return JBEvent pointer or 0 if no messages - */ - inline JBEvent* getMessage() - { return deque(); } - - /** - * Message processor. The derived classes must override this method - * to process received messages - * @param event The event to process - */ - virtual void processMessage(JBEvent* event); - - /** - * Create a 'message' element - * @param type Message type string - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @param message The message body - * @return A valid XMLElement pointer - */ - static XMLElement* createMessage(const char* type, const char* from, - const char* to, const char* id, const char* message); - - /** - * Create a 'message' element - * @param type Message type as enumeration - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param id The 'id' attribute - * @param message The message body - * @return A valid XMLElement pointer - */ - static inline XMLElement* createMessage(MsgType type, const char* from, - const char* to, const char* id, const char* message) - { return createMessage(lookup(type,s_msg,""),from,to,id,message); } - - /** - * Get the type of a 'message' stanza - * @param text The text to check - * @return Message type as enumeration - */ - static inline MsgType msgType(const char* text) - { return (MsgType)lookup(text,s_msg,None); } - - /** - * Get the text from a message type - * @param msg The message type - * @return The associated text or 0 - */ - static inline const char* msgText(MsgType msg) - { return lookup(msg,s_msg,0); } - - /** - * Keep the types of 'message' stanzas - */ - static TokenDict s_msg[]; - -protected: - /** - * Accept an event from the engine and process it if configured to do that - * @param event The event to accept - * @param processed Set to true on exit to signal that the event was already processed - * @param insert Set to true if accepted to insert on top of the event queue - * @return False if not accepted, let the engine try another service - */ - virtual bool accept(JBEvent* event, bool& processed, bool& insert); - -private: - bool m_syncProcess; // Process messages on accept -}; - - -/** - * This class is a presence service for Jabber engine. Handle presence stanzas and - * iq query info or items with destination containing a node and a valid domain - * @short A Jabber presence service - */ -class YJINGLE_API JBPresence : public JBService, public JBThreadList -{ - friend class XMPPUserRoster; -public: - /** - * Presence type enumeration - */ - enum Presence { - Error = 0, // error - Probe = 1, // probe - Subscribe = 2, // subscribe request - Subscribed = 3, // subscribe accepted - Unavailable = 4, // unavailable - Unsubscribe = 5, // unsubscribe request - Unsubscribed = 6, // unsubscribe accepted - None = 7, - }; - - /** - * Constructor. Constructs a Jabber Component presence service - * @param engine The Jabber engine - * @param params Service's parameters - * @param prio The priority of this service - */ - JBPresence(JBEngine* engine, const NamedList* params, int prio = 0); - - /** - * Destructor - */ - virtual ~JBPresence(); - - /** - * Get the auto subscribe parameter - * @return The auto subscribe parameter - */ - inline XMPPDirVal autoSubscribe() const - { return m_autoSubscribe; } - - /** - * Check if the unavailable resources must be deleted - * @return The delete unavailable parameter - */ - inline bool delUnavailable() const - { return m_delUnavailable; } - - /** - * Get the 'add on subscribe' flags - * @return The 'add on subscribe' flags - */ - inline XMPPDirVal addOnSubscribe() const - { return m_addOnSubscribe; } - - /** - * Get the 'add on probe' flags - * @return The 'add on probe' flags - */ - inline XMPPDirVal addOnProbe() const - { return m_addOnProbe; } - - /** - * Get the 'add on presence' flags - * @return The 'add on presence' flags - */ - inline XMPPDirVal addOnPresence() const - { return m_addOnPresence; } - - /** - * Check if this service should add new users when receiving presence, probe or subscribe - * @return True if should add a new user when receiving presence, probe or subscribe - */ - inline bool autoRoster() const - { return m_autoRoster; } - - /** - * Check if this service should ignore destination users not in roster - * @return True if non existent destinations should be ignored - */ - inline bool ignoreNonRoster() const - { return m_ignoreNonRoster; } - - /** - * Get the probe interval. Time to send a probe if nothing was received from that user - * @return The probe interval - */ - inline u_int32_t probeInterval() - { return m_probeInterval; } - - /** - * Get the expire after probe interval - * @return The expire after probe interval - */ - inline u_int32_t expireInterval() - { return m_expireInterval; } - - /** - * Initialize the presence service - * @param params Service's parameters - */ - virtual void initialize(const NamedList& params); - - /** - * Process an event from the receiving list - * This method is thread safe - * @return False if the list is empty - */ - virtual bool process(); - - /** - * Check presence timeout - * This method is thread safe - * @param time Current time - */ - virtual void checkTimeout(u_int64_t time); - - /** - * Process disco info elements - * @param event The event with the element - */ - virtual void processDisco(JBEvent* event); - - /** - * Process a presence error element - * @param event The event with the element - */ - virtual void processError(JBEvent* event); - - /** - * Process a presence probe element - * @param event The event with the element - */ - virtual void processProbe(JBEvent* event); - - /** - * Process a presence subscribe element - * @param event The event with the element - * @param presence Presence type: Subscribe,Subscribed,Unsubscribe,Unsubscribed - */ - virtual void processSubscribe(JBEvent* event, Presence presence); - - /** - * Process a presence unavailable element - * @param event The event with the element - */ - virtual void processUnavailable(JBEvent* event); - - /** - * Process a presence element - * @param event The event with the element - */ - virtual void processPresence(JBEvent* event); - - /** - * Notify on probe request with users we don't know about - * @param event The event with the element - * @return False to send item-not-found error - */ - virtual bool notifyProbe(JBEvent* event); - - /** - * Notify on subscribe event with users we don't know about - * @param event The event with the element - * @param presence Presence type: Subscribe,Subscribed,Unsubscribe,Unsubscribed - * @return False to send item-not-found error - */ - virtual bool notifySubscribe(JBEvent* event, Presence presence); - - /** - * Notify on subscribe event - * @param user The user that received the event - * @param presence Presence type: Subscribe,Subscribed,Unsubscribe,Unsubscribed - */ - virtual void notifySubscribe(XMPPUser* user, Presence presence); - - /** - * Notify on presence event with users we don't know about or presence unavailable - * received without resource (the remote user is entirely unavailable) - * @param event The event with the element - * @param available The availability of the remote user - * @return False to send item-not-found error - */ - virtual bool notifyPresence(JBEvent* event, bool available); - - /** - * Notify on state/capabilities change - * @param user The user that received the event - * @param resource The resource that changet its state or capabilities - */ - virtual void notifyPresence(XMPPUser* user, JIDResource* resource); - - /** - * Notify when a new user is added - * Used basically to add a local resource - * @param user The new user - */ - virtual void notifyNewUser(XMPPUser* user); - - /** - * Get a roster. Add a new one if requested. - * This method is thread safe - * @param jid The user's jid - * @param add True to add the user if doesn't exists - * @param added Optional parameter to be set if a new user was added - * @return Referenced pointer or 0 if none - */ - XMPPUserRoster* getRoster(const JabberID& jid, bool add, bool* added); - - /** - * Get a remote peer of a local one. Add a new one if requested. - * This method is thread safe - * @param local The local peer - * @param remote The remote peer - * @param addLocal True to add the local user if doesn't exists - * @param addedLocal Optional parameter to be set if a new local user was added - * @param addRemote True to add the remote user if doesn't exists - * @param addedRemote Optional parameter to be set if a new remote user was added - * @return Referenced pointer or 0 if none - */ - XMPPUser* getRemoteUser(const JabberID& local, const JabberID& remote, - bool addLocal, bool* addedLocal, bool addRemote, bool* addedRemote); - - /** - * Remove a remote peer of a local one - * This method is thread safe - * @param local The local peer - * @param remote The remote peer - */ - void removeRemoteUser(const JabberID& local, const JabberID& remote); - - /** - * Check if the given domain is a valid (known) one - * @param domain The domain name to check - * @return True if the given domain is a valid one - */ - bool validDomain(const String& domain); - - /** - * Send an element through the given stream. - * If the stream is 0 try to get one from the engine. - * In any case the element is consumed (deleted) - * @param element Element to send - * @param stream The stream to send through - * @return The result of send operation. False if element is 0 - */ - bool sendStanza(XMLElement* element, JBStream* stream); - - /** - * Create an 'presence' element - * @param from The 'from' attribute - * @param to The 'to' attribute - * @param type Presence type as enumeration - * @return A valid XMLElement pointer - */ - static XMLElement* createPresence(const char* from, - const char* to, Presence type = None); - - /** - * Decode an error element - * @param element The XML element - * @param code The 'code' attribute - * @param type The 'type' attribute - * @param error The name of the 'error' child - * @return False if 'element' is 0 or is not a presence one - */ - static bool decodeError(const XMLElement* element, - String& code, String& type, String& error); - - /** - * Get the type of a 'presence' stanza as enumeration - * @param text The text to check - * @return Presence type as enumeration - */ - static inline Presence presenceType(const char* text) - { return (Presence)lookup(text,s_presence,None); } - - /** - * Get the text from a presence type - * @param presence The presence type - * @return The associated text or 0 - */ - static inline const char* presenceText(Presence presence) - { return lookup(presence,s_presence,0); } - - /** - * Cleanup rosters - */ - void cleanup(); - -protected: - /** - * Accept an event from the engine - * @param event The event to accept - * @param processed Set to true on exit to signal that the event was already processed - * @param insert Set to true if accepted to insert on top of the event queue - * @return False if not accepted, let the engine try another service - */ - virtual bool accept(JBEvent* event, bool& processed, bool& insert); - - static TokenDict s_presence[]; // Keep the types of 'presence' - XMPPDirVal m_autoSubscribe; // Auto subscribe state - bool m_delUnavailable; // Delete unavailable user or resource - bool m_autoRoster; // True if this service make an automatically roster management - bool m_ignoreNonRoster; // Ignore all elements whose destination is not in roster - XMPPDirVal m_addOnSubscribe; // Add new user on subscribe request - XMPPDirVal m_addOnProbe; // Add new user on probe request - XMPPDirVal m_addOnPresence; // Add new user on presence - bool m_autoProbe; // Automatically respond to probe requests - u_int32_t m_probeInterval; // Interval to probe a remote user - u_int32_t m_expireInterval; // Expire interval after probe - ObjList m_rosters; // The rosters - JIDIdentity* m_defIdentity; // Default identity - JIDFeatureList m_defFeatures; // Default features - -private: - // Wrapper for getRemoteUser() used when receiving presence - // Show a debug message if not found - XMPPUser* recvGetRemoteUser(const char* type, const JabberID& local, const JabberID& remote, - bool addLocal = false, bool* addedLocal = 0, - bool addRemote = false, bool* addedRemote = 0); - void addRoster(XMPPUserRoster* ur); - void removeRoster(XMPPUserRoster* ur); -}; - - -/** - * This class holds a JID resource (name,presence,capabilities) - * @short A JID resource - */ -class YJINGLE_API JIDResource : public RefObject -{ -public: - /** - * Resource capabilities enumeration. - */ - enum Capability { - CapChat = 1, // Chat capability - CapAudio = 2, // Jingle capability - }; - - /** - * Resource presence enumeration - */ - enum Presence { - Unknown = 0, // unknown - Available = 1, // available - Unavailable = 2, // unavailable - }; - - /** - * Values of the 'show' child of a presence element - */ - enum Show { - ShowAway, // away : Temporarily away - ShowChat, // chat : Actively interested in chatting - ShowDND, // dnd : Busy - ShowXA, // xa : Extended away - ShowNone, // : Missing or no text - }; - - /** - * Constructor. Set data members - * @param name The resource name - * @param presence The resource presence - * @param capability The resource capability - * @param prio The resource priority - */ - inline JIDResource(const char* name, Presence presence = Unknown, - u_int32_t capability = CapChat, int prio = 0) - : m_name(name), m_presence(presence), m_priority(prio), - m_capability(capability), m_show(ShowNone) - {} - - /** - * Destructor - */ - inline virtual ~JIDResource() - {} - - /** - * Get the resource name - * @return The resource name - */ - inline const String& name() const - { return m_name; } - - /** - * Set the resource name - * @param name The new name of the resource - */ - inline void setName(const char* name) - { m_name = name; } - - /** - * Get the presence attribute - * @return The presence attribute - */ - inline Presence presence() const - { return m_presence; } - - /** - * Check if the resource is available - * @return True if the resource is available - */ - inline bool available() const - { return (m_presence == Available); } - - /** - * Get the show attribute as enumeration - * @return The show attribute as enumeration - */ - inline Show show() const - { return m_show; } - - /** - * Set the show attribute - * @param s The new show attribute - */ - inline void show(Show s) - { m_show = s; } - - /** - * Get the status of this resource - * @return The status of this resource - */ - inline const String& status() const - { return m_status; } - - /** - * Set the status of this resource - * @param s The new status of this resource - */ - inline void status(const char* s) - { m_status = s; } - - /** - * Get the priority of this resource - * @return The priority of this resource - */ - inline int priority() - { return m_priority; } - - /** - * Set the priority of this resource - * @param value The new priority of this resource - */ - inline void priority(int value) - { m_priority = value; } - - /** - * Get the list of resource features - * @return The resource features - */ - inline JIDFeatureList& features() - { return m_features; } - - /** - * Get the list containing XML elements with additional data describing this resource - * @return The info list - */ - inline ObjList* infoXml() - { return &m_info; } - - /** - * Set the presence information - * @param value True if available, False if not - * @return True if presence changed - */ - bool setPresence(bool value); - - /** - * Check if the resource has the required capability - * @param capability The required capability - * @return True if the resource has the required capability - */ - inline bool hasCap(Capability capability) const - { return (m_capability & capability) != 0; } - - /** - * Update resource from a presence element - * @param element A presence element - * @return True if presence or capability changed changed - */ - bool fromXML(XMLElement* element); - - /** - * Add capabilities to a presence element - * @param element The target presence element - * @param addInfo True to add the elements from info list - */ - void addTo(XMLElement* element, bool addInfo = true); - - /** - * Get the 'show' child of a presence element - * @param element The XML element - * @return The text or 0 - */ - static const char* getShow(XMLElement* element); - - /** - * Get the 'show' child of a presence element - * @param element The XML element - * @return The text or 0 - */ - static const char* getStatus(XMLElement* element); - - /** - * Get the type of a 'show' element as enumeration - * @param text The text to check - * @return Show type as enumeration - */ - static inline Show showType(const char* text) - { return (Show)lookup(text,s_show,ShowNone); } - - /** - * Get the text from a show type - * @param show The type to get text for - * @return The associated text or 0 - */ - static inline const char* showText(Show show) - { return lookup(show,s_show,0); } - -protected: - static TokenDict s_show[]; // Show texts - -private: - String m_name; // Resource name - Presence m_presence; // Resorce presence - int m_priority; // Resource priority - u_int32_t m_capability; // Resource capabilities - Show m_show; // Show attribute - String m_status; // Status attribute - JIDFeatureList m_features; // Resource features - ObjList m_info; // XML elements containing additional info about this resource -}; - - -/** - * This class holds a resource list - * @short A resource list - */ -class YJINGLE_API JIDResourceList : public Mutex -{ - friend class XMPPUser; - friend class JBPresence; -public: - /** - * Constructor - */ - inline JIDResourceList() - : Mutex(true,"JIDResourceList") - {} - - /** - * Add a resource to the list if a resource with the given name - * doesn't exists - * @param name The resource name - * @return False if the the resource already exists in the list - */ - inline bool add(const String& name) - { return add(new JIDResource(name)); } - - /** - * Add a resource to the list if not already there. - * Destroy the received resource if not added - * @param resource The resource to add - * @return False if the the resource already exists in the list - */ - bool add(JIDResource* resource); - - /** - * Remove a resource from the list - * @param resource The resource to remove - * @param del True to delete the resource - */ - inline void remove(JIDResource* resource, bool del = true) - { Lock lock(this); m_resources.remove(resource,del); } - - /** - * Clear the list - */ - inline void clear() - { Lock lock(this); m_resources.clear(); } - - /** - * Get a resource with the given name - * @param name The resource name - * @return A pointer to the resource or 0 - */ - JIDResource* get(const String& name); - - /** - * Get the first resource from the list - * @return A pointer to the resource or 0 - */ - inline JIDResource* getFirst() { - Lock lock(this); - ObjList* obj = m_resources.skipNull(); - return obj ? static_cast(obj->get()) : 0; - } - - /** - * Get the first resource with audio capability - * @param availableOnly True to get only if available - * @return A pointer to the resource or 0 - */ - JIDResource* getAudio(bool availableOnly = true); - -private: - ObjList m_resources; // The resources list -}; - - -/** - * This class holds a remote XMPP user along with his resources and subscribe state. - * @short An XMPP remote user. - */ -class YJINGLE_API XMPPUser : public RefObject, public Mutex -{ - friend class XMPPUserRoster; - friend class JBPresence; -public: - /** - * Create a remote user. - * @param local The local (owner) user peer. - * @param node The node (username) of the remote peer. - * @param domain The domain of the remote peer. - * @param sub The subscription state. - * @param subTo True to force a subscribe request to remote peer if not subscribed. - * @param sendProbe True to probe the new user. - */ - XMPPUser(XMPPUserRoster* local, const char* node, const char* domain, - XMPPDirVal sub, bool subTo = true, bool sendProbe = true); - - /** - * Destructor. - * Send unavailable if not already done. - */ - virtual ~XMPPUser(); - - /** - * Get the jid of this user. - * @return The jid of this user. - */ - inline const JabberID& jid() const - { return m_jid; } - - /** - * Get the roster this user belongs to. - * @return Pointer to the roster this user belongs to. - */ - inline XMPPUserRoster* local() const - { return m_local; } - - /** - * Get the subscription state of this user - * @return The subscription state of this user - */ - inline XMPPDirVal& subscription() - { return m_subscription; } - - /** - * Get the local resource list - * @return The local resource list - */ - inline JIDResourceList& localRes() - { return m_localRes; } - - /** - * Get the remote resource list - * @return The remote resource list - */ - inline JIDResourceList& remoteRes() - { return m_remoteRes; } - - /** - * Add a local resource to the list. - * Send presence if the remote peer is subscribed to the local one. - * This method is thread safe. - * @param resource The resource to add. - * @param send True to send presence from the resource if it is a new one - * @return False if the the resource already exists in the list. - */ - bool addLocalRes(JIDResource* resource, bool send = true); - - /** - * Remove a local resource from the list. - * Send unavailable if the remote peer is subscribed to the local one. - * This method is thread safe. - * @param resource The resource to remove. - */ - void removeLocalRes(JIDResource* resource); - - /** - * Remove all local resources. - * Send unavailable if the remote peer is subscribed to the local one. - * This method is thread safe. - */ - void clearLocalRes(); - - /** - * Add a remote resource to the list. This method is thread safe. - * @param resource The resource to add - * @return False if the the resource already exists in the list - */ - bool addRemoteRes(JIDResource* resource); - - /** - * Remove a remote resource from the list. This method is thread safe. - * @param resource The resource to remove - */ - void removeRemoteRes(JIDResource* resource); - - /** - * Get the first remote resource with audio capability. - * @param local True to request a local resource, false for a remote one. - * @param availableOnly True to get only if available. - * @return A pointer to the resource or 0. - */ - inline JIDResource* getAudio(bool local, bool availableOnly = true) { - return local ? m_localRes.getAudio(availableOnly) : - m_remoteRes.getAudio(availableOnly); - } - - /** - * Process received error elements. - * This method is thread safe. - * @param event The event with the element. - */ - void processError(JBEvent* event); - - /** - * Process received probe from remote peer. - * This method is thread safe. - * @param event The event with the element. - * @param resName The probed resource if any. - */ - void processProbe(JBEvent* event, const String* resName = 0); - - /** - * Process received presence from remote peer. - * This method is thread safe. - * @param event The event with the element. - * @param available The availability of the user. - * @return False if remote user has no more resources available. - */ - bool processPresence(JBEvent* event, bool available); - - /** - * Process received subscription from remote peer. - * This method is thread safe. - * @param event The event with the element. - * @param type The subscription type: subscribe/unsubscribe/subscribed/unsubscribed. - */ - void processSubscribe(JBEvent* event, JBPresence::Presence type); - - /** - * Probe the remote user. - * This method is thread safe. - * @param stream Optional stream to use to send the request. - * @param time Probe time. - * @return True if send succeedded. - */ - bool probe(JBStream* stream, u_int64_t time = Time::msecNow()); - - /** - * Send subscription to remote peer. - * This method is thread safe. - * @param type The subscription type: subscribe/unsubscribe/subscribed/unsubscribed. - * @param stream Optional stream to use to send the data. - * @return True if send succeedded. - */ - bool sendSubscribe(JBPresence::Presence type, JBStream* stream); - - /** - * Send unavailable to remote peer. - * This method is thread safe. - * @param stream Optional stream to use to send the data. - * @return True if send succeedded. - */ - bool sendUnavailable(JBStream* stream); - - /** - * Send presence to remote peer. - * This method is thread safe. - * @param resource The resource to send from. - * @param stream Optional stream to use to send the data. - * @param force True to send even if we've already sent presence from this resource. - * @return True if send succeedded. - */ - bool sendPresence(JIDResource* resource, JBStream* stream = 0, bool force = false); - - /** - * Check if this user sent us any presence data for a given interval. - * This method is thread safe. - * @param time Current time. - * @return True if the user timed out. - */ - bool timeout(u_int64_t time); - - /** - * Notify the state of a resource. - * This method is thread safe. - * @param remote True for a remote resource: notify the presence engine. - * False for a local resource: send presence to remote user. - * @param name Resource name. - * @param stream Optional stream to use to send the data if remote is false. - * @param force True to send even if we've already sent presence from this resource. - */ - void notifyResource(bool remote, const String& name, - JBStream* stream = 0, bool force = false); - - /** - * Notify the state of all resources. - * This method is thread safe. - * @param remote True for remote resources: notify the presence engine. - * False for local resources: send presence to remote user. - * @param stream Optional stream to use to send the data if remote is false. - * @param force True to send even if we've already sent presence from a resource. - */ - void notifyResources(bool remote, JBStream* stream = 0, bool force = false); - -protected: - /** - * Update subscription state. - * @param from True for subscription from remote user. False for subscription to the remote user. - * @param value True if subscribed. False is unsubscribed. - * @param stream Optional stream to use to send presence if subscription from remote user changed to true. - */ - void updateSubscription(bool from, bool value, JBStream* stream); - - /** - * Update user timeout data. - * @param from True if the update is made on incoming data. False if timeout is made on outgoing probe. - * @param time Current time. - */ - void updateTimeout(bool from, u_int64_t time = Time::msecNow()); - -private: - XMPPUserRoster* m_local; // Local user - JabberID m_jid; // User's JID - XMPPDirVal m_subscription; // Subscription state - JIDResourceList m_localRes; // Local user's resources - JIDResourceList m_remoteRes; // Remote user's resources - u_int64_t m_nextProbe; // Time to probe - u_int64_t m_expire; // Expire time -}; - -/** - * This class holds the roster for a local user. - * @short The roster of a local user. - */ -class YJINGLE_API XMPPUserRoster : public RefObject, public Mutex -{ - friend class JBPresence; - friend class JBClientStream; - friend class XMPPUser; -public: - /** - * Destructor. - * Remove this roster from engine's queue. - */ - virtual ~XMPPUserRoster(); - - /** - * Get the local user's jid. - * @return The local user's jid. - */ - const JabberID& jid() const - { return m_jid; } - - /** - * Get the presence engine this user belongs to. - * @return Pointer to the presence engine this user belongs to. - */ - JBPresence* engine() - { return m_engine; } - - /** - * Get the list of available resources belonging to the same user - * @return The list of available resources - */ - inline JIDResourceList& resources() - { return m_resources; } - - /** - * Get the list of remote users - * @return The list of remote users - */ - inline ObjList& users() - { return m_remote; } - - /** - * Get a remote user. - * This method is thread safe. - * @param jid User's jid. - * @param add True to add if not found. - * @param added Optional flag to set if added a new user. - * @return Referenced pointer to the user or 0. - */ - XMPPUser* getUser(const JabberID& jid, bool add = false, bool* added = 0); - - /** - * Remove a remote user. - * This method is thread safe. - * @param remote The user to remove. - * @return False if no more users. - */ - bool removeUser(const JabberID& remote); - - /**() - * Clear remote user list. - */ - inline void cleanup() { - Lock lock(this); - m_remote.clear(); - } - - /** - * Check timeout. - * This method is thread safe. - * @param time Current time. - * @return True to remove the roster. - */ - bool timeout(u_int64_t time); - - /** - * Create an iq result to respond to disco info. Add user's features and identity - * @param from The from attribute - * @param to The to attribute - * @param id The id attribute - * @return XMLElement pointer - */ - inline XMLElement* createDiscoInfoResult(const char* from, const char* to, - const char* id) - { return XMPPUtils::createDiscoInfoRes(from,to,id,&m_features,m_identity); } - -protected: - /** - * Constructor. - * @param engine Pointer to the presence engine this user belongs to - * @param node User's name - * @param domain User's domain - * @param proto Protocol. Used to create identity - */ - XMPPUserRoster(JBPresence* engine, const char* node, const char* domain, - JBEngine::Protocol proto = JBEngine::Component); - -private: - inline void addUser(XMPPUser* u) { - Lock lock(this); - m_remote.append(u); - } - void removeUser(XMPPUser* u) { - Lock lock(this); - m_remote.remove(u,false); - } - - JabberID m_jid; // User's bare JID - JIDFeatureList m_features; // Local user's resources - JIDIdentity* m_identity; // JID's identity - ObjList m_remote; // Remote users - JIDResourceList m_resources; // Available resources of the user - JBPresence* m_engine; // Presence engine -}; - -}; - -#endif /* __YATEJABBER_H */ - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yxml/Makefile.in b/libs/yxml/Makefile.in index f41e8c99..63f95f10 100644 --- a/libs/yxml/Makefile.in +++ b/libs/yxml/Makefile.in @@ -9,11 +9,11 @@ DEFS := INCLUDES := -I@top_srcdir@ -I../.. -I@srcdir@ CFLAGS := @CFLAGS@ @MODULE_CPPFLAGS@ @INLINE_FLAGS@ LDFLAGS:= @LDFLAGS@ -L../.. -lyate -INCFILES := @top_srcdir@/yateclass.h @srcdir@/tinystr.h @srcdir@/tinyxml.h +INCFILES := @top_srcdir@/yateclass.h @srcdir@/yatexml.h PROGS= LIBS = libyatexml.a -OBJS = tinystr.o tinyxml.o tinyxmlparser.o +OBJS = XML.o LOCALFLAGS = LOCALLIBS = diff --git a/libs/yxml/XML.cpp b/libs/yxml/XML.cpp new file mode 100644 index 00000000..ae849f71 --- /dev/null +++ b/libs/yxml/XML.cpp @@ -0,0 +1,2018 @@ +/** + * XML.cpp + * This file is part of the YATE Project http://YATE.null.ro + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +using namespace TelEngine; + + +const String XmlElement::s_ns = "xmlns"; +const String XmlElement::s_nsPrefix = "xmlns:"; + + +// Return a replacement char for the given string +char replace(const char* str, const XmlEscape* esc) +{ + if (!str) + return 0; + if (esc) { + for (; esc->value; esc++) + if (!::strcmp(str,esc->value)) + return esc->replace; + } + return 0; +} + +// Return a replacement string for the given char +const char* replace(char replace, const XmlEscape* esc) +{ + if (esc) { + for (; esc->value; esc++) + if (replace == esc->replace) + return esc->value; + } + return 0; +} + +// XmlEscape a string or replace it if found in a list of restrictions +static inline void addAuth(String& buf, const String& comp, const String& value, + bool esc, const String* auth) +{ + if (auth) { + for (; !auth->null(); auth++) + if (*auth == comp) { + buf << "***"; + return; + } + } + if (esc) + XmlSaxParser::escape(buf,value); + else + buf << value; +} + +// Find an entry by its value +// Optional: check if the name starts with a given string +static NamedString* findByValue(const NamedList& list, const String& value, + const String& nameStartsWith, const String& nameIs) +{ + unsigned int n = list.count(); + for (unsigned int i = 0; i < n; i++) { + NamedString* ns = list.getParam(i); + if (!ns || *ns != value) + continue; + if ((nameIs && ns->name() == nameIs) || + (nameStartsWith && ns->name().startsWith(nameStartsWith))) + return ns; + } + return 0; +} + + +/* + * XmlSaxParser + */ +TokenDict XmlSaxParser::s_errorString[] = { + {"No error", NoError}, + {"Error", Unknown}, + {"Not well formed", NotWellFormed}, + {"I/O error", IOError}, + {"Error parsing Element", ElementParse}, + {"Failed to read Element name", ReadElementName}, + {"Bad element name", InvalidElementName}, + {"Error reading Attributes", ReadingAttributes}, + {"Error reading end tag", ReadingEndTag}, + {"Error parsing Comment", CommentParse}, + {"Error parsing Declaration", DeclarationParse}, + {"Error parsing Definition", DefinitionParse}, + {"Error parsing CDATA", CDataParse}, + {"Incomplete", Incomplete}, + {"Invalid encoding", InvalidEncoding}, + {"Unsupported encoding", UnsupportedEncoding}, + {"Unsupported version", UnsupportedVersion}, + {0,0} +}; + +XmlEscape XmlSaxParser::s_escape[] = { + {"<", '<'}, + {">", '>'}, + {"&", '&'}, + {""", '\"'}, + {"'", '\''}, + {0,0} +}; + + +XmlSaxParser::XmlSaxParser(const char* name) + : m_offset(0), m_row(1), m_column(1), m_error(NoError), + m_parsed(""), m_unparsed(None) +{ + debugName(name); +} + +XmlSaxParser::~XmlSaxParser() +{ +} + +// Parse a given string +bool XmlSaxParser::parse(const char* text) +{ + if (TelEngine::null(text)) + return m_error == NoError; + char car; + setError(NoError); + String auxData; + m_buf << text; + if (m_buf.lenUtf8() == -1) { + //FIXME this should not be here in case we have a different encoding + Debug(this,DebugNote,"Request to parse invalid utf-8 data [%p]",this); + return setError(InvalidEncoding); + } + if (unparsed()) { + if (unparsed() != Text) { + if (!auxParse()) + return setError(Incomplete); + } + else + auxData = m_parsed; + resetParsed(); + setUnparsed(None); + } + unsigned int len = 0; + while (m_buf.at(len) && !error()) { + car = m_buf.at(len); + if (car != '<' ) { // We have a new child check what it is + if (car == '>' || !checkDataChar(car)) { + Debug(this,DebugNote,"XML text contains unescaped '%c' character [%p]", + car,this); + return setError(Unknown); + } + len++; // Append xml Text + continue; + } + if (len > 0) { + auxData << m_buf.substr(0,len); + } + if (auxData.c_str()) { // We have an end of tag or another child is riseing + resetError(); + unEscape(auxData); + gotText(auxData); + resetParsed(); + if (error()) + return false; + m_buf = m_buf.substr(len); + len = 0; + auxData = ""; + } + skipBlanks(); + len = 0; + if (!m_buf.at(len + 1)) + return setError(Incomplete); + char auxCar = m_buf.at(len + 1); + if (auxCar == '?') { + m_buf = m_buf.substr(2); + if (!parseInstruction()) + return false; + continue; + } + if (auxCar == '!') { + m_buf = m_buf.substr(2); + if (!parseSpecial()) + return false; + continue; + } + if (auxCar == '/') { + m_buf = m_buf.substr(2); + if (!parseEndTag()) + return false; + continue; + } + // If we are here mens that we have a element + // process an xml element + m_buf = m_buf.substr(1); + if (!parseElement()) + return false; + } + if (len > 0 && !completed()) { + // We have an element that is not complete + auxData << m_buf; + m_parsed.assign(auxData); + m_buf = ""; + setUnparsed(Text); + return setError(Incomplete); + } + if (error()) { + DDebug(this,DebugNote,"Got error while parsing %s [%p]",getError(),this); + return false; + } + m_buf = ""; + resetParsed(); + return true; +} + +// Parse an unfinished xml object +bool XmlSaxParser::auxParse() +{ + switch (unparsed()) { + case Element: + return parseElement(); + case CData: + return parseCData(); + case Comment: + return parseComment(); + case Declaration: + return parseDeclaration(); + case Instruction: + return parseInstruction(); + case EndTag: + return parseEndTag(); + case Special: + return parseSpecial(); + default: + return false; + } +} + +// Set the error code and destroys a child if error code is not NoError +bool XmlSaxParser::setError(Error error, XmlChild* child) +{ + m_error = error; + if (child && error) + TelEngine::destruct(child); + return m_error == XmlSaxParser::NoError; +} + +// Parse an endtag form the main buffer +bool XmlSaxParser::parseEndTag() +{ + bool aux = false; + String* name = extractName(aux); + // We don't check aux flag because we don't look for attributes here + if (!name) { + if (error() && error() == Incomplete) + setUnparsed(EndTag); + return false; + } + if (!aux || m_buf.at(0) == '/') { // The end tag has attributes or contains / char at the end of name + setError(ReadingEndTag); + Debug(this,DebugNote,"Got bad end tag [%p]",name->c_str(),this); + setUnparsed(EndTag); + m_buf = *name + m_buf; + return false; + } + resetError(); + endElement(*name); + if (error()) { + setUnparsed(EndTag); + m_buf = *name + ">"; + TelEngine::destruct(name); + return false; + } + m_buf = m_buf.substr(1); + TelEngine::destruct(name); + return true; +} + +// Parse an instruction form the main buffer +bool XmlSaxParser::parseInstruction() +{ + if (!m_buf.c_str()) { + setUnparsed(Instruction); + return false; + } + skipBlanks(); + // extract the name + String name; + char c; + int len = 0; + while (m_buf.at(len) && !m_parsed.c_str()) { + c = m_buf.at(len); + if (!blank(c) && c != '?') { + if (checkNameCharacter(c)) { + len++; + continue; + } + else { + setError(InvalidElementName); + Debug(this,DebugNote,"Instruction name contains bad character '%c' [%p]",c,this); + return false; + } + } + if (len == 0) { + setError(InvalidElementName); + Debug(this,DebugNote,"Instruction with empty name [%p]",this); + return false; + } + name = m_buf.substr(0,len); + m_buf = m_buf.substr(len); + break; + } + if (m_parsed.c_str()) { + name = m_parsed; + resetParsed(); + } + if (!name.c_str()) { + if (error() && error() == Incomplete) + setUnparsed(Instruction); + return false; + } + if (name.startsWith("xml")) { + return parseDeclaration(); + } + if (name.startsWith("xml",false,true)) { + setError(InvalidElementName); + Debug(this,DebugNote,"Instruction name begin with bad character set %s [%p]", + name.c_str(),this); + return false; + } + NamedString inst(name); + skipBlanks(); + len = 0; + while (m_buf.at(len)) { + c = m_buf.at(len); + if (c != '?') { + if (c == '>' || c == 0x0c) { + setError(Unknown); + Debug(this,DebugNote,"Xml instruction with unaccepted character '%c' [%p]", + c,this); + return false; + } + len++; + continue; + } + if (!m_buf.at(len + 1)) + break; + char ch = m_buf.at(len + 1); + if (ch == '>') { // end of instruction + inst << m_buf.substr(0,len); + resetError(); + if (!inst.c_str()) { + setError(Unknown); + Debug(this,DebugNote,"Empty instruction [%p]",this); + return false; + } + gotProcessing(inst);// TODO call an callback for process instruction + resetParsed(); + if (error()) + return false; + m_buf = m_buf.substr(len + 2); + return true; + } + len ++; + } + // If we are here mens that text has reach his bounds is an error or we need to receive more data + setError(Incomplete); + return false; +} + +// Parse a declaration form the main buffer +bool XmlSaxParser::parseDeclaration() +{ + if (!m_buf.c_str()) { + setUnparsed(Declaration); + return setError(Incomplete); + } + NamedList dc("xml"); + if (m_parsed.count()) { + dc.copyParams(m_parsed); + resetParsed(); + } + char c; + skipBlanks(); + int len = 0; + while (m_buf.at(len)) { + c = m_buf.at(len); + if (c != '?') { + skipBlanks(); + NamedString* s = getAttribute(); + if (!s) { + if (error() && error() == Incomplete) { + setUnparsed(Declaration); + m_parsed.copyParams(dc); + } + return false; + } + len = 0; + if (dc.getParam(s->name())) { + Debug(this,DebugNote,"Duplicate attribute '%s' in declaration [%p]", + s->name().c_str(),this); + return setError(DeclarationParse); + } + dc.setParam(s); + char ch = m_buf.at(len); + if (ch && !blank(ch) && ch != '?') { + Debug(this,DebugNote,"No blanks between attributes in declaration [%p]",this); + return setError(DeclarationParse); + } + skipBlanks(); + continue; + } + if (!m_buf.at(++len)) + break; + char ch = m_buf.at(len); + if (ch == '>') { // end of declaration + resetError(); + gotDeclaration(dc); + resetParsed(); + if (error()) + return false; + m_buf = m_buf.substr(len + 1); + return true; + } + Debug(this,DebugNote,"Invalid declaration ending char '%c' [%p]",ch,this); + return setError(DeclarationParse); + } + setUnparsed(Declaration);; + m_parsed.copyParams(dc); + setError(Incomplete); + return false; +} + +// Parse a CData section form the main buffer +bool XmlSaxParser::parseCData() +{ + if (!m_buf.c_str()) { + setUnparsed(CData); + setError(Incomplete); + return false; + } + String cdata = ""; + if (m_parsed.c_str()) { + cdata = m_parsed; + resetParsed(); + } + char c; + int len = 0; + while (m_buf.at(len)) { + c = m_buf.at(len); + if (c != ']') { + len ++; + continue; + } + if (m_buf.substr(++len,2) == "]>") { // End of CData section + cdata += m_buf.substr(0,len - 1); + resetError(); + gotCdata(cdata); + resetParsed(); + if (error()) + return false; + m_buf = m_buf.substr(len + 2); + return true; + } + } + cdata += m_buf; + m_buf = ""; + setUnparsed(CData); + int length = cdata.length(); + m_buf << cdata.substr(length - 2); + if (length > 1) + m_parsed.assign(cdata.substr(0,length - 2)); + setError(Incomplete); + return false; +} + +// Helper method to classify the Xml objects starting with "') { // End of comment + comment << m_buf.substr(0,len); + m_buf = m_buf.substr(len + 3); +#ifdef DEBUG + if (comment.at(0) == '-' || comment.at(comment.length() - 1) == '-') + DDebug(this,DebugInfo,"Comment starts or ends with '-' character [%p]",this); + if (comment.find("--") >= 0) + DDebug(this,DebugInfo,"Comment contains '--' char sequence [%p]",this); +#endif + gotComment(comment); + resetParsed(); + // The comment can apear anywhere sow SaxParser never + // sets an error when receive a comment + return true; + } + len++; + } + // If we are here we haven't detect the end of comment + comment << m_buf; + int length = comment.length(); + // Keep the last 2 charaters in buffer because if the input buffer ends + // between "--" and ">" + m_buf = comment.substr(length - 2); + setUnparsed(Comment); + if (length > 1) + m_parsed.assign(comment.substr(0,length - 2)); + return setError(Incomplete); +} + +// Parse an element form the main buffer +bool XmlSaxParser::parseElement() +{ + if (!m_buf.c_str()) { + setUnparsed(Element); + return setError(Incomplete); + } + bool empty = false; + if (!m_parsed.c_str()) { + String* name = extractName(empty); + if (!name) { + if (error() == Incomplete) + setUnparsed(Element); + return false; + } +#ifdef XML_STRICT + // http://www.w3.org/TR/REC-xml/ + // Names starting with 'xml' (case insensitive) are reserved + if (name->startsWith("xml",false,true)) { + Debug(this,DebugNote,"Element tag starts with 'xml' [%p]",this); + return setError(ReadElementName); + } +#endif + m_parsed.assign(*name); + TelEngine::destruct(name); + } + if (empty) { // empty flag means that the element does not have attributes + // check if the element is empty + bool aux = m_buf.at(0) == '/'; + if (!processElement(m_parsed,aux)) + return false; + if (aux) + m_buf = m_buf.substr(2); // go back where we were + else + m_buf = m_buf.substr(1); // go back where we were + return true; + } + char c; + skipBlanks(); + int len = 0; + while (m_buf.at(len)) { + c = m_buf.at(len); + if (c == '/' || c == '>') { // end of element declaration + if (c == '>') { + if (!processElement(m_parsed,false)) + return false; + m_buf = m_buf.substr(1); + return true; + } + if (!m_buf.at(++len)) + break; + char ch = m_buf.at(len); + if (ch != '>') { + Debug(this,DebugNote,"Element attribute name contains '/' character [%p]",this); + return setError(ReadingAttributes); + } + if (!processElement(m_parsed,true)) + return false; + m_buf = m_buf.substr(len + 1); + return true; + } + NamedString* ns = getAttribute(); + if (!ns) { // Attribute is invalid + if (error() == Incomplete) + break; + return false; + } + if (m_parsed.getParam(ns->name())) { + Debug(this,DebugNote,"Duplicate attribute '%s' [%p]",ns->name().c_str(),this); + return setError(NotWellFormed); + } + XDebug(this,DebugAll,"Parser adding attribute %s='%s' to '%s' [%p]", + ns->name().c_str(),ns->c_str(),m_parsed.c_str(),this); + m_parsed.setParam(ns); + char ch = m_buf.at(len); + if (ch && !blank(ch) && (ch != '/' && ch != '>')) { + Debug(this,DebugNote,"Element without blanks between attributes [%p]",this); + return setError(NotWellFormed); + } + skipBlanks(); + } + setUnparsed(Element); + return setError(Incomplete); +} + +// Parse a doctype form the main buffer +bool XmlSaxParser::parseDoctype() +{ + if (!m_buf.c_str()) { + setUnparsed(Doctype); + setError(Incomplete); + return false; + } + unsigned int len = 0; + skipBlanks(); + while (m_buf.at(len) && !blank(m_buf.at(len))) + len++; + // Use a while() to break to the end + while (m_buf.at(len)) { + while (m_buf.at(len) && blank(m_buf.at(len))) + len++; + if (len >= m_buf.length()) + break; + if (m_buf[len++] == '[') { + while (len < m_buf.length()) { + if (m_buf[len] != ']') { + len ++; + continue; + } + if (m_buf.at(++len) != '>') + continue; + gotDoctype(m_buf.substr(0,len)); + resetParsed(); + m_buf = m_buf.substr(len + 1); + return true; + } + break; + } + while (len < m_buf.length()) { + if (m_buf[len] != '>') { + len++; + continue; + } + gotDoctype(m_buf.substr(0,len)); + resetParsed(); + m_buf = m_buf.substr(len + 1); + return true; + } + break; + } + setUnparsed(Doctype); + return setError(Incomplete); +} + +// Extract the name of tag +String* XmlSaxParser::extractName(bool& empty) +{ + skipBlanks(); + unsigned int len = 0; + bool ok = false; + empty = false; + while (len < m_buf.length()) { + char c = m_buf[len]; + if (blank(c)) { + if (checkFirstNameCharacter(m_buf[0])) { + ok = true; + break; + } + Debug(this,DebugNote,"Element tag starting with invalid char %c [%p]", + m_buf[0],this); + setError(ReadElementName); + return 0; + } + if (c == '/' || c == '>') { // end of element declaration + if (c == '>') { + if (checkFirstNameCharacter(m_buf[0])) { + empty = true; + ok = true; + break; + } + Debug(this,DebugNote,"Element tag starting with invalid char %c [%p]", + m_buf[0],this); + setError(ReadElementName); + return 0; + } + char ch = m_buf.at(len + 1); + if (!ch) + break; + if (ch != '>') { + Debug(this,DebugNote,"Element tag contains '/' character [%p]",this); + setError(ReadElementName); + return 0; + } + if (checkFirstNameCharacter(m_buf[0])) { + empty = true; + ok = true; + break; + } + Debug(this,DebugNote,"Element tag starting with invalid char %c [%p]", + m_buf[0],this); + setError(ReadElementName); + return 0; + } + if (checkNameCharacter(c)) + len++; + else { + Debug(this,DebugNote,"Element tag contains invalid char %c [%p]",c,this); + setError(ReadElementName); + return 0; + } + } + if (ok) { + String* name = new String(m_buf.substr(0,len)); + m_buf = m_buf.substr(len); + if (!empty) { + skipBlanks(); + empty = (m_buf && m_buf[0] == '>') || + (m_buf.length() > 1 && m_buf[0] == '/' && m_buf[1] == '>'); + } + return name; + } + setError(Incomplete); + return 0; +} + +// Extract an attribute +NamedString* XmlSaxParser::getAttribute() +{ + String name = ""; + skipBlanks(); + char c,sep = 0; + unsigned int len = 0; + + while (len < m_buf.length()) { // Circle until we find attribute value startup character (["]|[']) + c = m_buf[len]; + if (blank(c) || c == '=') { + if (!name.c_str()) + name = m_buf.substr(0,len); + len++; + continue; + } + if (!name.c_str()) { + if (!checkNameCharacter(c)) { + Debug(this,DebugNote,"Attribute name contains %c character [%p]",c,this); + setError(ReadingAttributes); + return 0; + } + len++; + continue; + } + if (c != '\'' && c != '\"') { + Debug(this,DebugNote,"Unenclosed attribute value [%p]",this); + setError(ReadingAttributes); + return 0; + } + sep = c; + break; + } + + if (!sep) { + setError(Incomplete); + return 0; + } + if (!checkFirstNameCharacter(name[0])) { + Debug(this,DebugNote,"Attribute name starting with bad character %c [%p]", + name.at(0),this); + setError(ReadingAttributes); + return 0; + } + NamedString* ns = new NamedString(name); + int pos = ++len; + + while (len < m_buf.length()) { + c = m_buf[len]; + if (c != sep && !badCharacter(c)) { + len ++; + continue; + } + if (badCharacter(c)) { + Debug(this,DebugNote,"Attribute value with unescaped character '%c' [%p]", + c,this); + setError(ReadingAttributes); + return 0; + } + ns->assign(m_buf.substr(pos,len - pos)); + m_buf = m_buf.substr(len + 1); + // End of attribute value + unEscape(*ns); + if (error()) { + TelEngine::destruct(ns); + return 0; + } + return ns; + } + + setError(Incomplete); + return 0; +} + +// Reset this parser +void XmlSaxParser::reset() +{ + m_offset = 0; + m_row = 1; + m_column = 1; + m_error = NoError; + m_buf.clear(); + resetParsed(); + m_unparsed = None; +} + +// Verify if the given character is in the range allowed +bool XmlSaxParser::checkFirstNameCharacter(unsigned char ch) +{ + return ch == ':' || (ch >= 'A' && ch <= 'Z') || ch == '_' || (ch >= 'a' && ch <= 'z') + || (ch >= 0xc0 && ch <= 0xd6) || (ch >= 0xd8 && ch <= 0xf6) || (ch >= 0xf8); +} + +// Check if the given character is in the range allowed for an xml char +bool XmlSaxParser::checkDataChar(unsigned char c) +{ + return c == 0x9 || c == 0xA || c == 0xD || (c >= 0x20); +} + +// Verify if the given character is in the range allowed for a xml name +bool XmlSaxParser::checkNameCharacter(unsigned char ch) +{ + return checkFirstNameCharacter(ch) || ch == '-' || ch == '.' || (ch >= '0' && ch <= '9') + || ch == 0xB7; +} + +// Remove blank characters from the beginning of the buffer +void XmlSaxParser::skipBlanks() +{ + unsigned int len = 0; + while (len < m_buf.length() && blank(m_buf[len])) + len++; + if (len != 0) + m_buf = m_buf.substr(len); +} + +// Obtain a char from an ascii decimal char declaration +inline unsigned char getDec(String& dec) +{ + if (dec.length() > 6) { + DDebug(DebugNote,"Decimal number '%s' too long",dec.c_str()); + return 0; + } + int num = dec.substr(2,dec.length() - 3).toInteger(-1); + if (num > 0 && num < 256) + return num; + DDebug(DebugNote,"Invalid decimal number '%s'",dec.c_str()); + return 0; +} + +// Obtain a hexa representation of the given hexa char +inline signed char hexDecode(char c) +{ + if (('0' <= c) && (c <= '9')) + return c - '0'; + if (('A' <= c) && (c <= 'F')) + return c - 'A' + 10; + if (('a' <= c) && (c <= 'f')) + return c - 'a' + 10; + return -1; +} + +// Obtain a char from the given hexa number +inline unsigned char getHex(String& hex) +{ + if (hex.length() > 6) { + DDebug(DebugNote,"Hex number '%s' too long",hex.c_str()); + return 0; + } + signed char c1 = hexDecode(hex.at(3)); + signed char c2 = hexDecode(hex.at(4)); + if (c1 == -1 || c2 == -1) { + DDebug(DebugNote,"Invalid hex number '%s'",hex.c_str()); + return 0; + } + unsigned char c = (c1 << 4) | c2; + return c; +} + +// Unescape the given text +void XmlSaxParser::unEscape(String& text) +{ + const char* str = text.c_str(); + if (!str) + return; + String buf; + String aux = "&"; + unsigned int len = 0; + int found = -1; + while (str[len]) { + if (str[len] == '&' && found < 0) { + found = len++; + continue; + } + if (found < 0) { + len++; + continue; + } + if (str[len] == '&') { + Debug(this,DebugNote,"Unescape. Duplicate '&' in expression [%p]",this); + setError(NotWellFormed); + return; + } + if (str[len] != ';') + len++; + else { // We have a candidate for escaping + len += 1; // Append ';' character + String aux(str + found,len - found); + char re = 0; + if (aux.startsWith("&#")) + if (aux.at(2) == 'x') + re = getHex(aux); + else + re = getDec(aux); + if (re == '&') { + if (str[len] == '#') { + aux = String(str + len,4); + if (aux == "#60;") { + re = '<'; + len += 4; + } + if (aux == "#38;") { + re = '&'; + len += 4; + } + } + } + else if (!re) + re = replace(aux,s_escape); + if (re) { // We have an valid escape character + buf << String(str,found) << re; + str += len; + len = 0; + found = -1; + } + else { + Debug(this,DebugNote,"Unescape. No replacement found for '%s' [%p]", + String(str + found,len - found).c_str(),this); + setError(NotWellFormed); + return; + } + } + } + if (found >= 0) { + Debug(this,DebugNote,"Unescape. Unexpected end of expression [%p]",this); + setError(NotWellFormed); + return; + } + if (len) { + if (str != text.c_str()) { + buf << String(str,len); + text = buf; + } + } + else + text = buf; +} + +// XmlEscape the given text +void XmlSaxParser::escape(String& buf, const String& text) +{ + const char* str = text.c_str(); + if (!str) + return; + char c; + while ((c = *str++)) { + const char* rep = replace(c,XmlSaxParser::s_escape); + if (!rep) { + buf += c; + continue; + } + buf += rep; + } +} + +// Check duplicate namespace values. Calls gotElement() if ok +// See http://www.w3.org/TR/xml-names/ +bool XmlSaxParser::processElement(NamedList& list, bool empty) +{ + unsigned int n = list.count(); + for (unsigned int i = 0; i < n; i++) { + NamedString* ns = list.getParam(i); + if (!(ns && XmlElement::isXmlns(ns->name()))) + continue; + for (unsigned int j = i + 1; j < n; j++) { + NamedString* nsCmp = list.getParam(j); + if (nsCmp && XmlElement::isXmlns(nsCmp->name()) && *ns == *nsCmp) { + Debug(this,DebugNote, + "Duplicate namespace value '%s' for attributes '%s' and '%s' [%p]", + ns->c_str(),ns->name().c_str(),nsCmp->name().c_str(),this); + return setError(NotWellFormed); + } + } + } + gotElement(list,empty); + resetParsed(); + return true; +} + + +/* + * XmlDomPareser + */ +XmlDomParser::XmlDomParser(const char* name, bool fragment) + : XmlSaxParser(name), + m_current(0), m_data(0) +{ + if (fragment) + m_data = new XmlFragment(); + else + m_data = new XmlDocument(); +} + +XmlDomParser::XmlDomParser(XmlParent* parent) + : m_current(0), m_data(0) +{ + m_data = parent; +} + +XmlDomParser::~XmlDomParser() +{ + reset(); +} + +// Create a new xml comment and append it in the xml three +void XmlDomParser::gotComment(const String& text) +{ + XmlComment* com = new XmlComment(text); + if (m_current) + setError(m_current->addChild(com),com); + else + setError(m_data->addChild(com),com); + +} + +// Append a new xml doctype to main xml parent +void XmlDomParser::gotDoctype(const String& doc) +{ + m_data->addChild(new XmlDoctype(doc)); +} + +// TODO implement it see what to do +void XmlDomParser::gotProcessing(const NamedString& instr) +{ + DDebug(this,DebugStub,"gotProcessing('---> %s <---') not implemented",instr.c_str()); +} + +// Create a new xml declaration, verifies the version and encoding +// and append it in the main xml parent +void XmlDomParser::gotDeclaration(const NamedList& decl) +{ + if (m_current) { + setError(DeclarationParse); + Debug(this,DebugNote,"Received declaration inside element bounds [%p]",this); + return; + } + Error err = NoError; + while (true) { + String* version = decl.getParam("version"); + if (version) { + int ver = version->substr(0,version->find('.')).toInteger(); + if (ver != 1) { + err = UnsupportedVersion; + break; + } + } + String* enc = decl.getParam("encoding"); + if (enc && !(*enc &= "utf-8")) { + err = UnsupportedEncoding; + break; + } + break; + } + if (err == NoError) { + XmlDeclaration* dec = new XmlDeclaration(decl); + setError(m_data->addChild(dec),dec); + } + else { + setError(err); + Debug(this,DebugNote, + "Received unacceptable declaration version='%s' encoding='%s' error '%s' [%p]", + decl.getValue("version"),decl.getValue("encoding"),getError(),this); + } +} + +// Create a new xml text and append it in the xml tree +void XmlDomParser::gotText(const String& text) +{ + XmlText* tet = new XmlText(text); + if (m_current) + m_current->addChild(tet); + else + setError(m_data->addChild(tet),tet); +} + +// Create a new xml Cdata and append it in the xml tree +void XmlDomParser::gotCdata(const String& data) +{ + XmlCData* cdata = new XmlCData(data); + if (!m_current) { + if (m_data->document()) { + Debug(this,DebugNote,"Document got CDATA outside element [%p]",this); + setError(NotWellFormed); + TelEngine::destruct(cdata); + return; + } + setError(m_data->addChild(cdata),cdata); + return; + } + setError(m_current->addChild(cdata),cdata); +} + +// Create a new xml element and append it in the xml tree +void XmlDomParser::gotElement(const NamedList& elem, bool empty) +{ + XmlElement* element = 0; + if (!m_current) { + // If we don't have curent element menns that the main fragment + // should hold it + element = new XmlElement(elem,empty); + setError(m_data->addChild(element),element); + if (!empty) + m_current = element; + } + else { + if (empty) { + element = new XmlElement(elem,empty); + m_current->addChild(element); + } + else { + element = new XmlElement(elem,empty,m_current); + m_current->addChild(element); + m_current = element; + } + } +} + +// Verify if is the closeing tag for the current element +// Complete th current element and make current the current parent +void XmlDomParser::endElement(const String& name) +{ + if (!m_current) { + setError(ReadingEndTag); + Debug(this,DebugNote,"Unexpected element end tag %s [%p]",name.c_str(),this); + return; + } + if (m_current->getName() != name) { + setError(ReadingEndTag); + DDebug(this,DebugNote, + "Received end element for %s, but the expected one is for %s [%p]", + name.c_str(),m_current->getName().c_str(),this); + return; + } + m_current->setCompleted(); + XDebug(this,DebugInfo,"End element for %s [%p]",m_current->getName().c_str(),this); + m_current = static_cast(m_current->getParent()); +} + +// Reset this parser +void XmlDomParser::reset() +{ + m_data->reset(); + m_current = 0; + XmlSaxParser::reset(); +} + + +/* + * XmlDeclaration + */ +// Create a new XmlDeclaration from version and encoding +XmlDeclaration::XmlDeclaration(const char* version, const char* enc) + : m_declaration("") +{ + XDebug(DebugAll,"XmlDeclaration::XmlDeclaration(%s,%s) [%p]",version,enc,this); + if (!TelEngine::null(version)) + m_declaration.addParam("version",version); + if (!TelEngine::null(enc)) + m_declaration.addParam("encoding",enc); +} + +// Constructor +XmlDeclaration::XmlDeclaration(const NamedList& decl) + : m_declaration(decl) +{ + XDebug(DebugAll,"XmlDeclaration::XmlDeclaration(%s) [%p]",m_declaration.c_str(),this); +} + +// Copy Constructor +XmlDeclaration::XmlDeclaration(const XmlDeclaration& decl) + : m_declaration(decl.getDec()) +{ +} + +// Destructor +XmlDeclaration::~XmlDeclaration() +{ + XDebug(DebugAll,"XmlDeclaration::~XmlDeclaration() ( %s| %p )", + m_declaration.c_str(),this); +} + +// Create a String from this Xml Declaration +void XmlDeclaration::toString(String& dump, bool esc) const +{ + dump << "name(); + dump << "=\""; + if (esc) + XmlSaxParser::escape(dump,*ns); + else + dump += *ns; + dump << "\""; + } + dump << "?>"; +} + + +/* + * XmlFragment + */ +// Constructor +XmlFragment::XmlFragment() + : m_list() +{ + XDebug(DebugAll,"XmlFragment::XmlFragment() ( %p )",this); +} + +// Copy Constructor +XmlFragment::XmlFragment(const XmlFragment& orig) +{ + ObjList* ob = orig.getChildren().skipNull(); + for (;ob;ob = ob->skipNext()) { + XmlChild* obj = static_cast(ob->get()); + if (obj->xmlElement()) { + XmlElement* el = obj->xmlElement(); + if (el) + addChild(new XmlElement(*el)); + continue; + } + else if (obj->xmlCData()) { + XmlCData* cdata = obj->xmlCData(); + if (cdata) + addChild(new XmlCData(*cdata)); + continue; + } + else if (obj->xmlText()) { + const XmlText* text = obj->xmlText(); + if (text) + addChild(new XmlText(*text)); + continue; + } + else if (obj->xmlComment()) { + XmlComment* comm = obj->xmlComment(); + if (comm) + addChild(new XmlComment(*comm)); + continue; + } + else if (obj->xmlDeclaration()) { + XmlDeclaration* decl = obj->xmlDeclaration(); + if (decl) + addChild(new XmlDeclaration(*decl)); + continue; + } + else if (obj->xmlDoctype()) { + XmlDoctype* doctype = obj->xmlDoctype(); + if (doctype) + addChild(new XmlDoctype(*doctype)); + continue; + } + } +} + +// Destructor +XmlFragment::~XmlFragment() +{ + m_list.clear(); + XDebug(DebugAll,"XmlFragment::~XmlFragment() ( %p )",this); +} + +// Reset. Clear children list +void XmlFragment::reset() +{ + m_list.clear(); +} + +// Append a new child +XmlSaxParser::Error XmlFragment::addChild(XmlChild* child) +{ + if (child) + m_list.append(child); + return XmlSaxParser::NoError; +} + +// Remove a child +XmlChild* XmlFragment::removeChild(XmlChild* child, bool delObj) +{ + XmlChild* ch = static_cast(m_list.remove(child,delObj)); + if (ch && ch->xmlElement()) + ch->xmlElement()->setParent(0); + return ch; +} + +// Create a String from this XmlFragment +void XmlFragment::toString(String& dump, bool escape, const String& indent, + const String& origindent, bool completeOnly, const String* auth, + const XmlElement* parent) const +{ + ObjList* ob = m_list.skipNull(); + if (!ob) + return; + for (;ob;ob = ob->skipNext()) { + XmlChild* obj = static_cast(ob->get()); + if (obj->xmlElement()) + obj->xmlElement()->toString(dump,escape,indent,origindent,completeOnly,auth); + else if (obj->xmlText()) + obj->xmlText()->toString(dump,escape,indent,auth,parent); + else if (obj->xmlCData()) + obj->xmlCData()->toString(dump,indent); + else if (obj->xmlComment()) + obj->xmlComment()->toString(dump,indent); + else if (obj->xmlDeclaration()) + obj->xmlDeclaration()->toString(dump,escape); + else if (obj->xmlDoctype()) + obj->xmlDoctype()->toString(dump,origindent); + else + Debug(DebugStub,"XmlFragment::toString() unhandled element type!"); + } +} + +// Find a completed xml element in a list +XmlElement* XmlFragment::findElement(ObjList* list, const String* name, const String* ns) +{ + XmlElement* e = 0; + for (; list; list = list->skipNext()) { + e = (static_cast(list->get()))->xmlElement(); + if (!(e && e->completed())) + continue; + if (name || ns) { + if (!ns) { + if (*name == e->toString()) + break; + } + else if (name) { + const String* t = 0; + const String* n = 0; + if (e->getTag(t,n) && *t == *name && n && *n == *ns) + break; + } + else { + const String* n = e->xmlns(); + if (n && *n == *ns) + break; + } + } + else + break; + e = 0; + } + return e; +} + + +/* + * XmlDocument + */ +// Constructor +XmlDocument::XmlDocument() + : m_root(0) +{ + +} + +// Destructor +XmlDocument::~XmlDocument() +{ + +} + +// Append a new child to this document +// Set the root to an XML element if not already set. If we already have a completed root +// the element will be added to the root, otherwise an error will be returned. +// If we don't have a root non xml elements (other then text) will be added the list +// of elements before root +XmlSaxParser::Error XmlDocument::addChild(XmlChild* child) +{ + if (!child) + return XmlSaxParser::NoError; + + XmlElement* element = child->xmlElement(); + if (!m_root) { + if (element) { + m_root = element; + return XmlSaxParser::NoError; + } + XmlDeclaration* decl = child->xmlDeclaration(); + if (decl && declaration()) { + DDebug(DebugNote,"XmlDocument. Request to add duplicate declaration [%p]",this); + return XmlSaxParser::NotWellFormed; + } + // Text outside root: ignore empty, raise error otherwise + XmlText* text = child->xmlText(); + if (text) { + String tmp(text->getText()); + tmp.trimSpaces(); + if (!tmp) { + TelEngine::destruct(child); + return XmlSaxParser::NoError; + } + Debug(DebugNote,"XmlDocument. Got text outside element [%p]",this); + return XmlSaxParser::NotWellFormed; + } + return m_beforeRoot.addChild(child); + } + // We have a root + if (element) { + if (m_root->completed()) + return m_root->addChild(child); + DDebug(DebugStub,"XmlDocument. Request to add xml element child to incomplete root [%p]",this); + return XmlSaxParser::NotWellFormed; + } + // TODO: check what xml we can add after the root or if we can add + // anything after an incomplete root + Debug(DebugStub,"XmlDocument. Request to add non element while having a root [%p]",this); + return XmlSaxParser::NotWellFormed; +} + +// Retrieve the document declaration +XmlDeclaration* XmlDocument::declaration() const +{ + for (ObjList* o = m_beforeRoot.getChildren().skipNull(); o; o = o->skipNull()) { + XmlDeclaration* d = (static_cast(o->get()))->xmlDeclaration(); + if (d) + return d; + } + return 0; +} + +// Obtain root element completed ot not +XmlElement* XmlDocument::root(bool completed) const +{ + return (m_root && (m_root->completed() || !completed)) ? m_root : 0; +} + +void XmlDocument::toString(String& dump, bool escape, const String& indent, const String& origindent) const +{ + m_beforeRoot.toString(dump,escape,indent,origindent); + if (m_root) { + dump << origindent; + m_root->toString(dump,escape,indent,origindent); + } +} + +// Reset this XmlDocument. Destroys root and clear the others xml objects +void XmlDocument::reset() +{ + TelEngine::destruct(m_root); + m_beforeRoot.clearChildren(); + m_file.clear(); +} + +// Load this document from data stream and parse it +XmlSaxParser::Error XmlDocument::read(Stream& in, int* error) +{ + XmlDomParser* parser = new XmlDomParser(this); + char buf[8096]; + while (true) { + int rd = in.readData(buf,sizeof(buf) - 1); + if (rd > 0) { + buf[rd] = 0; + if (parser->parse(buf) || parser->error() == XmlSaxParser::Incomplete) + continue; + break; + } + break; + } + if (parser->error() != XmlSaxParser::NoError) { + DDebug(DebugNote,"XmlDocument error loading stream. Parser error %d '%s' [%p]", + parser->error(),parser->getError(),this); + return parser->error(); + } + if (in.error()) { + if (error) + *error = in.error(); +#ifdef DEBUG + String tmp; + Thread::errorString(tmp,in.error()); + Debug(DebugNote,"XmlDocument error loading stream. I/O error %d '%s' [%p]", + in.error(),tmp.c_str(),this); +#endif + return XmlSaxParser::IOError; + } + return XmlSaxParser::NoError; +} + +// Write this document to a data stream +int XmlDocument::write(Stream& out, bool escape, const String& indent, + const String& origindent, bool completeOnly) const +{ + String dump; + m_beforeRoot.toString(dump,escape,indent,origindent); + if (m_root) + m_root->toString(dump,escape,indent,origindent,completeOnly); + return out.writeData(dump); +} + +// Load a file and parse it +XmlSaxParser::Error XmlDocument::loadFile(const char* file, int* error) +{ + reset(); + if (TelEngine::null(file)) + return XmlSaxParser::NoError; + m_file = file; + File f; + if (f.openPath(file)) + return read(f,error); + if (error) + *error = f.error(); +#ifdef DEBUG + String tmp; + Thread::errorString(tmp,f.error()); + Debug(DebugNote,"XmlDocument error opening file '%s': %d '%s' [%p]", + file,f.error(),tmp.c_str(),this); +#endif + return XmlSaxParser::IOError; +} + +// Save this xml document in a file +int XmlDocument::saveFile(const char* file, bool esc, const String& indent, + bool completeOnly) const +{ + if (!file) + file = m_file; + if (!file) + return 0; + File f; + int err = 0; + if (f.openPath(file,true,false,true,false)) { + String eol("\r\n"); + write(f,esc,eol,indent,completeOnly); + err = f.error(); + // Add an empty line + if (err >= 0) + f.writeData((void*)eol.c_str(),eol.length()); + } + else + err = f.error(); + if (!err) { + XDebug(DebugAll,"XmlDocument saved file '%s' [%p]",file,this); + return 0; + } +#ifdef DEBUG + String error; + Thread::errorString(error,err); + Debug(DebugNote,"Error saving XmlDocument to file '%s'. %d '%s' [%p]", + file,err,error.c_str(),this); +#endif + return f.error(); +} + + +/* + * XmlChild + */ +XmlChild::XmlChild() +{ +} + + +/* + * XmlElement + */ +XmlElement::XmlElement(const NamedList& element, bool empty, XmlParent* parent) + : m_element(element), m_prefixed(0), + m_parent(0), m_inheritedNs(0), + m_empty(empty), m_complete(empty) +{ + XDebug(DebugAll,"XmlElement::XmlElement(%s,%u,%p) [%p]", + element.c_str(),empty,parent,this); + setPrefixed(); + setParent(parent); +} + +// Copy constructor +XmlElement::XmlElement(const XmlElement& el) + : m_children(el.m_children), + m_element(el.getElement()), m_prefixed(0), + m_parent(0), m_inheritedNs(0), + m_empty(el.empty()), m_complete(el.completed()) +{ + setPrefixed(); + setInheritedNs(&el,true); +} + +// Create an empty xml element +XmlElement::XmlElement(const char* name, bool complete) + : m_element(name), m_prefixed(0), + m_parent(0), m_inheritedNs(0), + m_empty(true), m_complete(complete) +{ + setPrefixed(); + XDebug(DebugAll,"XmlElement::XmlElement(%s) [%p]", + m_element.c_str(),this); +} + +// Destructor +XmlElement::~XmlElement() +{ + setInheritedNs(); + XDebug(DebugAll,"XmlElement::~XmlElement() ( %s| %p )", + m_element.c_str(),this); +} + +// Set inherited namespaces from a given element. Reset them anyway +void XmlElement::setInheritedNs(const XmlElement* xml, bool inherit) +{ + XDebug(DebugAll,"XmlElement(%s) setInheritedNs(%p,%s) [%p]", + tag(),xml,String::boolText(inherit),this); + TelEngine::destruct(m_inheritedNs); + if (!xml) + return; + addInheritedNs(xml->attributes()); + if (!inherit) + return; + XmlElement* p = xml->parent(); + bool xmlAdd = (p == 0); + while (p) { + addInheritedNs(p->attributes()); + const NamedList* i = p->inheritedNs(); + p = p->parent(); + if (!p && i) + addInheritedNs(*i); + } + if (xmlAdd && xml->inheritedNs()) + addInheritedNs(*xml->inheritedNs()); +} + +// Add inherited namespaces from a list +void XmlElement::addInheritedNs(const NamedList& list) +{ + XDebug(DebugAll,"XmlElement(%s) addInheritedNs(%s) [%p]",tag(),list.c_str(),this); + unsigned int n = list.count(); + for (unsigned int i = 0; i < n; i++) { + NamedString* ns = list.getParam(i); + if (!(ns && isXmlns(ns->name()))) + continue; + // Avoid adding already overridden namespaces + if (m_element.getParam(ns->name())) + continue; + if (m_inheritedNs && m_inheritedNs->getParam(ns->name())) + continue; + // Avoid adding duplicate namespace values + // See http://www.w3.org/TR/xml-names/ + if (findByValue(m_element,*ns,s_nsPrefix,s_ns)) + continue; + if (m_inheritedNs && findByValue(*m_inheritedNs,*ns,s_nsPrefix,s_ns)) + continue; + if (!m_inheritedNs) + m_inheritedNs = new NamedList(""); + XDebug(DebugAll,"XmlElement(%s) adding inherited %s=%s [%p]", + tag(),ns->name().c_str(),ns->c_str(),this); + m_inheritedNs->addParam(ns->name(),*ns); + } +} + +// Obtain the first text of this xml element +const String& XmlElement::getText() +{ + XmlText* txt = 0; + for (ObjList* ob = getChildren().skipNull(); ob && !txt; ob = ob->skipNext()) + txt = (static_cast(ob->get()))->xmlText(); + return txt ? txt->getText() : String::empty(); +} + +// Add a text child +void XmlElement::addText(const char* text) +{ + if (!TelEngine::null(text)) + addChild(new XmlText(text)); +} + +// Retrieve the element's tag (without prefix) and namespace +bool XmlElement::getTag(const String*& tag, const String*& ns) const +{ + if (!m_prefixed) { + tag = &m_element; + ns = xmlns(); + return true; + } + // Prefixed element + tag = &m_prefixed->name(); + ns = xmlns(); + return ns != 0; +} + +// Append a new child +XmlSaxParser::Error XmlElement::addChild(XmlChild* child) +{ + if (!child) + return XmlSaxParser::NoError; + XmlSaxParser::Error err = m_children.addChild(child); + if (err == XmlSaxParser::NoError) + child->setParent(this); + return err; +} + +// Remove a child +XmlChild* XmlElement::removeChild(XmlChild* child, bool delObj) +{ + return m_children.removeChild(child,delObj); +} + +// Set this element's parent. Update inherited namespaces +void XmlElement::setParent(XmlParent* parent) +{ + XDebug(DebugAll,"XmlElement(%s) setParent(%p) element=%s [%p]", + tag(),parent,String::boolText(parent != 0),this); + if (m_parent && m_parent->element()) { + // Reset inherited namespaces if the new parent is an element + // Otherwise set them from the old parent + if (parent && parent->element()) + setInheritedNs(0); + else + setInheritedNs(m_parent->element()); + } + m_parent = parent; +} + +// Obtain a string from this xml element +void XmlElement::toString(String& dump, bool esc, const String& indent, + const String& origindent, bool completeOnly, const String* auth) const +{ + XDebug(DebugAll,"XmlElement(%s) toString(%u,%s,%s,%u,%p) complete=%u [%p]", + tag(),esc,indent.c_str(),origindent.c_str(),completeOnly,auth,m_complete,this); + if (!m_complete && completeOnly) + return; + String auxDump; + auxDump << indent << "<" << m_element; + String newIndent(indent + origindent); + const char* sep = newIndent ? newIndent.c_str() : " "; + int n = m_element.count(); + for (int i = 0; i < n; i++) { + NamedString* ns = m_element.getParam(i); + if (!ns) + continue; + auxDump << sep << ns->name() << "=\""; + addAuth(auxDump,ns->name(),*ns,esc,auth); + auxDump << "\""; + } + int m = getChildren().count(); + if (n) + auxDump << indent; + if (m_complete && !m) + auxDump << "/"; + auxDump << ">"; + if (m) { + m_children.toString(auxDump,esc,newIndent,origindent,completeOnly,auth,this); + if (m_complete) + auxDump << indent << ""; + } + dump << auxDump; +} + +// Retrieve a namespace attribute. Search in parent or inherited for it +String* XmlElement::xmlnsAttribute(const String& name) const +{ + String* tmp = getAttribute(name); + if (tmp) + return tmp; + XmlElement* p = parent(); + if (p) + return p->xmlnsAttribute(name); + return m_inheritedNs ? m_inheritedNs->getParam(name) : 0; +} + +// Set the element's namespace +bool XmlElement::setXmlns(const String& name, bool addAttr, const String& value) +{ + const String* cmp = name ? &name : &s_ns; + XDebug(DebugAll,"XmlElement(%s)::setXmlns(%s,%u,%s) [%p]", + tag(),cmp->c_str(),addAttr,value.c_str(),this); + if (*cmp == s_ns) { + if (m_prefixed) { + m_element.assign(m_prefixed->name()); + setPrefixed(); + // TODO: remove children and attributes prefixes + } + } + else if (!m_prefixed || *m_prefixed != cmp) { + if (!m_prefixed) + m_element.assign(*cmp + ":" + tag()); + else + m_element.assign(*cmp + ":" + m_prefixed->name()); + setPrefixed(); + // TODO: change children and attributes prefixes + } + if (!(addAttr && value)) + return true; + // Check non repeating value + String attr; + if (*cmp == s_ns) + attr = s_ns; + else + attr << s_nsPrefix << *cmp; + NamedString* ns = findByValue(m_element,value,s_nsPrefix,s_ns); + if (ns) + return ns->name() == attr; + if (m_inheritedNs) { + ns = findByValue(*m_inheritedNs,value,s_nsPrefix,s_ns); + if (ns) { + if (ns->name() == attr) + m_inheritedNs->clearParam(attr); + else + return false; + } + } + m_element.addParam(attr,value); + return true; +} + + +/* + * XmlComment + */ +// Constructor +XmlComment::XmlComment(const String& comm) + : m_comment(comm) +{ + XDebug(DebugAll,"XmlComment::XmlComment(const String& comm) ( %s| %p )", + m_comment.c_str(),this); +} + +// Copy Constructor +XmlComment::XmlComment(const XmlComment& comm) + : m_comment(comm.getComment()) +{ +} + +// Destructor +XmlComment::~XmlComment() +{ + XDebug(DebugAll,"XmlComment::~XmlComment() ( %s| %p )", + m_comment.c_str(),this); +} + +// Obtain string representation of this xml comment +void XmlComment::toString(String& dump, const String& indent) const +{ + dump << indent << ""; +} + + +/* + * XmlCData + */ +// Constructor +XmlCData::XmlCData(const String& data) + : m_data(data) +{ + XDebug(DebugAll,"XmlCData::XmlCData(const String& data) ( %s| %p )", + m_data.c_str(),this); +} + +// Copy Constructor +XmlCData::XmlCData(const XmlCData& data) + : m_data(data.getCData()) +{ +} + +// Destructor +XmlCData::~XmlCData() +{ + XDebug(DebugAll,"XmlCData::~XmlCData() ( %s| %p )", + m_data.c_str(),this); +} + +// Obtain string representation of this xml Cdata +void XmlCData::toString(String& dump, const String& indent) const +{ + dump << indent << ""; +} + + +/* + * XmlText + */ +// Constructor +XmlText::XmlText(const String& text) + : m_text(text) +{ + XDebug(DebugAll,"XmlText::XmlText(%s) [%p]",m_text.c_str(),this); +} + +// Copy Constructor +XmlText::XmlText(const XmlText& text) + : m_text(text.getText()) +{ + XDebug(DebugAll,"XmlText::XmlText(%p,%s) [%p]", + &text,TelEngine::c_safe(text.getText()),this); +} + +// Destructor +XmlText::~XmlText() +{ + XDebug(DebugAll,"XmlText::~XmlText [%p]",this); +} + +// Obtain string representation of this xml text +void XmlText::toString(String& dump, bool esc, const String& indent, + const String* auth, const XmlElement* parent) const +{ + dump << indent; + if (auth) + addAuth(dump,parent ? parent->toString() : String::empty(),m_text,esc,auth); + else + dump << m_text; +} + + +/* + * XmlDoctype + */ +// Constructor +XmlDoctype::XmlDoctype(const String& doctype) + : m_doctype(doctype) +{ + XDebug(DebugAll,"XmlDoctype::XmlDoctype(const String& doctype) ( %s| %p )", + m_doctype.c_str(),this); +} + +// Copy Constructor +XmlDoctype::XmlDoctype(const XmlDoctype& doctype) + : m_doctype(doctype.getDoctype()) +{ +} + +// Destructor +XmlDoctype::~XmlDoctype() +{ + XDebug(DebugAll,"XmlDoctype::~XmlDoctype() ( %s| %p )", + m_doctype.c_str(),this); +} + +// Obtain string representation of this xml doctype +void XmlDoctype::toString(String& dump, const String& indent) const +{ + dump << indent << ""; +} + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/libs/yxml/tinystr.cpp b/libs/yxml/tinystr.cpp deleted file mode 100644 index 4a5dc4c8..00000000 --- a/libs/yxml/tinystr.cpp +++ /dev/null @@ -1,117 +0,0 @@ -/* -www.sourceforge.net/projects/tinyxml -Original file by Yves Berquin. - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -/* - * THIS FILE WAS ALTERED BY Tyge Løvset, 7. April 2005. - */ - - -#ifndef TIXML_USE_STL - -#include "tinystr.h" - -using namespace TelEngine; - -// Error value for find primitive -const TiXmlString::size_type TiXmlString::npos = static_cast< size_type >(-1); - -// Null rep. -TiXmlString::Rep TiXmlString::nullrep_ = { 0, 0, '\0' }; - - -void TiXmlString::reserve (size_type cap) -{ - if (cap > capacity()) - { - TiXmlString tmp; - tmp.init(length(), cap); - memcpy(tmp.start(), data(), length()); - swap(tmp); - } -} - - -TiXmlString& TiXmlString::assign(const char* str, size_type len) -{ - size_type cap = capacity(); - if (len > cap || cap > 3*(len + 8)) - { - TiXmlString tmp; - tmp.init(len); - memcpy(tmp.start(), str, len); - swap(tmp); - } - else - { - memmove(start(), str, len); - set_size(len); - } - return *this; -} - - -TiXmlString& TiXmlString::append(const char* str, size_type len) -{ - size_type newsize = length() + len; - if (newsize > capacity()) - { - reserve (newsize + capacity()); - } - memmove(finish(), str, len); - set_size(newsize); - return *this; -} - - -TiXmlString operator + (const TiXmlString & a, const TiXmlString & b) -{ - TiXmlString tmp; - tmp.reserve(a.length() + b.length()); - tmp += a; - tmp += b; - return tmp; -} - -TiXmlString operator + (const TiXmlString & a, const char* b) -{ - TiXmlString tmp; - TiXmlString::size_type b_len = static_cast( strlen(b) ); - tmp.reserve(a.length() + b_len); - tmp += a; - tmp.append(b, b_len); - return tmp; -} - -TiXmlString operator + (const char* a, const TiXmlString & b) -{ - TiXmlString tmp; - TiXmlString::size_type a_len = static_cast( strlen(a) ); - tmp.reserve(a_len + b.length()); - tmp.append(a, a_len); - tmp += b; - return tmp; -} - - -#endif // TIXML_USE_STL diff --git a/libs/yxml/tinystr.h b/libs/yxml/tinystr.h deleted file mode 100644 index 20d2f601..00000000 --- a/libs/yxml/tinystr.h +++ /dev/null @@ -1,342 +0,0 @@ -/* -www.sourceforge.net/projects/tinyxml -Original file by Yves Berquin. - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -/* - * THIS FILE WAS ALTERED BY Tyge Lovset, 7. April 2005. - * - * - completely rewritten. compact, clean, and fast implementation. - * - sizeof(TiXmlString) = pointer size (4 bytes on 32-bit systems) - * - fixed reserve() to work as per specification. - * - fixed buggy compares operator==(), operator<(), and operator>() - * - fixed operator+=() to take a const ref argument, following spec. - * - added "copy" constructor with length, and most compare operators. - * - added swap(), clear(), size(), capacity(), operator+(). - */ - -#ifdef _WINDOWS - -#ifdef LIBYXML_EXPORTS -#define YXML_API __declspec(dllexport) -#else -#ifndef LIBYXML_STATIC -#define YXML_API __declspec(dllimport) -#endif -#endif - -#endif /* _WINDOWS */ - -#ifndef YXML_API -#define YXML_API -#endif - -/** - * Holds all Telephony Engine related classes. - */ -namespace TelEngine { - -#ifndef TIXML_USE_STL - -#ifndef TIXML_STRING_INCLUDED -#define TIXML_STRING_INCLUDED - -#include -#include - -/* The support for explicit isn't that universal, and it isn't really - required - it is used to check that the TiXmlString class isn't incorrectly - used. Be nice to old compilers and macro it here: -*/ -#if defined(_MSC_VER) && (_MSC_VER >= 1200 ) - // Microsoft visual studio, version 6 and higher. - #define TIXML_EXPLICIT explicit -#elif defined(__GNUC__) && (__GNUC__ >= 3 ) - // GCC version 3 and higher.s - #define TIXML_EXPLICIT explicit -#else - #define TIXML_EXPLICIT -#endif - - -/* - TiXmlString is an emulation of a subset of the std::string template. - Its purpose is to allow compiling TinyXML on compilers with no or poor STL support. - Only the member functions relevant to the TinyXML project have been implemented. - The buffer allocation is made by a simplistic power of 2 like mechanism : if we increase - a string and there's no more room, we allocate a buffer twice as big as we need. -*/ -class YXML_API TiXmlString -{ - public : - // The size type used - typedef size_t size_type; - - // Error value for find primitive - static const size_type npos; // = -1; - - - // TiXmlString empty constructor - TiXmlString () : rep_(&nullrep_) - { - } - - // TiXmlString copy constructor - TiXmlString ( const TiXmlString & copy) - { - init(copy.length()); - memcpy(start(), copy.data(), length()); - } - - // TiXmlString constructor, based on a string - TIXML_EXPLICIT TiXmlString ( const char * copy) - { - init( static_cast( strlen(copy) )); - memcpy(start(), copy, length()); - } - - // TiXmlString constructor, based on a string - TIXML_EXPLICIT TiXmlString ( const char * str, size_type len) - { - init(len); - memcpy(start(), str, len); - } - - // TiXmlString destructor - ~TiXmlString () - { - quit(); - } - - // = operator - TiXmlString& operator = (const char * copy) - { - return assign( copy, (size_type)strlen(copy)); - } - - // = operator - TiXmlString& operator = (const TiXmlString & copy) - { - return assign(copy.start(), copy.length()); - } - - - // += operator. Maps to append - TiXmlString& operator += (const char * suffix) - { - return append(suffix, static_cast( strlen(suffix) )); - } - - // += operator. Maps to append - TiXmlString& operator += (char single) - { - return append(&single, 1); - } - - // += operator. Maps to append - TiXmlString& operator += (const TiXmlString & suffix) - { - return append(suffix.data(), suffix.length()); - } - - - // Convert a TiXmlString into a null-terminated char * - const char * c_str () const { return rep_->str; } - - // Convert a TiXmlString into a char * (need not be null terminated). - const char * data () const { return rep_->str; } - - // Return the length of a TiXmlString - size_type length () const { return rep_->size; } - - // Alias for length() - size_type size () const { return rep_->size; } - - // Checks if a TiXmlString is empty - bool empty () const { return rep_->size == 0; } - - // Return capacity of string - size_type capacity () const { return rep_->capacity; } - - - // single char extraction - const char& at (size_type index) const - { - assert( index < length() ); - return rep_->str[ index ]; - } - - // [] operator - char& operator [] (size_type index) const - { - assert( index < length() ); - return rep_->str[ index ]; - } - - // find a char in a string. Return TiXmlString::npos if not found - size_type find (char lookup) const - { - return find(lookup, 0); - } - - // find a char in a string from an offset. Return TiXmlString::npos if not found - size_type find (char tofind, size_type offset) const - { - if (offset >= length()) return npos; - - for (const char* p = c_str() + offset; *p != '\0'; ++p) - { - if (*p == tofind) return static_cast< size_type >( p - c_str() ); - } - return npos; - } - - void clear () - { - //Lee: - //The original was just too strange, though correct: - // TiXmlString().swap(*this); - //Instead use the quit & re-init: - quit(); - init(0,0); - } - - /* Function to reserve a big amount of data when we know we'll need it. Be aware that this - function DOES NOT clear the content of the TiXmlString if any exists. - */ - void reserve (size_type cap); - - TiXmlString& assign (const char* str, size_type len); - - TiXmlString& append (const char* str, size_type len); - - void swap (TiXmlString& other) - { - Rep* r = rep_; - rep_ = other.rep_; - other.rep_ = r; - } - - private: - - void init(size_type sz) { init(sz, sz); } - void set_size(size_type sz) { rep_->str[ rep_->size = sz ] = '\0'; } - char* start() const { return rep_->str; } - char* finish() const { return rep_->str + rep_->size; } - - struct Rep - { - size_type size, capacity; - char str[1]; - }; - - void init(size_type sz, size_type cap) - { - if (cap) - { - // Lee: the original form: - // rep_ = static_cast(operator new(sizeof(Rep) + cap)); - // doesn't work in some cases of new being overloaded. Switching - // to the normal allocation, although use an 'int' for systems - // that are overly picky about structure alignment. - const size_type bytesNeeded = sizeof(Rep) + cap; - const size_type intsNeeded = ( bytesNeeded + sizeof(int) - 1 ) / sizeof( int ); - rep_ = reinterpret_cast( new int[ intsNeeded ] ); - - rep_->str[ rep_->size = sz ] = '\0'; - rep_->capacity = cap; - } - else - { - rep_ = &nullrep_; - } - } - - void quit() - { - if (rep_ != &nullrep_) - { - // The rep_ is really an array of ints. (see the allocator, above). - // Cast it back before delete, so the compiler won't incorrectly call destructors. - delete [] ( reinterpret_cast( rep_ ) ); - } - } - - Rep * rep_; - static Rep nullrep_; - -} ; - - -inline bool operator == (const TiXmlString & a, const TiXmlString & b) -{ - return ( a.length() == b.length() ) // optimization on some platforms - && ( strcmp(a.c_str(), b.c_str()) == 0 ); // actual compare -} -inline bool operator < (const TiXmlString & a, const TiXmlString & b) -{ - return strcmp(a.c_str(), b.c_str()) < 0; -} - -inline bool operator != (const TiXmlString & a, const TiXmlString & b) { return !(a == b); } -inline bool operator > (const TiXmlString & a, const TiXmlString & b) { return b < a; } -inline bool operator <= (const TiXmlString & a, const TiXmlString & b) { return !(b < a); } -inline bool operator >= (const TiXmlString & a, const TiXmlString & b) { return !(a < b); } - -inline bool operator == (const TiXmlString & a, const char* b) { return strcmp(a.c_str(), b) == 0; } -inline bool operator == (const char* a, const TiXmlString & b) { return b == a; } -inline bool operator != (const TiXmlString & a, const char* b) { return !(a == b); } -inline bool operator != (const char* a, const TiXmlString & b) { return !(b == a); } - -TiXmlString operator + (const TiXmlString & a, const TiXmlString & b); -TiXmlString operator + (const TiXmlString & a, const char* b); -TiXmlString operator + (const char* a, const TiXmlString & b); - - -/* - TiXmlOutStream is an emulation of std::ostream. It is based on TiXmlString. - Only the operators that we need for TinyXML have been developped. -*/ -class YXML_API TiXmlOutStream : public TiXmlString -{ -public : - - // TiXmlOutStream << operator. - TiXmlOutStream & operator << (const TiXmlString & in) - { - *this += in; - return *this; - } - - // TiXmlOutStream << operator. - TiXmlOutStream & operator << (const char * in) - { - *this += in; - return *this; - } - -}; - -}; // TelEngine namespace - -#endif // TIXML_STRING_INCLUDED -#endif // TIXML_USE_STL diff --git a/libs/yxml/tinyxml.cpp b/libs/yxml/tinyxml.cpp deleted file mode 100644 index d00c8272..00000000 --- a/libs/yxml/tinyxml.cpp +++ /dev/null @@ -1,1816 +0,0 @@ -/* -www.sourceforge.net/projects/tinyxml -Original code (2.0 and earlier )copyright (c) 2000-2002 Lee Thomason (www.grinninglizard.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include - -#ifdef TIXML_USE_STL -#include -#include -#endif - -using namespace TelEngine; - -bool TiXmlBase::condenseWhiteSpace = true; - -void TiXmlBase::PutString( const TIXML_STRING& str, TIXML_OSTREAM* stream ) -{ - TIXML_STRING buffer; - PutString( str, &buffer ); - (*stream) << buffer; -} - -void TiXmlBase::PutString( const TIXML_STRING& str, TIXML_STRING* outString ) -{ - int i=0; - - while( i<(int)str.length() ) - { - unsigned char c = (unsigned char) str[i]; - - if ( c == '&' - && i < ( (int)str.length() - 2 ) - && str[i+1] == '#' - && str[i+2] == 'x' ) - { - // Hexadecimal character reference. - // Pass through unchanged. - // © -- copyright symbol, for example. - // - // The -1 is a bug fix from Rob Laveaux. It keeps - // an overflow from happening if there is no ';'. - // There are actually 2 ways to exit this loop - - // while fails (error case) and break (semicolon found). - // However, there is no mechanism (currently) for - // this function to return an error. - while ( i<(int)str.length()-1 ) - { - outString->append( str.c_str() + i, 1 ); - ++i; - if ( str[i] == ';' ) - break; - } - } - else if ( c == '&' ) - { - outString->append( entity[0].str, entity[0].strLength ); - ++i; - } - else if ( c == '<' ) - { - outString->append( entity[1].str, entity[1].strLength ); - ++i; - } - else if ( c == '>' ) - { - outString->append( entity[2].str, entity[2].strLength ); - ++i; - } - else if ( c == '\"' ) - { - outString->append( entity[3].str, entity[3].strLength ); - ++i; - } - else if ( c == '\'' ) - { - outString->append( entity[4].str, entity[4].strLength ); - ++i; - } - else if ( c < 32 ) - { - // Easy pass at non-alpha/numeric/symbol - // Below 32 is symbolic. - char buf[ 32 ]; - - #if defined(TIXML_SNPRINTF) - TIXML_SNPRINTF( buf, sizeof(buf), "&#x%02X;", (unsigned) ( c & 0xff ) ); - #else - sprintf( buf, "&#x%02X;", (unsigned) ( c & 0xff ) ); - #endif - - //*ME: warning C4267: convert 'size_t' to 'int' - //*ME: Int-Cast to make compiler happy ... - outString->append( buf, (int)strlen( buf ) ); - ++i; - } - else - { - //char realc = (char) c; - //outString->append( &realc, 1 ); - *outString += (char) c; // somewhat more efficient function call. - ++i; - } - } -} - - -// <-- Strange class for a bug fix. Search for STL_STRING_BUG -TiXmlBase::StringToBuffer::StringToBuffer( const TIXML_STRING& str ) -{ - buffer = new char[ str.length()+1 ]; - if ( buffer ) - { - ::strcpy( buffer, str.c_str() ); - } -} - - -TiXmlBase::StringToBuffer::~StringToBuffer() -{ - delete [] buffer; -} -// End strange bug fix. --> - - -TiXmlNode::TiXmlNode( NodeType _type ) : TiXmlBase() -{ - parent = 0; - type = _type; - firstChild = 0; - lastChild = 0; - prev = 0; - next = 0; -} - - -TiXmlNode::~TiXmlNode() -{ - TiXmlNode* node = firstChild; - TiXmlNode* temp = 0; - - while ( node ) - { - temp = node; - node = node->next; - delete temp; - } -} - - -void TiXmlNode::CopyTo( TiXmlNode* target ) const -{ - target->SetValue (value.c_str() ); - target->userData = userData; -} - - -void TiXmlNode::Clear() -{ - TiXmlNode* node = firstChild; - TiXmlNode* temp = 0; - - while ( node ) - { - temp = node; - node = node->next; - delete temp; - } - - firstChild = 0; - lastChild = 0; -} - - -TiXmlNode* TiXmlNode::LinkEndChild( TiXmlNode* node ) -{ - assert( node->parent == 0 || node->parent == this ); - assert( node->GetDocument() == 0 || node->GetDocument() == this->GetDocument() ); - - node->parent = this; - - node->prev = lastChild; - node->next = 0; - - if ( lastChild ) - lastChild->next = node; - else - firstChild = node; // it was an empty list. - - lastChild = node; - return node; -} - - -TiXmlNode* TiXmlNode::InsertEndChild( const TiXmlNode& addThis ) -{ - TiXmlNode* node = addThis.Clone(); - if ( !node ) - return 0; - - return LinkEndChild( node ); -} - - -TiXmlNode* TiXmlNode::InsertBeforeChild( TiXmlNode* beforeThis, const TiXmlNode& addThis ) -{ - if ( !beforeThis || beforeThis->parent != this ) - return 0; - - TiXmlNode* node = addThis.Clone(); - if ( !node ) - return 0; - node->parent = this; - - node->next = beforeThis; - node->prev = beforeThis->prev; - if ( beforeThis->prev ) - { - beforeThis->prev->next = node; - } - else - { - assert( firstChild == beforeThis ); - firstChild = node; - } - beforeThis->prev = node; - return node; -} - - -TiXmlNode* TiXmlNode::InsertAfterChild( TiXmlNode* afterThis, const TiXmlNode& addThis ) -{ - if ( !afterThis || afterThis->parent != this ) - return 0; - - TiXmlNode* node = addThis.Clone(); - if ( !node ) - return 0; - node->parent = this; - - node->prev = afterThis; - node->next = afterThis->next; - if ( afterThis->next ) - { - afterThis->next->prev = node; - } - else - { - assert( lastChild == afterThis ); - lastChild = node; - } - afterThis->next = node; - return node; -} - - -TiXmlNode* TiXmlNode::ReplaceChild( TiXmlNode* replaceThis, const TiXmlNode& withThis ) -{ - if ( replaceThis->parent != this ) - return 0; - - TiXmlNode* node = withThis.Clone(); - if ( !node ) - return 0; - - node->next = replaceThis->next; - node->prev = replaceThis->prev; - - if ( replaceThis->next ) - replaceThis->next->prev = node; - else - lastChild = node; - - if ( replaceThis->prev ) - replaceThis->prev->next = node; - else - firstChild = node; - - delete replaceThis; - node->parent = this; - return node; -} - -#if 0 -bool TiXmlNode::RemoveChild( TiXmlNode* removeThis ) - -Modified: - Added an optional parameter to delete the node after removing it from the childrens -#endif -bool TiXmlNode::RemoveChild( TiXmlNode* removeThis, bool del ) -{ - if ( removeThis->parent != this ) - { - assert( 0 ); - return false; - } - - if ( removeThis->next ) - removeThis->next->prev = removeThis->prev; - else - lastChild = removeThis->prev; - - if ( removeThis->prev ) - removeThis->prev->next = removeThis->next; - else - firstChild = removeThis->next; - // Delete ? - if (del) - delete removeThis; - else - { - // Reset links - removeThis->parent = 0; - removeThis->prev = 0; - removeThis->next = 0; - } - return true; -} - -const TiXmlNode* TiXmlNode::FirstChild( const char * _value ) const -{ - const TiXmlNode* node; - for ( node = firstChild; node; node = node->next ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - - -TiXmlNode* TiXmlNode::FirstChild( const char * _value ) -{ - TiXmlNode* node; - for ( node = firstChild; node; node = node->next ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - - -const TiXmlNode* TiXmlNode::LastChild( const char * _value ) const -{ - const TiXmlNode* node; - for ( node = lastChild; node; node = node->prev ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - -TiXmlNode* TiXmlNode::LastChild( const char * _value ) -{ - TiXmlNode* node; - for ( node = lastChild; node; node = node->prev ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - -const TiXmlNode* TiXmlNode::IterateChildren( const TiXmlNode* previous ) const -{ - if ( !previous ) - { - return FirstChild(); - } - else - { - assert( previous->parent == this ); - return previous->NextSibling(); - } -} - -TiXmlNode* TiXmlNode::IterateChildren( TiXmlNode* previous ) -{ - if ( !previous ) - { - return FirstChild(); - } - else - { - assert( previous->parent == this ); - return previous->NextSibling(); - } -} - -const TiXmlNode* TiXmlNode::IterateChildren( const char * val, const TiXmlNode* previous ) const -{ - if ( !previous ) - { - return FirstChild( val ); - } - else - { - assert( previous->parent == this ); - return previous->NextSibling( val ); - } -} - -TiXmlNode* TiXmlNode::IterateChildren( const char * val, TiXmlNode* previous ) -{ - if ( !previous ) - { - return FirstChild( val ); - } - else - { - assert( previous->parent == this ); - return previous->NextSibling( val ); - } -} - -const TiXmlNode* TiXmlNode::NextSibling( const char * _value ) const -{ - const TiXmlNode* node; - for ( node = next; node; node = node->next ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - -TiXmlNode* TiXmlNode::NextSibling( const char * _value ) -{ - TiXmlNode* node; - for ( node = next; node; node = node->next ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - -const TiXmlNode* TiXmlNode::PreviousSibling( const char * _value ) const -{ - const TiXmlNode* node; - for ( node = prev; node; node = node->prev ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - -TiXmlNode* TiXmlNode::PreviousSibling( const char * _value ) -{ - TiXmlNode* node; - for ( node = prev; node; node = node->prev ) - { - if ( strcmp( node->Value(), _value ) == 0 ) - return node; - } - return 0; -} - -void TiXmlElement::RemoveAttribute( const char * name ) -{ - TIXML_STRING str( name ); - TiXmlAttribute* node = attributeSet.Find( str ); - if ( node ) - { - attributeSet.Remove( node ); - delete node; - } -} - -const TiXmlElement* TiXmlNode::FirstChildElement() const -{ - const TiXmlNode* node; - - for ( node = FirstChild(); - node; - node = node->NextSibling() ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - -TiXmlElement* TiXmlNode::FirstChildElement() -{ - TiXmlNode* node; - - for ( node = FirstChild(); - node; - node = node->NextSibling() ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - -const TiXmlElement* TiXmlNode::FirstChildElement( const char * _value ) const -{ - const TiXmlNode* node; - - for ( node = FirstChild( _value ); - node; - node = node->NextSibling( _value ) ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - -TiXmlElement* TiXmlNode::FirstChildElement( const char * _value ) -{ - TiXmlNode* node; - - for ( node = FirstChild( _value ); - node; - node = node->NextSibling( _value ) ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - -const TiXmlElement* TiXmlNode::NextSiblingElement() const -{ - const TiXmlNode* node; - - for ( node = NextSibling(); - node; - node = node->NextSibling() ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - -TiXmlElement* TiXmlNode::NextSiblingElement() -{ - TiXmlNode* node; - - for ( node = NextSibling(); - node; - node = node->NextSibling() ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - -const TiXmlElement* TiXmlNode::NextSiblingElement( const char * _value ) const -{ - const TiXmlNode* node; - - for ( node = NextSibling( _value ); - node; - node = node->NextSibling( _value ) ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - -TiXmlElement* TiXmlNode::NextSiblingElement( const char * _value ) -{ - TiXmlNode* node; - - for ( node = NextSibling( _value ); - node; - node = node->NextSibling( _value ) ) - { - if ( node->ToElement() ) - return node->ToElement(); - } - return 0; -} - - -const TiXmlDocument* TiXmlNode::GetDocument() const -{ - const TiXmlNode* node; - - for( node = this; node; node = node->parent ) - { - if ( node->ToDocument() ) - return node->ToDocument(); - } - return 0; -} - -TiXmlDocument* TiXmlNode::GetDocument() -{ - TiXmlNode* node; - - for( node = this; node; node = node->parent ) - { - if ( node->ToDocument() ) - return node->ToDocument(); - } - return 0; -} - -TiXmlElement::TiXmlElement (const char * _value) - : TiXmlNode( TiXmlNode::ELEMENT ) -{ - firstChild = lastChild = 0; - value = _value; -} - - -#ifdef TIXML_USE_STL -TiXmlElement::TiXmlElement( const std::string& _value ) - : TiXmlNode( TiXmlNode::ELEMENT ) -{ - firstChild = lastChild = 0; - value = _value; -} -#endif - - -TiXmlElement::TiXmlElement( const TiXmlElement& copy) - : TiXmlNode( TiXmlNode::ELEMENT ) -{ - firstChild = lastChild = 0; - copy.CopyTo( this ); -} - - -void TiXmlElement::operator=( const TiXmlElement& base ) -{ - ClearThis(); - base.CopyTo( this ); -} - - -TiXmlElement::~TiXmlElement() -{ - ClearThis(); -} - - -void TiXmlElement::ClearThis() -{ - Clear(); - while( attributeSet.First() ) - { - TiXmlAttribute* node = attributeSet.First(); - attributeSet.Remove( node ); - delete node; - } -} - - -const char * TiXmlElement::Attribute( const char * name ) const -{ - TIXML_STRING str( name ); - const TiXmlAttribute* node = attributeSet.Find( str ); - - if ( node ) - return node->Value(); - - return 0; -} - - -const char * TiXmlElement::Attribute( const char * name, int* i ) const -{ - const char * s = Attribute( name ); - if ( i ) - { - if ( s ) - *i = atoi( s ); - else - *i = 0; - } - return s; -} - - -const char * TiXmlElement::Attribute( const char * name, double* d ) const -{ - const char * s = Attribute( name ); - if ( d ) - { - if ( s ) - *d = atof( s ); - else - *d = 0; - } - return s; -} - - -int TiXmlElement::QueryIntAttribute( const char* name, int* ival ) const -{ - TIXML_STRING str( name ); - const TiXmlAttribute* node = attributeSet.Find( str ); - if ( !node ) - return TIXML_NO_ATTRIBUTE; - - return node->QueryIntValue( ival ); -} - - -int TiXmlElement::QueryDoubleAttribute( const char* name, double* dval ) const -{ - TIXML_STRING str( name ); - const TiXmlAttribute* node = attributeSet.Find( str ); - if ( !node ) - return TIXML_NO_ATTRIBUTE; - - return node->QueryDoubleValue( dval ); -} - - -void TiXmlElement::SetAttribute( const char * name, int val ) -{ - char buf[64]; - #if defined(TIXML_SNPRINTF) - TIXML_SNPRINTF( buf, sizeof(buf), "%d", val ); - #else - sprintf( buf, "%d", val ); - #endif - SetAttribute( name, buf ); -} - - -#ifdef TIXML_USE_STL -void TiXmlElement::SetAttribute( const std::string& name, int val ) -{ - std::ostringstream oss; - oss << val; - SetAttribute( name, oss.str() ); -} -#endif - - -void TiXmlElement::SetDoubleAttribute( const char * name, double val ) -{ - char buf[256]; - #if defined(TIXML_SNPRINTF) - TIXML_SNPRINTF( buf, sizeof(buf), "%f", val ); - #else - sprintf( buf, "%f", val ); - #endif - SetAttribute( name, buf ); -} - - -void TiXmlElement::SetAttribute( const char * cname, const char * cvalue ) -{ - TIXML_STRING _name( cname ); - TIXML_STRING _value( cvalue ); - - TiXmlAttribute* node = attributeSet.Find( _name ); - if ( node ) - { - node->SetValue( cvalue ); - return; - } - - TiXmlAttribute* attrib = new TiXmlAttribute( cname, cvalue ); - if ( attrib ) - { - attributeSet.Add( attrib ); - } - else - { - TiXmlDocument* document = GetDocument(); - if ( document ) document->SetError( TIXML_ERROR_OUT_OF_MEMORY, 0, 0, TIXML_ENCODING_UNKNOWN ); - } -} - - -#ifdef TIXML_USE_STL -void TiXmlElement::SetAttribute( const std::string& name, const std::string& _value ) -{ - TiXmlAttribute* node = attributeSet.Find( name ); - if ( node ) - { - node->SetValue( _value ); - return; - } - - TiXmlAttribute* attrib = new TiXmlAttribute( name, _value ); - if ( attrib ) - { - attributeSet.Add( attrib ); - } - else - { - TiXmlDocument* document = GetDocument(); - if ( document ) document->SetError( TIXML_ERROR_OUT_OF_MEMORY, 0, 0, TIXML_ENCODING_UNKNOWN ); - } -} -#endif - - -void TiXmlElement::Print( FILE* cfile, int depth ) const -{ - int i; - for ( i=0; iNext() ) - { - fprintf( cfile, " " ); - attrib->Print( cfile, depth ); - } - - // There are 3 different formatting approaches: - // 1) An element without children is printed as a node - // 2) An element with only a text child is printed as text - // 3) An element with children is printed on multiple lines. - TiXmlNode* node; - if ( !firstChild ) - { - fprintf( cfile, " />" ); - } - else if ( firstChild == lastChild && firstChild->ToText() ) - { - fprintf( cfile, ">" ); - firstChild->Print( cfile, depth + 1 ); - fprintf( cfile, "", value.c_str() ); - } - else - { - fprintf( cfile, ">" ); - - for ( node = firstChild; node; node=node->NextSibling() ) - { - if ( !node->ToText() ) - { - fprintf( cfile, "\n" ); - } - node->Print( cfile, depth+1 ); - } - fprintf( cfile, "\n" ); - for( i=0; i", value.c_str() ); - } -} - -void TiXmlElement::StreamOut( TIXML_OSTREAM * stream, bool unclosed ) const -{ - (*stream) << "<" << value; - - const TiXmlAttribute* attrib; - for ( attrib = attributeSet.First(); attrib; attrib = attrib->Next() ) - { - (*stream) << " "; - attrib->StreamOut( stream ); - } - - // If this node has children, give it a closing tag. Else - // make it an empty tag. - TiXmlNode* node; - if ( firstChild ) - { - (*stream) << ">"; - - for ( node = firstChild; node; node=node->NextSibling() ) - { - node->StreamOut( stream ); - } - (*stream) << (unclosed ? "<" : ""; - } - else - { - if ( unclosed ) - (*stream) << " >"; - else - (*stream) << " />"; - } -} - - -void TiXmlElement::CopyTo( TiXmlElement* target ) const -{ - // superclass: - TiXmlNode::CopyTo( target ); - - // Element class: - // Clone the attributes, then clone the children. - const TiXmlAttribute* attribute = 0; - for( attribute = attributeSet.First(); - attribute; - attribute = attribute->Next() ) - { - target->SetAttribute( attribute->Name(), attribute->Value() ); - } - - TiXmlNode* node = 0; - for ( node = firstChild; node; node = node->NextSibling() ) - { - target->LinkEndChild( node->Clone() ); - } -} - - -TiXmlNode* TiXmlElement::Clone() const -{ - TiXmlElement* clone = new TiXmlElement( Value() ); - if ( !clone ) - return 0; - - CopyTo( clone ); - return clone; -} - - -const char* TiXmlElement::GetText() const -{ - const TiXmlNode* child = this->FirstChild(); - if ( child ) { - const TiXmlText* childText = child->ToText(); - if ( childText ) { - return childText->Value(); - } - } - return 0; -} - - -TiXmlDocument::TiXmlDocument() : TiXmlNode( TiXmlNode::DOCUMENT ) -{ - tabsize = 4; - useMicrosoftBOM = false; - ClearError(); -} - -TiXmlDocument::TiXmlDocument( const char * documentName ) : TiXmlNode( TiXmlNode::DOCUMENT ) -{ - tabsize = 4; - useMicrosoftBOM = false; - value = documentName; - ClearError(); -} - - -#ifdef TIXML_USE_STL -TiXmlDocument::TiXmlDocument( const std::string& documentName ) : TiXmlNode( TiXmlNode::DOCUMENT ) -{ - tabsize = 4; - useMicrosoftBOM = false; - value = documentName; - ClearError(); -} -#endif - - -TiXmlDocument::TiXmlDocument( const TiXmlDocument& copy ) : TiXmlNode( TiXmlNode::DOCUMENT ) -{ - copy.CopyTo( this ); -} - - -void TiXmlDocument::operator=( const TiXmlDocument& copy ) -{ - Clear(); - copy.CopyTo( this ); -} - - -bool TiXmlDocument::LoadFile( TiXmlEncoding encoding ) -{ - // See STL_STRING_BUG below. - StringToBuffer buf( value ); - - if ( buf.buffer && LoadFile( buf.buffer, encoding ) ) - return true; - - return false; -} - - -bool TiXmlDocument::SaveFile() const -{ - // See STL_STRING_BUG below. - StringToBuffer buf( value ); - - if ( buf.buffer && SaveFile( buf.buffer ) ) - return true; - - return false; -} - -bool TiXmlDocument::LoadFile( const char* filename, TiXmlEncoding encoding ) -{ - // There was a really terrifying little bug here. The code: - // value = filename - // in the STL case, cause the assignment method of the std::string to - // be called. What is strange, is that the std::string had the same - // address as it's c_str() method, and so bad things happen. Looks - // like a bug in the Microsoft STL implementation. - // See STL_STRING_BUG above. - // Fixed with the StringToBuffer class. - value = filename; - - // reading in binary mode so that tinyxml can normalize the EOL - FILE* file = fopen( value.c_str (), "rb" ); - - if ( file ) - { - bool result = LoadFile( file, encoding ); - fclose( file ); - return result; - } - else - { - SetError( TIXML_ERROR_OPENING_FILE, 0, 0, TIXML_ENCODING_UNKNOWN ); - return false; - } -} - -bool TiXmlDocument::LoadFile( FILE* file, TiXmlEncoding encoding ) -{ - if ( !file ) - { - SetError( TIXML_ERROR_OPENING_FILE, 0, 0, TIXML_ENCODING_UNKNOWN ); - return false; - } - - // Delete the existing data: - Clear(); - location.Clear(); - - // Get the file size, so we can pre-allocate the string. HUGE speed impact. - long length = 0; - fseek( file, 0, SEEK_END ); - length = ftell( file ); - fseek( file, 0, SEEK_SET ); - - // Strange case, but good to handle up front. - if ( length == 0 ) - { - SetError( TIXML_ERROR_DOCUMENT_EMPTY, 0, 0, TIXML_ENCODING_UNKNOWN ); - return false; - } - - // If we have a file, assume it is all one big XML file, and read it in. - // The document parser may decide the document ends sooner than the entire file, however. - TIXML_STRING data; - data.reserve( length ); - - // Subtle bug here. TinyXml did use fgets. But from the XML spec: - // 2.11 End-of-Line Handling - // - // - // ...the XML processor MUST behave as if it normalized all line breaks in external - // parsed entities (including the document entity) on input, before parsing, by translating - // both the two-character sequence #xD #xA and any #xD that is not followed by #xA to - // a single #xA character. - // - // - // It is not clear fgets does that, and certainly isn't clear it works cross platform. - // Generally, you expect fgets to translate from the convention of the OS to the c/unix - // convention, and not work generally. - - /* - while( fgets( buf, sizeof(buf), file ) ) - { - data += buf; - } - */ - - char* buf = new char[ length+1 ]; - buf[0] = 0; - - if ( fread( buf, length, 1, file ) != 1 ) { - SetError( TIXML_ERROR_OPENING_FILE, 0, 0, TIXML_ENCODING_UNKNOWN ); - return false; - } - - const char* lastPos = buf; - const char* p = buf; - - buf[length] = 0; - while( *p ) { - assert( p < (buf+length) ); - if ( *p == 0xa ) { - // Newline character. No special rules for this. Append all the characters - // since the last string, and include the newline. - data.append( lastPos, (p-lastPos+1) ); // append, include the newline - ++p; // move past the newline - lastPos = p; // and point to the new buffer (may be 0) - assert( p <= (buf+length) ); - } - else if ( *p == 0xd ) { - // Carriage return. Append what we have so far, then - // handle moving forward in the buffer. - if ( (p-lastPos) > 0 ) { - data.append( lastPos, p-lastPos ); // do not add the CR - } - data += (char)0xa; // a proper newline - - if ( *(p+1) == 0xa ) { - // Carriage return - new line sequence - p += 2; - lastPos = p; - assert( p <= (buf+length) ); - } - else { - // it was followed by something else...that is presumably characters again. - ++p; - lastPos = p; - assert( p <= (buf+length) ); - } - } - else { - ++p; - } - } - // Handle any left over characters. - if ( p-lastPos ) { - data.append( lastPos, p-lastPos ); - } - delete [] buf; - buf = 0; - - Parse( data.c_str(), 0, encoding ); - - if ( Error() ) - return false; - else - return true; -} - - -bool TiXmlDocument::SaveFile( const char * filename ) const -{ - // The old c stuff lives on... - FILE* fp = fopen( filename, "w" ); - if ( fp ) - { - bool result = SaveFile( fp ); - fclose( fp ); - return result; - } - return false; -} - - -bool TiXmlDocument::SaveFile( FILE* fp ) const -{ - if ( useMicrosoftBOM ) - { - const unsigned char TIXML_UTF_LEAD_0 = 0xefU; - const unsigned char TIXML_UTF_LEAD_1 = 0xbbU; - const unsigned char TIXML_UTF_LEAD_2 = 0xbfU; - - fputc( TIXML_UTF_LEAD_0, fp ); - fputc( TIXML_UTF_LEAD_1, fp ); - fputc( TIXML_UTF_LEAD_2, fp ); - } - Print( fp, 0 ); - return true; -} - - -void TiXmlDocument::CopyTo( TiXmlDocument* target ) const -{ - TiXmlNode::CopyTo( target ); - - target->error = error; - target->errorDesc = errorDesc.c_str (); - - TiXmlNode* node = 0; - for ( node = firstChild; node; node = node->NextSibling() ) - { - target->LinkEndChild( node->Clone() ); - } -} - - -TiXmlNode* TiXmlDocument::Clone() const -{ - TiXmlDocument* clone = new TiXmlDocument(); - if ( !clone ) - return 0; - - CopyTo( clone ); - return clone; -} - - -void TiXmlDocument::Print( FILE* cfile, int depth ) const -{ - const TiXmlNode* node; - for ( node=FirstChild(); node; node=node->NextSibling() ) - { - node->Print( cfile, depth ); - fprintf( cfile, "\n" ); - } -} - -void TiXmlDocument::StreamOut( TIXML_OSTREAM * out, bool unclosed ) const -{ - const TiXmlNode* node; - for ( node=FirstChild(); node; node=node->NextSibling() ) - { - node->StreamOut( out ); - - // Special rule for streams: stop after the root element. - // The stream in code will only read one element, so don't - // write more than one. - if ( node->ToElement() ) - break; - } -} - - -const TiXmlAttribute* TiXmlAttribute::Next() const -{ - // We are using knowledge of the sentinel. The sentinel - // have a value or name. - if ( next->value.empty() && next->name.empty() ) - return 0; - return next; -} - -TiXmlAttribute* TiXmlAttribute::Next() -{ - // We are using knowledge of the sentinel. The sentinel - // have a value or name. - if ( next->value.empty() && next->name.empty() ) - return 0; - return next; -} - -const TiXmlAttribute* TiXmlAttribute::Previous() const -{ - // We are using knowledge of the sentinel. The sentinel - // have a value or name. - if ( prev->value.empty() && prev->name.empty() ) - return 0; - return prev; -} - -TiXmlAttribute* TiXmlAttribute::Previous() -{ - // We are using knowledge of the sentinel. The sentinel - // have a value or name. - if ( prev->value.empty() && prev->name.empty() ) - return 0; - return prev; -} - -void TiXmlAttribute::Print( FILE* cfile, int /*depth*/ ) const -{ - TIXML_STRING n, v; - - PutString( name, &n ); - PutString( value, &v ); - - if (value.find ('\"') == TIXML_STRING::npos) - fprintf (cfile, "%s=\"%s\"", n.c_str(), v.c_str() ); - else - fprintf (cfile, "%s='%s'", n.c_str(), v.c_str() ); -} - - -void TiXmlAttribute::StreamOut( TIXML_OSTREAM * stream, bool unclosed ) const -{ - if (value.find( '\"' ) != TIXML_STRING::npos) - { - PutString( name, stream ); - (*stream) << "=" << "'"; - PutString( value, stream ); - (*stream) << "'"; - } - else - { - PutString( name, stream ); - (*stream) << "=" << "\""; - PutString( value, stream ); - (*stream) << "\""; - } -} - -int TiXmlAttribute::QueryIntValue( int* ival ) const -{ - if ( sscanf( value.c_str(), "%d", ival ) == 1 ) - return TIXML_SUCCESS; - return TIXML_WRONG_TYPE; -} - -int TiXmlAttribute::QueryDoubleValue( double* dval ) const -{ - if ( sscanf( value.c_str(), "%lf", dval ) == 1 ) - return TIXML_SUCCESS; - return TIXML_WRONG_TYPE; -} - -void TiXmlAttribute::SetIntValue( int _value ) -{ - char buf [64]; - #if defined(TIXML_SNPRINTF) - TIXML_SNPRINTF(buf, sizeof(buf), "%d", _value); - #else - sprintf (buf, "%d", _value); - #endif - SetValue (buf); -} - -void TiXmlAttribute::SetDoubleValue( double _value ) -{ - char buf [256]; - #if defined(TIXML_SNPRINTF) - TIXML_SNPRINTF( buf, sizeof(buf), "%lf", _value); - #else - sprintf (buf, "%lf", _value); - #endif - SetValue (buf); -} - -int TiXmlAttribute::IntValue() const -{ - return atoi (value.c_str ()); -} - -double TiXmlAttribute::DoubleValue() const -{ - return atof (value.c_str ()); -} - - -TiXmlComment::TiXmlComment( const TiXmlComment& copy ) : TiXmlNode( TiXmlNode::COMMENT ) -{ - copy.CopyTo( this ); -} - - -void TiXmlComment::operator=( const TiXmlComment& base ) -{ - Clear(); - base.CopyTo( this ); -} - - -void TiXmlComment::Print( FILE* cfile, int depth ) const -{ - for ( int i=0; i", value.c_str() ); -} - -void TiXmlComment::StreamOut( TIXML_OSTREAM * stream, bool unclosed ) const -{ - (*stream) << ""; -} - - -void TiXmlComment::CopyTo( TiXmlComment* target ) const -{ - TiXmlNode::CopyTo( target ); -} - - -TiXmlNode* TiXmlComment::Clone() const -{ - TiXmlComment* clone = new TiXmlComment(); - - if ( !clone ) - return 0; - - CopyTo( clone ); - return clone; -} - - -void TiXmlText::Print( FILE* cfile, int depth ) const -{ - if ( cdata ) - { - int i; - fprintf( cfile, "\n" ); - for ( i=0; i\n" ); - } - else - { - TIXML_STRING buffer; - PutString( value, &buffer ); - fprintf( cfile, "%s", buffer.c_str() ); - } -} - - -void TiXmlText::StreamOut( TIXML_OSTREAM * stream, bool unclosed ) const -{ - if ( cdata ) - { - (*stream) << ""; - } - else - { - PutString( value, stream ); - } -} - - -void TiXmlText::CopyTo( TiXmlText* target ) const -{ - TiXmlNode::CopyTo( target ); - target->cdata = cdata; -} - - -TiXmlNode* TiXmlText::Clone() const -{ - TiXmlText* clone = 0; - clone = new TiXmlText( "" ); - - if ( !clone ) - return 0; - - CopyTo( clone ); - return clone; -} - - -TiXmlDeclaration::TiXmlDeclaration( const char * _version, - const char * _encoding, - const char * _standalone ) - : TiXmlNode( TiXmlNode::DECLARATION ) -{ - version = _version; - encoding = _encoding; - standalone = _standalone; -} - - -#ifdef TIXML_USE_STL -TiXmlDeclaration::TiXmlDeclaration( const std::string& _version, - const std::string& _encoding, - const std::string& _standalone ) - : TiXmlNode( TiXmlNode::DECLARATION ) -{ - version = _version; - encoding = _encoding; - standalone = _standalone; -} -#endif - - -TiXmlDeclaration::TiXmlDeclaration( const TiXmlDeclaration& copy ) - : TiXmlNode( TiXmlNode::DECLARATION ) -{ - copy.CopyTo( this ); -} - - -void TiXmlDeclaration::operator=( const TiXmlDeclaration& copy ) -{ - Clear(); - copy.CopyTo( this ); -} - - -void TiXmlDeclaration::Print( FILE* cfile, int /*depth*/ ) const -{ - fprintf (cfile, ""); -} - -void TiXmlDeclaration::StreamOut( TIXML_OSTREAM * stream, bool unclosed ) const -{ - (*stream) << ""; -} - - -void TiXmlDeclaration::CopyTo( TiXmlDeclaration* target ) const -{ - TiXmlNode::CopyTo( target ); - - target->version = version; - target->encoding = encoding; - target->standalone = standalone; -} - - -TiXmlNode* TiXmlDeclaration::Clone() const -{ - TiXmlDeclaration* clone = new TiXmlDeclaration(); - - if ( !clone ) - return 0; - - CopyTo( clone ); - return clone; -} - - -void TiXmlUnknown::Print( FILE* cfile, int depth ) const -{ - for ( int i=0; i", value.c_str() ); -} - - -void TiXmlUnknown::StreamOut( TIXML_OSTREAM * stream, bool unclosed ) const -{ - (*stream) << "<" << value << ">"; // Don't use entities here! It is unknown. -} - - -void TiXmlUnknown::CopyTo( TiXmlUnknown* target ) const -{ - TiXmlNode::CopyTo( target ); -} - - -TiXmlNode* TiXmlUnknown::Clone() const -{ - TiXmlUnknown* clone = new TiXmlUnknown(); - - if ( !clone ) - return 0; - - CopyTo( clone ); - return clone; -} - - -TiXmlAttributeSet::TiXmlAttributeSet() -{ - sentinel.next = &sentinel; - sentinel.prev = &sentinel; -} - - -TiXmlAttributeSet::~TiXmlAttributeSet() -{ - assert( sentinel.next == &sentinel ); - assert( sentinel.prev == &sentinel ); -} - - -void TiXmlAttributeSet::Add( TiXmlAttribute* addMe ) -{ - assert( !Find( TIXML_STRING( addMe->Name() ) ) ); // Shouldn't be multiply adding to the set. - - addMe->next = &sentinel; - addMe->prev = sentinel.prev; - - sentinel.prev->next = addMe; - sentinel.prev = addMe; -} - -void TiXmlAttributeSet::Remove( TiXmlAttribute* removeMe ) -{ - TiXmlAttribute* node; - - for( node = sentinel.next; node != &sentinel; node = node->next ) - { - if ( node == removeMe ) - { - node->prev->next = node->next; - node->next->prev = node->prev; - node->next = 0; - node->prev = 0; - return; - } - } - assert( 0 ); // we tried to remove a non-linked attribute. -} - -const TiXmlAttribute* TiXmlAttributeSet::Find( const TIXML_STRING& name ) const -{ - const TiXmlAttribute* node; - - for( node = sentinel.next; node != &sentinel; node = node->next ) - { - if ( node->name == name ) - return node; - } - return 0; -} - -TiXmlAttribute* TiXmlAttributeSet::Find( const TIXML_STRING& name ) -{ - TiXmlAttribute* node; - - for( node = sentinel.next; node != &sentinel; node = node->next ) - { - if ( node->name == name ) - return node; - } - return 0; -} - -#ifdef TIXML_USE_STL -TIXML_ISTREAM & operator >> (TIXML_ISTREAM & in, TiXmlNode & base) -{ - TIXML_STRING tag; - tag.reserve( 8 * 1000 ); - base.StreamIn( &in, &tag ); - - base.Parse( tag.c_str(), 0, TIXML_DEFAULT_ENCODING ); - return in; -} -#endif - - -TIXML_OSTREAM & operator<< (TIXML_OSTREAM & out, const TiXmlNode & base) -{ - base.StreamOut (& out); - return out; -} - - -#ifdef TIXML_USE_STL -std::string & operator<< (std::string& out, const TiXmlNode& base ) -{ - std::ostringstream os_stream( std::ostringstream::out ); - base.StreamOut( &os_stream ); - - out.append( os_stream.str() ); - return out; -} -#endif - - -TiXmlHandle TiXmlHandle::FirstChild() const -{ - if ( node ) - { - TiXmlNode* child = node->FirstChild(); - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} - - -TiXmlHandle TiXmlHandle::FirstChild( const char * value ) const -{ - if ( node ) - { - TiXmlNode* child = node->FirstChild( value ); - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} - - -TiXmlHandle TiXmlHandle::FirstChildElement() const -{ - if ( node ) - { - TiXmlElement* child = node->FirstChildElement(); - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} - - -TiXmlHandle TiXmlHandle::FirstChildElement( const char * value ) const -{ - if ( node ) - { - TiXmlElement* child = node->FirstChildElement( value ); - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} - - -TiXmlHandle TiXmlHandle::Child( int count ) const -{ - if ( node ) - { - int i; - TiXmlNode* child = node->FirstChild(); - for ( i=0; - child && iNextSibling(), ++i ) - { - // nothing - } - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} - - -TiXmlHandle TiXmlHandle::Child( const char* value, int count ) const -{ - if ( node ) - { - int i; - TiXmlNode* child = node->FirstChild( value ); - for ( i=0; - child && iNextSibling( value ), ++i ) - { - // nothing - } - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} - - -TiXmlHandle TiXmlHandle::ChildElement( int count ) const -{ - if ( node ) - { - int i; - TiXmlElement* child = node->FirstChildElement(); - for ( i=0; - child && iNextSiblingElement(), ++i ) - { - // nothing - } - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} - - -TiXmlHandle TiXmlHandle::ChildElement( const char* value, int count ) const -{ - if ( node ) - { - int i; - TiXmlElement* child = node->FirstChildElement( value ); - for ( i=0; - child && iNextSiblingElement( value ), ++i ) - { - // nothing - } - if ( child ) - return TiXmlHandle( child ); - } - return TiXmlHandle( 0 ); -} diff --git a/libs/yxml/tinyxml.h b/libs/yxml/tinyxml.h deleted file mode 100644 index fa8eb4c2..00000000 --- a/libs/yxml/tinyxml.h +++ /dev/null @@ -1,1596 +0,0 @@ -/* -www.sourceforge.net/projects/tinyxml -Original code (2.0 and earlier )copyright (c) 2000-2002 Lee Thomason (www.grinninglizard.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#ifndef TINYXML_INCLUDED -#define TINYXML_INCLUDED - -#include - -#ifdef _MSC_VER -#pragma warning( push ) -#pragma warning( disable : 4530 ) -#pragma warning( disable : 4786 ) -#endif - -#include -#include -#include -#include -#include - -// Help out windows: -#if defined( _DEBUG ) && !defined( DEBUG ) -#define DEBUG -#endif - -#ifdef TIXML_USE_STL - #include - #include - #define TIXML_STRING std::string - #define TIXML_ISTREAM std::istream - #define TIXML_OSTREAM std::ostream -#else - #include "tinystr.h" - #define TIXML_STRING TiXmlString - #define TIXML_OSTREAM TiXmlOutStream -#endif - -// Deprecated library function hell. Compilers want to use the -// new safe versions. This probably doesn't fully address the problem, -// but it gets closer. There are too many compilers for me to fully -// test. If you get compilation troubles, undefine TIXML_SAFE - -#define TIXML_SAFE // TinyXml isn't fully buffer overrun protected, safe code. This is work in progress. -#ifdef TIXML_SAFE - #if defined(_MSC_VER) && (_MSC_VER >= 1400 ) - // Microsoft visual studio, version 2005 and higher. - #define TIXML_SNPRINTF _snprintf_s - #define TIXML_SNSCANF _snscanf_s - #elif defined(_MSC_VER) && (_MSC_VER >= 1200 ) - // Microsoft visual studio, version 6 and higher. - //#pragma message( "Using _sn* functions." ) - #define TIXML_SNPRINTF _snprintf - #define TIXML_SNSCANF _snscanf - #elif defined(__GNUC__) && (__GNUC__ >= 3 ) - // GCC version 3 and higher.s - //#warning( "Using sn* functions." ) - #define TIXML_SNPRINTF snprintf - #define TIXML_SNSCANF snscanf - #endif -#endif - -#ifdef _WINDOWS - -#ifdef LIBYXML_EXPORTS -#define YXML_API __declspec(dllexport) -#else -#ifndef LIBYXML_STATIC -#define YXML_API __declspec(dllimport) -#endif -#endif - -#endif /* _WINDOWS */ - -#ifndef YXML_API -#define YXML_API -#endif - -/** - * Holds all Telephony Engine related classes. - */ -namespace TelEngine { - -class TiXmlDocument; -class TiXmlElement; -class TiXmlComment; -class TiXmlUnknown; -class TiXmlAttribute; -class TiXmlText; -class TiXmlDeclaration; -class TiXmlParsingData; - -const int TIXML_MAJOR_VERSION = 2; -const int TIXML_MINOR_VERSION = 4; -const int TIXML_PATCH_VERSION = 3; - -/* Internal structure for tracking location of items - in the XML file. -*/ -struct YXML_API TiXmlCursor -{ - TiXmlCursor() { Clear(); } - void Clear() { row = col = -1; } - - int row; // 0 based. - int col; // 0 based. -}; - - -// Only used by Attribute::Query functions -enum -{ - TIXML_SUCCESS, - TIXML_NO_ATTRIBUTE, - TIXML_WRONG_TYPE -}; - - -// Used by the parsing routines. -enum TiXmlEncoding -{ - TIXML_ENCODING_UNKNOWN, - TIXML_ENCODING_UTF8, - TIXML_ENCODING_LEGACY -}; - -const TiXmlEncoding TIXML_DEFAULT_ENCODING = TIXML_ENCODING_UNKNOWN; - -/** TiXmlBase is a base class for every class in TinyXml. - It does little except to establish that TinyXml classes - can be printed and provide some utility functions. - - In XML, the document and elements can contain - other elements and other types of nodes. - - @verbatim - A Document can contain: Element (container or leaf) - Comment (leaf) - Unknown (leaf) - Declaration( leaf ) - - An Element can contain: Element (container or leaf) - Text (leaf) - Attributes (not on tree) - Comment (leaf) - Unknown (leaf) - - A Decleration contains: Attributes (not on tree) - @endverbatim -*/ -class YXML_API TiXmlBase -{ - friend class TiXmlNode; - friend class TiXmlElement; - friend class TiXmlDocument; - -public: - TiXmlBase() : userData(0) {} - virtual ~TiXmlBase() {} - - /** All TinyXml classes can print themselves to a filestream. - This is a formatted print, and will insert tabs and newlines. - - (For an unformatted stream, use the << operator.) - */ - virtual void Print( FILE* cfile, int depth ) const = 0; - - /** The world does not agree on whether white space should be kept or - not. In order to make everyone happy, these global, static functions - are provided to set whether or not TinyXml will condense all white space - into a single space or not. The default is to condense. Note changing this - values is not thread safe. - */ - static void SetCondenseWhiteSpace( bool condense ) { condenseWhiteSpace = condense; } - - /// Return the current white space setting. - static bool IsWhiteSpaceCondensed() { return condenseWhiteSpace; } - - /** Return the position, in the original source file, of this node or attribute. - The row and column are 1-based. (That is the first row and first column is - 1,1). If the returns values are 0 or less, then the parser does not have - a row and column value. - - Generally, the row and column value will be set when the TiXmlDocument::Load(), - TiXmlDocument::LoadFile(), or any TiXmlNode::Parse() is called. It will NOT be set - when the DOM was created from operator>>. - - The values reflect the initial load. Once the DOM is modified programmatically - (by adding or changing nodes and attributes) the new values will NOT update to - reflect changes in the document. - - There is a minor performance cost to computing the row and column. Computation - can be disabled if TiXmlDocument::SetTabSize() is called with 0 as the value. - - @sa TiXmlDocument::SetTabSize() - */ - int Row() const { return location.row + 1; } - int Column() const { return location.col + 1; } ///< See Row() - - void SetUserData( void* user ) { userData = user; } - void* GetUserData() { return userData; } - - // Table that returs, for a given lead byte, the total number of bytes - // in the UTF-8 sequence. - static const int utf8ByteTable[256]; - - - - virtual const char* Parse(const char* p, - TiXmlParsingData* data, - TiXmlEncoding encoding /*= TIXML_ENCODING_UNKNOWN */ ) = 0; - - enum - { - TIXML_NO_ERROR = 0, - TIXML_ERROR, - TIXML_ERROR_OPENING_FILE, - TIXML_ERROR_OUT_OF_MEMORY, - TIXML_ERROR_PARSING_ELEMENT, - TIXML_ERROR_FAILED_TO_READ_ELEMENT_NAME, - TIXML_ERROR_READING_ELEMENT_VALUE, - TIXML_ERROR_READING_ATTRIBUTES, - TIXML_ERROR_PARSING_EMPTY, - TIXML_ERROR_READING_END_TAG, - TIXML_ERROR_PARSING_UNKNOWN, - TIXML_ERROR_PARSING_COMMENT, - TIXML_ERROR_PARSING_DECLARATION, - TIXML_ERROR_DOCUMENT_EMPTY, - TIXML_ERROR_EMBEDDED_NULL, - TIXML_ERROR_PARSING_CDATA, - TIXML_ERROR_INCOMPLETE, - TIXML_ERROR_BUFFEROVERRUN, - - TIXML_ERROR_STRING_COUNT - }; - -protected: - - // See STL_STRING_BUG - // Utility class to overcome a bug. - class StringToBuffer - { - public: - StringToBuffer( const TIXML_STRING& str ); - ~StringToBuffer(); - char* buffer; - }; - - static const char* SkipWhiteSpace( const char*, TiXmlEncoding encoding ); - inline static bool IsWhiteSpace( char c ) - { - return ( isspace( (unsigned char) c ) || c == '\n' || c == '\r' ); - } - inline static bool IsWhiteSpace( int c ) - { - if ( c < 256 ) - return IsWhiteSpace( (char) c ); - return false; // Again, only truly correct for English/Latin...but usually works. - } - -public: - virtual void StreamOut (TIXML_OSTREAM *, bool = false) const = 0; - -protected: - #ifdef TIXML_USE_STL - static bool StreamWhiteSpace( TIXML_ISTREAM * in, TIXML_STRING * tag ); - static bool StreamTo( TIXML_ISTREAM * in, int character, TIXML_STRING * tag ); - #endif - - /* Reads an XML name into the string provided. Returns - a pointer just past the last character of the name, - or 0 if p or *p are 0. - */ - static const char* ReadName( const char* p, TIXML_STRING* name, TiXmlEncoding encoding ); - - /* Reads text. Returns a pointer past the given end tag. - Wickedly complex options, but it keeps the (sensitive) code in one place. - *** Return 0 if end tag is not found. - *** Set eof to true if data ended before end tag. - */ - static const char* ReadText(const char* in, // where to start - TIXML_STRING* text, // the string read - bool ignoreWhiteSpace, // whether to keep the white space - const char* endTag, // what ends this text - bool ignoreCase, // whether to ignore case in the end tag - TiXmlEncoding encoding, // the current encoding - bool* eof = 0); // Optional flag set to true if data finishes before endTag - - // If an entity has been found, transform it into a character. - static const char* GetEntity( const char* in, char* value, int* length, TiXmlEncoding encoding ); - - // Get a character, while interpreting entities. - // The length can be from 0 to 4 bytes. - inline static const char* GetChar( const char* p, char* _value, int* length, TiXmlEncoding encoding ) - { - assert( p ); - if ( encoding == TIXML_ENCODING_UTF8 ) - { - *length = utf8ByteTable[ *((unsigned char*)p) ]; - assert( *length >= 0 && *length < 5 ); - } - else - { - *length = 1; - } - - if ( *length == 1 ) - { - if ( *p == '&' ) - return GetEntity( p, _value, length, encoding ); - *_value = *p; - return p+1; - } - else if ( *length ) - { - //strncpy( _value, p, *length ); - // lots of compilers don't like this function (unsafe), - // and the null terminator isn't needed - for( int i=0; p[i] && i<*length; ++i ) { - _value[i] = p[i]; - } - return p + (*length); - } - else - { - // Not valid text. - return 0; - } - } - - // Puts a string to a stream, expanding entities as it goes. - // Note this should not contian the '<', '>', etc, or they will be transformed into entities! - static void PutString( const TIXML_STRING& str, TIXML_OSTREAM* out ); - - static void PutString( const TIXML_STRING& str, TIXML_STRING* out ); - - // Return true if the next characters in the stream are any of the endTag sequences. - // Ignore case only works for english, and should only be relied on when comparing - // to English words: StringEqual( p, "version", true ) is fine. - // eof is set to true if p ended before endTag - static bool StringEqual(const char* p, - const char* endTag, - bool ignoreCase, - TiXmlEncoding encoding, - bool* eof = 0); - - static const char* errorString[ TIXML_ERROR_STRING_COUNT ]; - - TiXmlCursor location; - - /// Field containing a generic user pointer - void* userData; - - // None of these methods are reliable for any language except English. - // Good for approximation, not great for accuracy. - static int IsAlpha( unsigned char anyByte, TiXmlEncoding encoding ); - static int IsAlphaNum( unsigned char anyByte, TiXmlEncoding encoding ); - inline static int ToLower( int v, TiXmlEncoding encoding ) - { - if ( encoding == TIXML_ENCODING_UTF8 ) - { - if ( v < 128 ) return tolower( v ); - return v; - } - else - { - return tolower( v ); - } - } - static void ConvertUTF32ToUTF8( unsigned long input, char* output, int* length ); - -private: - TiXmlBase( const TiXmlBase& ); // not implemented. - void operator=( const TiXmlBase& base ); // not allowed. - - struct Entity - { - const char* str; - unsigned int strLength; - char chr; - }; - enum - { - NUM_ENTITY = 5, - MAX_ENTITY_LENGTH = 6 - - }; - static Entity entity[ NUM_ENTITY ]; - static bool condenseWhiteSpace; -}; - - -/** The parent class for everything in the Document Object Model. - (Except for attributes). - Nodes have siblings, a parent, and children. A node can be - in a document, or stand on its own. The type of a TiXmlNode - can be queried, and it can be cast to its more defined type. -*/ -class YXML_API TiXmlNode : public TiXmlBase -{ - friend class TiXmlDocument; - friend class TiXmlElement; - -public: - #ifdef TIXML_USE_STL - - /** An input stream operator, for every class. Tolerant of newlines and - formatting, but doesn't expect them. - */ - friend std::istream& operator >> (std::istream& in, TiXmlNode& base); - - /** An output stream operator, for every class. Note that this outputs - without any newlines or formatting, as opposed to Print(), which - includes tabs and new lines. - - The operator<< and operator>> are not completely symmetric. Writing - a node to a stream is very well defined. You'll get a nice stream - of output, without any extra whitespace or newlines. - - But reading is not as well defined. (As it always is.) If you create - a TiXmlElement (for example) and read that from an input stream, - the text needs to define an element or junk will result. This is - true of all input streams, but it's worth keeping in mind. - - A TiXmlDocument will read nodes until it reads a root element, and - all the children of that root element. - */ - friend std::ostream& operator<< (std::ostream& out, const TiXmlNode& base); - - /// Appends the XML node or attribute to a std::string. - friend std::string& operator<< (std::string& out, const TiXmlNode& base ); - - #else - // Used internally, not part of the public API. - friend TIXML_OSTREAM& operator<< (TIXML_OSTREAM& out, const TiXmlNode& base); - #endif - - /** The types of XML nodes supported by TinyXml. (All the - unsupported types are picked up by UNKNOWN.) - */ - enum NodeType - { - DOCUMENT, - ELEMENT, - COMMENT, - UNKNOWN, - TEXT, - DECLARATION, - TYPECOUNT - }; - - virtual ~TiXmlNode(); - - /** The meaning of 'value' changes for the specific type of - TiXmlNode. - @verbatim - Document: filename of the xml file - Element: name of the element - Comment: the comment text - Unknown: the tag contents - Text: the text string - @endverbatim - - The subclasses will wrap this function. - */ - const char *Value() const { return value.c_str (); } - - #ifdef TIXML_USE_STL - /** Return Value() as a std::string. If you only use STL, - this is more efficient than calling Value(). - Only available in STL mode. - */ - const std::string& ValueStr() const { return value; } - #endif - - /** Changes the value of the node. Defined as: - @verbatim - Document: filename of the xml file - Element: name of the element - Comment: the comment text - Unknown: the tag contents - Text: the text string - @endverbatim - */ - void SetValue(const char * _value) { value = _value;} - - #ifdef TIXML_USE_STL - /// STL std::string form. - void SetValue( const std::string& _value ) { value = _value; } - #endif - - /// Delete all the children of this node. Does not affect 'this'. - void Clear(); - - /// One step up the DOM. - TiXmlNode* Parent() { return parent; } - const TiXmlNode* Parent() const { return parent; } - - const TiXmlNode* FirstChild() const { return firstChild; } ///< The first child of this node. Will be null if there are no children. - TiXmlNode* FirstChild() { return firstChild; } - const TiXmlNode* FirstChild( const char * value ) const; ///< The first child of this node with the matching 'value'. Will be null if none found. - TiXmlNode* FirstChild( const char * value ); ///< The first child of this node with the matching 'value'. Will be null if none found. - - const TiXmlNode* LastChild() const { return lastChild; } /// The last child of this node. Will be null if there are no children. - TiXmlNode* LastChild() { return lastChild; } - const TiXmlNode* LastChild( const char * value ) const; /// The last child of this node matching 'value'. Will be null if there are no children. - TiXmlNode* LastChild( const char * value ); - - #ifdef TIXML_USE_STL - const TiXmlNode* FirstChild( const std::string& _value ) const { return FirstChild (_value.c_str ()); } ///< STL std::string form. - TiXmlNode* FirstChild( const std::string& _value ) { return FirstChild (_value.c_str ()); } ///< STL std::string form. - const TiXmlNode* LastChild( const std::string& _value ) const { return LastChild (_value.c_str ()); } ///< STL std::string form. - TiXmlNode* LastChild( const std::string& _value ) { return LastChild (_value.c_str ()); } ///< STL std::string form. - #endif - - /** An alternate way to walk the children of a node. - One way to iterate over nodes is: - @verbatim - for( child = parent->FirstChild(); child; child = child->NextSibling() ) - @endverbatim - - IterateChildren does the same thing with the syntax: - @verbatim - child = 0; - while( child = parent->IterateChildren( child ) ) - @endverbatim - - IterateChildren takes the previous child as input and finds - the next one. If the previous child is null, it returns the - first. IterateChildren will return null when done. - */ - const TiXmlNode* IterateChildren( const TiXmlNode* previous ) const; - TiXmlNode* IterateChildren( TiXmlNode* previous ); - - /// This flavor of IterateChildren searches for children with a particular 'value' - const TiXmlNode* IterateChildren( const char * value, const TiXmlNode* previous ) const; - TiXmlNode* IterateChildren( const char * value, TiXmlNode* previous ); - - #ifdef TIXML_USE_STL - const TiXmlNode* IterateChildren( const std::string& _value, const TiXmlNode* previous ) const { return IterateChildren (_value.c_str (), previous); } ///< STL std::string form. - TiXmlNode* IterateChildren( const std::string& _value, TiXmlNode* previous ) { return IterateChildren (_value.c_str (), previous); } ///< STL std::string form. - #endif - - /** Add a new node related to this. Adds a child past the LastChild. - Returns a pointer to the new object or NULL if an error occured. - */ - TiXmlNode* InsertEndChild( const TiXmlNode& addThis ); - - - /** Add a new node related to this. Adds a child past the LastChild. - - NOTE: the node to be added is passed by pointer, and will be - henceforth owned (and deleted) by tinyXml. This method is efficient - and avoids an extra copy, but should be used with care as it - uses a different memory model than the other insert functions. - - @sa InsertEndChild - */ - TiXmlNode* LinkEndChild( TiXmlNode* addThis ); - - /** Add a new node related to this. Adds a child before the specified child. - Returns a pointer to the new object or NULL if an error occured. - */ - TiXmlNode* InsertBeforeChild( TiXmlNode* beforeThis, const TiXmlNode& addThis ); - - /** Add a new node related to this. Adds a child after the specified child. - Returns a pointer to the new object or NULL if an error occured. - */ - TiXmlNode* InsertAfterChild( TiXmlNode* afterThis, const TiXmlNode& addThis ); - - /** Replace a child of this node. - Returns a pointer to the new object or NULL if an error occured. - */ - TiXmlNode* ReplaceChild( TiXmlNode* replaceThis, const TiXmlNode& withThis ); - - /// Remove a node from the document. Delete it if 'del' is true - bool RemoveChild( TiXmlNode* removeThis, bool del = true ); - - /// Navigate to a sibling node. - const TiXmlNode* PreviousSibling() const { return prev; } - TiXmlNode* PreviousSibling() { return prev; } - - /// Navigate to a sibling node. - const TiXmlNode* PreviousSibling( const char * ) const; - TiXmlNode* PreviousSibling( const char * ); - - #ifdef TIXML_USE_STL - const TiXmlNode* PreviousSibling( const std::string& _value ) const { return PreviousSibling (_value.c_str ()); } ///< STL std::string form. - TiXmlNode* PreviousSibling( const std::string& _value ) { return PreviousSibling (_value.c_str ()); } ///< STL std::string form. - const TiXmlNode* NextSibling( const std::string& _value) const { return NextSibling (_value.c_str ()); } ///< STL std::string form. - TiXmlNode* NextSibling( const std::string& _value) { return NextSibling (_value.c_str ()); } ///< STL std::string form. - #endif - - /// Navigate to a sibling node. - const TiXmlNode* NextSibling() const { return next; } - TiXmlNode* NextSibling() { return next; } - - /// Navigate to a sibling node with the given 'value'. - const TiXmlNode* NextSibling( const char * ) const; - TiXmlNode* NextSibling( const char * ); - - /** Convenience function to get through elements. - Calls NextSibling and ToElement. Will skip all non-Element - nodes. Returns 0 if there is not another element. - */ - const TiXmlElement* NextSiblingElement() const; - TiXmlElement* NextSiblingElement(); - - /** Convenience function to get through elements. - Calls NextSibling and ToElement. Will skip all non-Element - nodes. Returns 0 if there is not another element. - */ - const TiXmlElement* NextSiblingElement( const char * ) const; - TiXmlElement* NextSiblingElement( const char * ); - - #ifdef TIXML_USE_STL - const TiXmlElement* NextSiblingElement( const std::string& _value) const { return NextSiblingElement (_value.c_str ()); } ///< STL std::string form. - TiXmlElement* NextSiblingElement( const std::string& _value) { return NextSiblingElement (_value.c_str ()); } ///< STL std::string form. - #endif - - /// Convenience function to get through elements. - const TiXmlElement* FirstChildElement() const; - TiXmlElement* FirstChildElement(); - - /// Convenience function to get through elements. - const TiXmlElement* FirstChildElement( const char * value ) const; - TiXmlElement* FirstChildElement( const char * value ); - - #ifdef TIXML_USE_STL - const TiXmlElement* FirstChildElement( const std::string& _value ) const { return FirstChildElement (_value.c_str ()); } ///< STL std::string form. - TiXmlElement* FirstChildElement( const std::string& _value ) { return FirstChildElement (_value.c_str ()); } ///< STL std::string form. - #endif - - /** Query the type (as an enumerated value, above) of this node. - The possible types are: DOCUMENT, ELEMENT, COMMENT, - UNKNOWN, TEXT, and DECLARATION. - */ - int Type() const { return type; } - - /** Return a pointer to the Document this node lives in. - Returns null if not in a document. - */ - const TiXmlDocument* GetDocument() const; - TiXmlDocument* GetDocument(); - - /// Returns true if this node has no children. - bool NoChildren() const { return !firstChild; } - - virtual const TiXmlDocument* ToDocument() const { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual const TiXmlElement* ToElement() const { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual const TiXmlComment* ToComment() const { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual const TiXmlUnknown* ToUnknown() const { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual const TiXmlText* ToText() const { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual const TiXmlDeclaration* ToDeclaration() const { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - - virtual TiXmlDocument* ToDocument() { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual TiXmlElement* ToElement() { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual TiXmlComment* ToComment() { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual TiXmlUnknown* ToUnknown() { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual TiXmlText* ToText() { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - virtual TiXmlDeclaration* ToDeclaration() { return 0; } ///< Cast to a more defined type. Will return null if not of the requested type. - - /** Create an exact duplicate of this node and return it. The memory must be deleted - by the caller. - */ - virtual TiXmlNode* Clone() const = 0; - -protected: - TiXmlNode( NodeType _type ); - - // Copy to the allocated object. Shared functionality between Clone, Copy constructor, - // and the assignment operator. - void CopyTo( TiXmlNode* target ) const; - - #ifdef TIXML_USE_STL - // The real work of the input operator. - virtual void StreamIn( TIXML_ISTREAM* in, TIXML_STRING* tag ) = 0; - #endif - - // Figure out what is at *p, and parse it. Returns null if it is not an xml node. - TiXmlNode* Identify( const char* start, TiXmlEncoding encoding ); - - TiXmlNode* parent; - NodeType type; - - TiXmlNode* firstChild; - TiXmlNode* lastChild; - - TIXML_STRING value; - - TiXmlNode* prev; - TiXmlNode* next; - -private: - TiXmlNode( const TiXmlNode& ); // not implemented. - void operator=( const TiXmlNode& base ); // not allowed. -}; - - -/** An attribute is a name-value pair. Elements have an arbitrary - number of attributes, each with a unique name. - - @note The attributes are not TiXmlNodes, since they are not - part of the tinyXML document object model. There are other - suggested ways to look at this problem. -*/ -class YXML_API TiXmlAttribute : public TiXmlBase -{ - friend class TiXmlAttributeSet; - friend class TiXmlElement; - -public: - /// Construct an empty attribute. - TiXmlAttribute() : TiXmlBase() - { - document = 0; - prev = next = 0; - } - - #ifdef TIXML_USE_STL - /// std::string constructor. - TiXmlAttribute( const std::string& _name, const std::string& _value ) - { - name = _name; - value = _value; - document = 0; - prev = next = 0; - } - #endif - - /// Construct an attribute with a name and value. - TiXmlAttribute( const char * _name, const char * _value ) - { - name = _name; - value = _value; - document = 0; - prev = next = 0; - } - - const char* Name() const { return name.c_str (); } ///< Return the name of this attribute. - const char* Value() const { return value.c_str (); } ///< Return the value of this attribute. - int IntValue() const; ///< Return the value of this attribute, converted to an integer. - double DoubleValue() const; ///< Return the value of this attribute, converted to a double. - - // Get the tinyxml string representation - const TIXML_STRING& NameTStr() const { return name; } - - /** QueryIntValue examines the value string. It is an alternative to the - IntValue() method with richer error checking. - If the value is an integer, it is stored in 'value' and - the call returns TIXML_SUCCESS. If it is not - an integer, it returns TIXML_WRONG_TYPE. - - A specialized but useful call. Note that for success it returns 0, - which is the opposite of almost all other TinyXml calls. - */ - int QueryIntValue( int* _value ) const; - /// QueryDoubleValue examines the value string. See QueryIntValue(). - int QueryDoubleValue( double* _value ) const; - - void SetName( const char* _name ) { name = _name; } ///< Set the name of this attribute. - void SetValue( const char* _value ) { value = _value; } ///< Set the value. - - void SetIntValue( int _value ); ///< Set the value from an integer. - void SetDoubleValue( double _value ); ///< Set the value from a double. - - #ifdef TIXML_USE_STL - /// STL std::string form. - void SetName( const std::string& _name ) { name = _name; } - /// STL std::string form. - void SetValue( const std::string& _value ) { value = _value; } - #endif - - /// Get the next sibling attribute in the DOM. Returns null at end. - const TiXmlAttribute* Next() const; - TiXmlAttribute* Next(); - /// Get the previous sibling attribute in the DOM. Returns null at beginning. - const TiXmlAttribute* Previous() const; - TiXmlAttribute* Previous(); - - bool operator==( const TiXmlAttribute& rhs ) const { return rhs.name == name; } - bool operator<( const TiXmlAttribute& rhs ) const { return name < rhs.name; } - bool operator>( const TiXmlAttribute& rhs ) const { return name > rhs.name; } - - /* Attribute parsing starts: first letter of the name - returns: the next char after the value end quote - */ - virtual const char* Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ); - - // Prints this Attribute to a FILE stream. - virtual void Print( FILE* cfile, int depth ) const; - -public: - virtual void StreamOut( TIXML_OSTREAM * out, bool unclosed = false ) const; - -protected: - // [internal use] - // Set the document pointer so the attribute can report errors. - void SetDocument( TiXmlDocument* doc ) { document = doc; } - -private: - TiXmlAttribute( const TiXmlAttribute& ); // not implemented. - void operator=( const TiXmlAttribute& base ); // not allowed. - - TiXmlDocument* document; // A pointer back to a document, for error reporting. - TIXML_STRING name; - TIXML_STRING value; - TiXmlAttribute* prev; - TiXmlAttribute* next; -}; - - -/* A class used to manage a group of attributes. - It is only used internally, both by the ELEMENT and the DECLARATION. - - The set can be changed transparent to the Element and Declaration - classes that use it, but NOT transparent to the Attribute - which has to implement a next() and previous() method. Which makes - it a bit problematic and prevents the use of STL. - - This version is implemented with circular lists because: - - I like circular lists - - it demonstrates some independence from the (typical) doubly linked list. -*/ -class YXML_API TiXmlAttributeSet -{ -public: - TiXmlAttributeSet(); - ~TiXmlAttributeSet(); - - void Add( TiXmlAttribute* attribute ); - void Remove( TiXmlAttribute* attribute ); - - const TiXmlAttribute* First() const { return ( sentinel.next == &sentinel ) ? 0 : sentinel.next; } - TiXmlAttribute* First() { return ( sentinel.next == &sentinel ) ? 0 : sentinel.next; } - const TiXmlAttribute* Last() const { return ( sentinel.prev == &sentinel ) ? 0 : sentinel.prev; } - TiXmlAttribute* Last() { return ( sentinel.prev == &sentinel ) ? 0 : sentinel.prev; } - - const TiXmlAttribute* Find( const TIXML_STRING& name ) const; - TiXmlAttribute* Find( const TIXML_STRING& name ); - -private: - //*ME: Because of hidden/disabled copy-construktor in TiXmlAttribute (sentinel-element), - //*ME: this class must be also use a hidden/disabled copy-constructor !!! - TiXmlAttributeSet( const TiXmlAttributeSet& ); // not allowed - void operator=( const TiXmlAttributeSet& ); // not allowed (as TiXmlAttribute) - - TiXmlAttribute sentinel; -}; - - -/** The element is a container class. It has a value, the element name, - and can contain other elements, text, comments, and unknowns. - Elements also contain an arbitrary number of attributes. -*/ -class YXML_API TiXmlElement : public TiXmlNode -{ -public: - /// Construct an element. - TiXmlElement (const char * in_value); - - #ifdef TIXML_USE_STL - /// std::string constructor. - TiXmlElement( const std::string& _value ); - #endif - - TiXmlElement( const TiXmlElement& ); - - void operator=( const TiXmlElement& base ); - - virtual ~TiXmlElement(); - - /** Given an attribute name, Attribute() returns the value - for the attribute of that name, or null if none exists. - */ - const char* Attribute( const char* name ) const; - - /** Given an attribute name, Attribute() returns the value - for the attribute of that name, or null if none exists. - If the attribute exists and can be converted to an integer, - the integer value will be put in the return 'i', if 'i' - is non-null. - */ - const char* Attribute( const char* name, int* i ) const; - - /** Given an attribute name, Attribute() returns the value - for the attribute of that name, or null if none exists. - If the attribute exists and can be converted to an double, - the double value will be put in the return 'd', if 'd' - is non-null. - */ - const char* Attribute( const char* name, double* d ) const; - - /** QueryIntAttribute examines the attribute - it is an alternative to the - Attribute() method with richer error checking. - If the attribute is an integer, it is stored in 'value' and - the call returns TIXML_SUCCESS. If it is not - an integer, it returns TIXML_WRONG_TYPE. If the attribute - does not exist, then TIXML_NO_ATTRIBUTE is returned. - */ - int QueryIntAttribute( const char* name, int* _value ) const; - /// QueryDoubleAttribute examines the attribute - see QueryIntAttribute(). - int QueryDoubleAttribute( const char* name, double* _value ) const; - /// QueryFloatAttribute examines the attribute - see QueryIntAttribute(). - int QueryFloatAttribute( const char* name, float* _value ) const { - double d; - int result = QueryDoubleAttribute( name, &d ); - if ( result == TIXML_SUCCESS ) { - *_value = (float)d; - } - return result; - } - - /** Sets an attribute of name to a given value. The attribute - will be created if it does not exist, or changed if it does. - */ - void SetAttribute( const char* name, const char * _value ); - - #ifdef TIXML_USE_STL - const char* Attribute( const std::string& name ) const { return Attribute( name.c_str() ); } - const char* Attribute( const std::string& name, int* i ) const { return Attribute( name.c_str(), i ); } - const char* Attribute( const std::string& name, double* d ) const { return Attribute( name.c_str(), d ); } - int QueryIntAttribute( const std::string& name, int* _value ) const { return QueryIntAttribute( name.c_str(), _value ); } - int QueryDoubleAttribute( const std::string& name, double* _value ) const { return QueryDoubleAttribute( name.c_str(), _value ); } - - /// STL std::string form. - void SetAttribute( const std::string& name, const std::string& _value ); - ///< STL std::string form. - void SetAttribute( const std::string& name, int _value ); - #endif - - /** Sets an attribute of name to a given value. The attribute - will be created if it does not exist, or changed if it does. - */ - void SetAttribute( const char * name, int value ); - - /** Sets an attribute of name to a given value. The attribute - will be created if it does not exist, or changed if it does. - */ - void SetDoubleAttribute( const char * name, double value ); - - /** Deletes an attribute with the given name. - */ - void RemoveAttribute( const char * name ); - #ifdef TIXML_USE_STL - void RemoveAttribute( const std::string& name ) { RemoveAttribute (name.c_str ()); } ///< STL std::string form. - #endif - - const TiXmlAttribute* FirstAttribute() const { return attributeSet.First(); } ///< Access the first attribute in this element. - TiXmlAttribute* FirstAttribute() { return attributeSet.First(); } - const TiXmlAttribute* LastAttribute() const { return attributeSet.Last(); } ///< Access the last attribute in this element. - TiXmlAttribute* LastAttribute() { return attributeSet.Last(); } - - /** Convenience function for easy access to the text inside an element. Although easy - and concise, GetText() is limited compared to getting the TiXmlText child - and accessing it directly. - - If the first child of 'this' is a TiXmlText, the GetText() - returns the character string of the Text node, else null is returned. - - This is a convenient method for getting the text of simple contained text: - @verbatim - This is text - const char* str = fooElement->GetText(); - @endverbatim - - 'str' will be a pointer to "This is text". - - Note that this function can be misleading. If the element foo was created from - this XML: - @verbatim - This is text - @endverbatim - - then the value of str would be null. The first child node isn't a text node, it is - another element. From this XML: - @verbatim - This is text - @endverbatim - GetText() will return "This is ". - - WARNING: GetText() accesses a child node - don't become confused with the - similarly named TiXmlHandle::Text() and TiXmlNode::ToText() which are - safe type casts on the referenced node. - */ - const char* GetText() const; - - /// Creates a new Element and returns it - the returned element is a copy. - virtual TiXmlNode* Clone() const; - // Print the Element to a FILE stream. - virtual void Print( FILE* cfile, int depth ) const; - - /* Attribtue parsing starts: next char past '<' - returns: next char past '>' - */ - virtual const char* Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ); - - virtual const TiXmlElement* ToElement() const { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - virtual TiXmlElement* ToElement() { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - -protected: - - void CopyTo( TiXmlElement* target ) const; - void ClearThis(); // like clear, but initializes 'this' object as well - - // Used to be public [internal use] - #ifdef TIXML_USE_STL - virtual void StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ); - #endif -public: - virtual void StreamOut( TIXML_OSTREAM * out, bool unclosed = false ) const; - -protected: - /* [internal use] - Reads the "value" of the element -- another element, or text. - This should terminate with the current end tag. - */ - const char* ReadValue( const char* in, TiXmlParsingData* prevData, TiXmlEncoding encoding ); - -private: - - TiXmlAttributeSet attributeSet; -}; - - -/** An XML comment. -*/ -class YXML_API TiXmlComment : public TiXmlNode -{ -public: - /// Constructs an empty comment. - TiXmlComment() : TiXmlNode( TiXmlNode::COMMENT ) {} - TiXmlComment( const TiXmlComment& ); - void operator=( const TiXmlComment& base ); - - virtual ~TiXmlComment() {} - - /// Returns a copy of this Comment. - virtual TiXmlNode* Clone() const; - /// Write this Comment to a FILE stream. - virtual void Print( FILE* cfile, int depth ) const; - - /* Attribtue parsing starts: at the ! of the !-- - returns: next char past '>' - */ - virtual const char* Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ); - - virtual const TiXmlComment* ToComment() const { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - virtual TiXmlComment* ToComment() { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - -protected: - void CopyTo( TiXmlComment* target ) const; - - // used to be public - #ifdef TIXML_USE_STL - virtual void StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ); - #endif - -public: - virtual void StreamOut( TIXML_OSTREAM * out, bool unclosed = false ) const; - -private: - -}; - - -/** XML text. A text node can have 2 ways to output the next. "normal" output - and CDATA. It will default to the mode it was parsed from the XML file and - you generally want to leave it alone, but you can change the output mode with - SetCDATA() and query it with CDATA(). -*/ -class YXML_API TiXmlText : public TiXmlNode -{ - friend class TiXmlElement; -public: - /** Constructor for text element. By default, it is treated as - normal, encoded text. If you want it be output as a CDATA text - element, set the parameter _cdata to 'true' - */ - TiXmlText (const char * initValue ) : TiXmlNode (TiXmlNode::TEXT) - { - SetValue( initValue ); - cdata = false; - } - virtual ~TiXmlText() {} - - #ifdef TIXML_USE_STL - /// Constructor. - TiXmlText( const std::string& initValue ) : TiXmlNode (TiXmlNode::TEXT) - { - SetValue( initValue ); - cdata = false; - } - #endif - - TiXmlText( const TiXmlText& copy ) : TiXmlNode( TiXmlNode::TEXT ) { copy.CopyTo( this ); } - void operator=( const TiXmlText& base ) { base.CopyTo( this ); } - - /// Write this text object to a FILE stream. - virtual void Print( FILE* cfile, int depth ) const; - - /// Queries whether this represents text using a CDATA section. - bool CDATA() { return cdata; } - /// Turns on or off a CDATA representation of text. - void SetCDATA( bool _cdata ) { cdata = _cdata; } - - virtual const char* Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ); - - virtual const TiXmlText* ToText() const { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - virtual TiXmlText* ToText() { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - -protected : - /// [internal use] Creates a new Element and returns it. - virtual TiXmlNode* Clone() const; - void CopyTo( TiXmlText* target ) const; - -public: - virtual void StreamOut ( TIXML_OSTREAM * out, bool unclosed = false ) const; - -protected: - bool Blank() const; // returns true if all white space and new lines - // [internal use] - #ifdef TIXML_USE_STL - virtual void StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ); - #endif - -private: - bool cdata; // true if this should be input and output as a CDATA style text element -}; - - -/** In correct XML the declaration is the first entry in the file. - @verbatim - - @endverbatim - - TinyXml will happily read or write files without a declaration, - however. There are 3 possible attributes to the declaration: - version, encoding, and standalone. - - Note: In this version of the code, the attributes are - handled as special cases, not generic attributes, simply - because there can only be at most 3 and they are always the same. -*/ -class YXML_API TiXmlDeclaration : public TiXmlNode -{ -public: - /// Construct an empty declaration. - TiXmlDeclaration() : TiXmlNode( TiXmlNode::DECLARATION ) {} - -#ifdef TIXML_USE_STL - /// Constructor. - TiXmlDeclaration( const std::string& _version, - const std::string& _encoding, - const std::string& _standalone ); -#endif - - /// Construct. - TiXmlDeclaration( const char* _version, - const char* _encoding, - const char* _standalone ); - - TiXmlDeclaration( const TiXmlDeclaration& copy ); - void operator=( const TiXmlDeclaration& copy ); - - virtual ~TiXmlDeclaration() {} - - /// Version. Will return an empty string if none was found. - const char *Version() const { return version.c_str (); } - /// Encoding. Will return an empty string if none was found. - const char *Encoding() const { return encoding.c_str (); } - /// Is this a standalone document? - const char *Standalone() const { return standalone.c_str (); } - - /// Creates a copy of this Declaration and returns it. - virtual TiXmlNode* Clone() const; - /// Print this declaration to a FILE stream. - virtual void Print( FILE* cfile, int depth ) const; - - virtual const char* Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ); - - virtual const TiXmlDeclaration* ToDeclaration() const { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - virtual TiXmlDeclaration* ToDeclaration() { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - -protected: - void CopyTo( TiXmlDeclaration* target ) const; - // used to be public - #ifdef TIXML_USE_STL - virtual void StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ); - #endif - -public: - virtual void StreamOut ( TIXML_OSTREAM * out, bool unclosed = false ) const; - -private: - - TIXML_STRING version; - TIXML_STRING encoding; - TIXML_STRING standalone; -}; - - -/** Any tag that tinyXml doesn't recognize is saved as an - unknown. It is a tag of text, but should not be modified. - It will be written back to the XML, unchanged, when the file - is saved. - - DTD tags get thrown into TiXmlUnknowns. -*/ -class YXML_API TiXmlUnknown : public TiXmlNode -{ -public: - TiXmlUnknown() : TiXmlNode( TiXmlNode::UNKNOWN ) {} - virtual ~TiXmlUnknown() {} - - TiXmlUnknown( const TiXmlUnknown& copy ) : TiXmlNode( TiXmlNode::UNKNOWN ) { copy.CopyTo( this ); } - void operator=( const TiXmlUnknown& copy ) { copy.CopyTo( this ); } - - /// Creates a copy of this Unknown and returns it. - virtual TiXmlNode* Clone() const; - /// Print this Unknown to a FILE stream. - virtual void Print( FILE* cfile, int depth ) const; - - virtual const char* Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ); - - virtual const TiXmlUnknown* ToUnknown() const { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - virtual TiXmlUnknown* ToUnknown() { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - -protected: - void CopyTo( TiXmlUnknown* target ) const; - - #ifdef TIXML_USE_STL - virtual void StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ); - #endif - -public: - virtual void StreamOut ( TIXML_OSTREAM * out, bool unclosed = false ) const; - -private: - -}; - - -/** Always the top level node. A document binds together all the - XML pieces. It can be saved, loaded, and printed to the screen. - The 'value' of a document node is the xml file name. -*/ -class YXML_API TiXmlDocument : public TiXmlNode -{ -public: - /// Create an empty document, that has no name. - TiXmlDocument(); - /// Create a document with a name. The name of the document is also the filename of the xml. - TiXmlDocument( const char * documentName ); - - #ifdef TIXML_USE_STL - /// Constructor. - TiXmlDocument( const std::string& documentName ); - #endif - - TiXmlDocument( const TiXmlDocument& copy ); - void operator=( const TiXmlDocument& copy ); - - virtual ~TiXmlDocument() {} - - /** Load a file using the current document value. - Returns true if successful. Will delete any existing - document data before loading. - */ - bool LoadFile( TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING ); - /// Save a file using the current document value. Returns true if successful. - bool SaveFile() const; - /// Load a file using the given filename. Returns true if successful. - bool LoadFile( const char * filename, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING ); - /// Save a file using the given filename. Returns true if successful. - bool SaveFile( const char * filename ) const; - /** Load a file using the given FILE*. Returns true if successful. Note that this method - doesn't stream - the entire object pointed at by the FILE* - will be interpreted as an XML file. TinyXML doesn't stream in XML from the current - file location. Streaming may be added in the future. - */ - bool LoadFile( FILE*, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING ); - /// Save a file using the given FILE*. Returns true if successful. - bool SaveFile( FILE* ) const; - - #ifdef TIXML_USE_STL - bool LoadFile( const std::string& filename, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING ) ///< STL std::string version. - { - StringToBuffer f( filename ); - return ( f.buffer && LoadFile( f.buffer, encoding )); - } - bool SaveFile( const std::string& filename ) const ///< STL std::string version. - { - StringToBuffer f( filename ); - return ( f.buffer && SaveFile( f.buffer )); - } - #endif - - /** Parse the given null terminated block of xml data. Passing in an encoding to this - method (either TIXML_ENCODING_LEGACY or TIXML_ENCODING_UTF8 will force TinyXml - to use that encoding, regardless of what TinyXml might otherwise try to detect. - @return The location where parsing stopped. On error, this is the start point of the first level node that generated the error. - */ - virtual const char* Parse( const char* p, TiXmlParsingData* data = 0, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING ); - - /** Get the root element -- the only top level element -- of the document. - In well formed XML, there should only be one. TinyXml is tolerant of - multiple elements at the document level. - */ - const TiXmlElement* RootElement() const { return FirstChildElement(); } - TiXmlElement* RootElement() { return FirstChildElement(); } - - /** If an error occurs, Error will be set to true. Also, - - The ErrorId() will contain the integer identifier of the error (not generally useful) - - The ErrorDesc() method will return the name of the error. (very useful) - - The ErrorRow() and ErrorCol() will return the location of the error (if known) - */ - bool Error() const { return error; } - - /// Contains a textual (english) description of the error if one occurs. - const char * ErrorDesc() const { return errorDesc.c_str (); } - - /** Generally, you probably want the error string ( ErrorDesc() ). But if you - prefer the ErrorId, this function will fetch it. - */ - int ErrorId() const { return errorId; } - - /** Returns the location (if known) of the error. The first column is column 1, - and the first row is row 1. A value of 0 means the row and column wasn't applicable - (memory errors, for example, have no row/column) or the parser lost the error. (An - error in the error reporting, in that case.) - - @sa SetTabSize, Row, Column - */ - int ErrorRow() { return errorLocation.row+1; } - int ErrorCol() { return errorLocation.col+1; } ///< The column where the error occured. See ErrorRow() - - /** SetTabSize() allows the error reporting functions (ErrorRow() and ErrorCol()) - to report the correct values for row and column. It does not change the output - or input in any way. - - By calling this method, with a tab size - greater than 0, the row and column of each node and attribute is stored - when the file is loaded. Very useful for tracking the DOM back in to - the source file. - - The tab size is required for calculating the location of nodes. If not - set, the default of 4 is used. The tabsize is set per document. Setting - the tabsize to 0 disables row/column tracking. - - Note that row and column tracking is not supported when using operator>>. - - The tab size needs to be enabled before the parse or load. Correct usage: - @verbatim - TiXmlDocument doc; - doc.SetTabSize( 8 ); - doc.Load( "myfile.xml" ); - @endverbatim - - @sa Row, Column - */ - void SetTabSize( int _tabsize ) { tabsize = _tabsize; } - - int TabSize() const { return tabsize; } - - /** If you have handled the error, it can be reset with this call. The error - state is automatically cleared if you Parse a new XML block. - */ - void ClearError() { error = false; - errorId = 0; - errorDesc = ""; - errorLocation.row = errorLocation.col = 0; - //errorLocation.last = 0; - } - - /** Dump the document to standard out. */ - void Print() const { Print( stdout, 0 ); } - - /// Print this Document to a FILE stream. - virtual void Print( FILE* cfile, int depth = 0 ) const; - // [internal use] - void SetError( int err, const char* errorLocation, TiXmlParsingData* prevData, TiXmlEncoding encoding ); - virtual const TiXmlDocument* ToDocument() const { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - virtual TiXmlDocument* ToDocument() { return this; } ///< Cast to a more defined type. Will return null not of the requested type. - - virtual void StreamOut ( TIXML_OSTREAM * out, bool unclosed = false ) const; - -protected : - // [internal use] - virtual TiXmlNode* Clone() const; - #ifdef TIXML_USE_STL - virtual void StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ); - #endif - -private: - void CopyTo( TiXmlDocument* target ) const; - - bool error; - int errorId; - TIXML_STRING errorDesc; - int tabsize; - TiXmlCursor errorLocation; - bool useMicrosoftBOM; // the UTF-8 BOM were found when read. Note this, and try to write. -}; - - -/** - A TiXmlHandle is a class that wraps a node pointer with null checks; this is - an incredibly useful thing. Note that TiXmlHandle is not part of the TinyXml - DOM structure. It is a separate utility class. - - Take an example: - @verbatim - - - - - - - @endverbatim - - Assuming you want the value of "attributeB" in the 2nd "Child" element, it's very - easy to write a *lot* of code that looks like: - - @verbatim - TiXmlElement* root = document.FirstChildElement( "Document" ); - if ( root ) - { - TiXmlElement* element = root->FirstChildElement( "Element" ); - if ( element ) - { - TiXmlElement* child = element->FirstChildElement( "Child" ); - if ( child ) - { - TiXmlElement* child2 = child->NextSiblingElement( "Child" ); - if ( child2 ) - { - // Finally do something useful. - @endverbatim - - And that doesn't even cover "else" cases. TiXmlHandle addresses the verbosity - of such code. A TiXmlHandle checks for null pointers so it is perfectly safe - and correct to use: - - @verbatim - TiXmlHandle docHandle( &document ); - TiXmlElement* child2 = docHandle.FirstChild( "Document" ).FirstChild( "Element" ).Child( "Child", 1 ).Element(); - if ( child2 ) - { - // do something useful - @endverbatim - - Which is MUCH more concise and useful. - - It is also safe to copy handles - internally they are nothing more than node pointers. - @verbatim - TiXmlHandle handleCopy = handle; - @endverbatim - - What they should not be used for is iteration: - - @verbatim - int i=0; - while ( true ) - { - TiXmlElement* child = docHandle.FirstChild( "Document" ).FirstChild( "Element" ).Child( "Child", i ).Element(); - if ( !child ) - break; - // do something - ++i; - } - @endverbatim - - It seems reasonable, but it is in fact two embedded while loops. The Child method is - a linear walk to find the element, so this code would iterate much more than it needs - to. Instead, prefer: - - @verbatim - TiXmlElement* child = docHandle.FirstChild( "Document" ).FirstChild( "Element" ).FirstChild( "Child" ).Element(); - - for( child; child; child=child->NextSiblingElement() ) - { - // do something - } - @endverbatim -*/ -class YXML_API TiXmlHandle -{ -public: - /// Create a handle from any node (at any depth of the tree.) This can be a null pointer. - TiXmlHandle( TiXmlNode* _node ) { this->node = _node; } - /// Copy constructor - TiXmlHandle( const TiXmlHandle& ref ) { this->node = ref.node; } - TiXmlHandle operator=( const TiXmlHandle& ref ) { this->node = ref.node; return *this; } - - /// Return a handle to the first child node. - TiXmlHandle FirstChild() const; - /// Return a handle to the first child node with the given name. - TiXmlHandle FirstChild( const char * value ) const; - /// Return a handle to the first child element. - TiXmlHandle FirstChildElement() const; - /// Return a handle to the first child element with the given name. - TiXmlHandle FirstChildElement( const char * value ) const; - - /** Return a handle to the "index" child with the given name. - The first child is 0, the second 1, etc. - */ - TiXmlHandle Child( const char* value, int index ) const; - /** Return a handle to the "index" child. - The first child is 0, the second 1, etc. - */ - TiXmlHandle Child( int index ) const; - /** Return a handle to the "index" child element with the given name. - The first child element is 0, the second 1, etc. Note that only TiXmlElements - are indexed: other types are not counted. - */ - TiXmlHandle ChildElement( const char* value, int index ) const; - /** Return a handle to the "index" child element. - The first child element is 0, the second 1, etc. Note that only TiXmlElements - are indexed: other types are not counted. - */ - TiXmlHandle ChildElement( int index ) const; - - #ifdef TIXML_USE_STL - TiXmlHandle FirstChild( const std::string& _value ) const { return FirstChild( _value.c_str() ); } - TiXmlHandle FirstChildElement( const std::string& _value ) const { return FirstChildElement( _value.c_str() ); } - - TiXmlHandle Child( const std::string& _value, int index ) const { return Child( _value.c_str(), index ); } - TiXmlHandle ChildElement( const std::string& _value, int index ) const { return ChildElement( _value.c_str(), index ); } - #endif - - /// Return the handle as a TiXmlNode. This may return null. - TiXmlNode* Node() const { return node; } - /// Return the handle as a TiXmlElement. This may return null. - TiXmlElement* Element() const { return ( ( node && node->ToElement() ) ? node->ToElement() : 0 ); } - /// Return the handle as a TiXmlText. This may return null. - TiXmlText* Text() const { return ( ( node && node->ToText() ) ? node->ToText() : 0 ); } - /// Return the handle as a TiXmlUnknown. This may return null; - TiXmlUnknown* Unknown() const { return ( ( node && node->ToUnknown() ) ? node->ToUnknown() : 0 ); } - -private: - TiXmlNode* node; -}; - -class YXML_API TiXmlParsingData -{ - friend class TiXmlDocument; - public: - void Stamp( const char* now, TiXmlEncoding encoding ); - - const TiXmlCursor& Cursor() { return cursor; } - - private: - // Only used by the document! - TiXmlParsingData( const char* start, int _tabsize, int row, int col ) - { - assert( start ); - stamp = start; - tabsize = _tabsize; - cursor.row = row; - cursor.col = col; - } - - TiXmlCursor cursor; - const char* stamp; - int tabsize; -}; - -}; // TelEngine namespace - -#ifdef _MSC_VER -#pragma warning( pop ) -#endif - -#endif - diff --git a/libs/yxml/tinyxmlparser.cpp b/libs/yxml/tinyxmlparser.cpp deleted file mode 100644 index c592678d..00000000 --- a/libs/yxml/tinyxmlparser.cpp +++ /dev/null @@ -1,1625 +0,0 @@ -/* -www.sourceforge.net/projects/tinyxml -Original code (2.0 and earlier )copyright (c) 2000-2002 Lee Thomason (www.grinninglizard.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include -#include - -using namespace TelEngine; - -//#define DEBUG_PARSER -#if defined( DEBUG_PARSER ) -# if defined( DEBUG ) && defined( _MSC_VER ) -# include -# define TIXML_LOG OutputDebugString -# else -# define TIXML_LOG printf -# endif -#endif - -inline const char* error(TiXmlDocument* document, int err, const char* errorLocation, - TiXmlParsingData* prevData, TiXmlEncoding encoding) -{ - if (document) - document->SetError(err,errorLocation,prevData,encoding); - return 0; -} - -inline const char* errorIncomplete(TiXmlDocument* document, const char* errorLocation, - TiXmlParsingData* prevData, TiXmlEncoding encoding) -{ - return error(document,TiXmlBase::TIXML_ERROR_INCOMPLETE,errorLocation,prevData,encoding); -} - -const char* TiXmlBase::errorString[ TIXML_ERROR_STRING_COUNT ] = -{ - "No error", - "Error", - "Failed to open file", - "Memory allocation failed.", - "Error parsing Element.", - "Failed to read Element name", - "Error reading Element value.", - "Error reading Attributes.", - "Error: empty tag.", - "Error reading end tag.", - "Error parsing Unknown.", - "Error parsing Comment.", - "Error parsing Declaration.", - "Error document empty.", - "Error null (0) or unexpected EOF found in input stream.", - "Error parsing CDATA.", - "Error: Not terminated.", - "Buffer overrun.", -}; - -// Note tha "PutString" hardcodes the same list. This -// is less flexible than it appears. Changing the entries -// or order will break putstring. -TiXmlBase::Entity TiXmlBase::entity[ NUM_ENTITY ] = -{ - { "&", 5, '&' }, - { "<", 4, '<' }, - { ">", 4, '>' }, - { """, 6, '\"' }, - { "'", 6, '\'' } -}; - -// Bunch of unicode info at: -// http://www.unicode.org/faq/utf_bom.html -// Including the basic of this table, which determines the #bytes in the -// sequence from the lead byte. 1 placed for invalid sequences -- -// although the result will be junk, pass it through as much as possible. -// Beware of the non-characters in UTF-8: -// ef bb bf (Microsoft "lead bytes") -// ef bf be -// ef bf bf - -const unsigned char TIXML_UTF_LEAD_0 = 0xefU; -const unsigned char TIXML_UTF_LEAD_1 = 0xbbU; -const unsigned char TIXML_UTF_LEAD_2 = 0xbfU; - -const int TiXmlBase::utf8ByteTable[256] = -{ - // 0 1 2 3 4 5 6 7 8 9 a b c d e f - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x00 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x10 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x30 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x50 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x70 End of ASCII range - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x80 0x80 to 0xc1 invalid - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x90 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xa0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xb0 - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xc0 0xc2 to 0xdf 2 byte - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xd0 - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 0xe0 0xe0 to 0xef 3 byte - 4, 4, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // 0xf0 0xf0 to 0xf4 4 byte, 0xf5 and higher invalid -}; - - -void TiXmlBase::ConvertUTF32ToUTF8( unsigned long input, char* output, int* length ) -{ - const unsigned long BYTE_MASK = 0xBF; - const unsigned long BYTE_MARK = 0x80; - const unsigned long FIRST_BYTE_MARK[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; - - if (input < 0x80) - *length = 1; - else if ( input < 0x800 ) - *length = 2; - else if ( input < 0x10000 ) - *length = 3; - else if ( input < 0x200000 ) - *length = 4; - else - { *length = 0; return; } // This code won't covert this correctly anyway. - - output += *length; - - // Scary scary fall throughs. - switch (*length) - { - case 4: - --output; - *output = (char)((input | BYTE_MARK) & BYTE_MASK); - input >>= 6; - case 3: - --output; - *output = (char)((input | BYTE_MARK) & BYTE_MASK); - input >>= 6; - case 2: - --output; - *output = (char)((input | BYTE_MARK) & BYTE_MASK); - input >>= 6; - case 1: - --output; - *output = (char)(input | FIRST_BYTE_MARK[*length]); - } -} - - -/*static*/ int TiXmlBase::IsAlpha( unsigned char anyByte, TiXmlEncoding /*encoding*/ ) -{ - // This will only work for low-ascii, everything else is assumed to be a valid - // letter. I'm not sure this is the best approach, but it is quite tricky trying - // to figure out alhabetical vs. not across encoding. So take a very - // conservative approach. - -// if ( encoding == TIXML_ENCODING_UTF8 ) -// { - if ( anyByte < 127 ) - return isalpha( anyByte ); - else - return 1; // What else to do? The unicode set is huge...get the english ones right. -// } -// else -// { -// return isalpha( anyByte ); -// } -} - - -/*static*/ int TiXmlBase::IsAlphaNum( unsigned char anyByte, TiXmlEncoding /*encoding*/ ) -{ - // This will only work for low-ascii, everything else is assumed to be a valid - // letter. I'm not sure this is the best approach, but it is quite tricky trying - // to figure out alhabetical vs. not across encoding. So take a very - // conservative approach. - -// if ( encoding == TIXML_ENCODING_UTF8 ) -// { - if ( anyByte < 127 ) - return isalnum( anyByte ); - else - return 1; // What else to do? The unicode set is huge...get the english ones right. -// } -// else -// { -// return isalnum( anyByte ); -// } -} - -#if 0 -class YJINGLE_API TiXmlParsingData -{ - friend class TiXmlDocument; - public: - void Stamp( const char* now, TiXmlEncoding encoding ); - - const TiXmlCursor& Cursor() { return cursor; } - - private: - // Only used by the document! - TiXmlParsingData( const char* start, int _tabsize, int row, int col ) - { - assert( start ); - stamp = start; - tabsize = _tabsize; - cursor.row = row; - cursor.col = col; - } - - TiXmlCursor cursor; - const char* stamp; - int tabsize; -}; -#endif - -void TiXmlParsingData::Stamp( const char* now, TiXmlEncoding encoding ) -{ - assert( now ); - - // Do nothing if the tabsize is 0. - if ( tabsize < 1 ) - { - return; - } - - // Get the current row, column. - int row = cursor.row; - int col = cursor.col; - const char* p = stamp; - assert( p ); - - while ( p < now ) - { - // Treat p as unsigned, so we have a happy compiler. - const unsigned char* pU = (const unsigned char*)p; - - // Code contributed by Fletcher Dunn: (modified by lee) - switch (*pU) { - case 0: - // We *should* never get here, but in case we do, don't - // advance past the terminating null character, ever - return; - - case '\r': - // bump down to the next line - ++row; - col = 0; - // Eat the character - ++p; - - // Check for \r\n sequence, and treat this as a single character - if (*p == '\n') { - ++p; - } - break; - - case '\n': - // bump down to the next line - ++row; - col = 0; - - // Eat the character - ++p; - - // Check for \n\r sequence, and treat this as a single - // character. (Yes, this bizarre thing does occur still - // on some arcane platforms...) - if (*p == '\r') { - ++p; - } - break; - - case '\t': - // Eat the character - ++p; - - // Skip to next tab stop - col = (col / tabsize + 1) * tabsize; - break; - - case TIXML_UTF_LEAD_0: - if ( encoding == TIXML_ENCODING_UTF8 ) - { - if ( *(p+1) && *(p+2) ) - { - // In these cases, don't advance the column. These are - // 0-width spaces. - if ( *(pU+1)==TIXML_UTF_LEAD_1 && *(pU+2)==TIXML_UTF_LEAD_2 ) - p += 3; - else if ( *(pU+1)==0xbfU && *(pU+2)==0xbeU ) - p += 3; - else if ( *(pU+1)==0xbfU && *(pU+2)==0xbfU ) - p += 3; - else - { p +=3; ++col; } // A normal character. - } - } - else - { - ++p; - ++col; - } - break; - - default: - if ( encoding == TIXML_ENCODING_UTF8 ) - { - // Eat the 1 to 4 byte utf8 character. - int step = TiXmlBase::utf8ByteTable[*((unsigned char*)p)]; - if ( step == 0 ) - step = 1; // Error case from bad encoding, but handle gracefully. - p += step; - - // Just advance one column, of course. - ++col; - } - else - { - ++p; - ++col; - } - break; - } - } - cursor.row = row; - cursor.col = col; - assert( cursor.row >= -1 ); - assert( cursor.col >= -1 ); - stamp = p; - assert( stamp ); -} - - -const char* TiXmlBase::SkipWhiteSpace( const char* p, TiXmlEncoding encoding ) -{ - if ( !p || !*p ) - { - return p; - } - if ( encoding == TIXML_ENCODING_UTF8 ) - { - while ( *p ) - { - const unsigned char* pU = (const unsigned char*)p; - - // Skip the stupid Microsoft UTF-8 Byte order marks - if ( *(pU+0)==TIXML_UTF_LEAD_0 - && *(pU+1)==TIXML_UTF_LEAD_1 - && *(pU+2)==TIXML_UTF_LEAD_2 ) - { - p += 3; - continue; - } - else if(*(pU+0)==TIXML_UTF_LEAD_0 - && *(pU+1)==0xbfU - && *(pU+2)==0xbeU ) - { - p += 3; - continue; - } - else if(*(pU+0)==TIXML_UTF_LEAD_0 - && *(pU+1)==0xbfU - && *(pU+2)==0xbfU ) - { - p += 3; - continue; - } - - if ( IsWhiteSpace( *p ) || *p == '\n' || *p =='\r' ) // Still using old rules for white space. - ++p; - else - break; - } - } - else - { - while ( *p && ( IsWhiteSpace( *p ) || *p == '\n' || *p =='\r' ) ) - ++p; - } - - return p; -} - -#ifdef TIXML_USE_STL -/*static*/ bool TiXmlBase::StreamWhiteSpace( TIXML_ISTREAM * in, TIXML_STRING * tag ) -{ - for( ;; ) - { - if ( !in->good() ) return false; - - int c = in->peek(); - // At this scope, we can't get to a document. So fail silently. - if ( !IsWhiteSpace( c ) || c <= 0 ) - return true; - - *tag += (char) in->get(); - } -} - -/*static*/ bool TiXmlBase::StreamTo( TIXML_ISTREAM * in, int character, TIXML_STRING * tag ) -{ - //assert( character > 0 && character < 128 ); // else it won't work in utf-8 - while ( in->good() ) - { - int c = in->peek(); - if ( c == character ) - return true; - if ( c <= 0 ) // Silent failure: can't get document at this scope - return false; - - in->get(); - *tag += (char) c; - } - return false; -} -#endif - -const char* TiXmlBase::ReadName( const char* p, TIXML_STRING * name, TiXmlEncoding encoding ) -{ - *name = ""; - // Names start with letters or underscores. - // Of course, in unicode, tinyxml has no idea what a letter *is*. The - // algorithm is generous. - // - // After that, they can be letters, underscores, numbers, - // hyphens, or colons. (Colons are valid ony for namespaces, - // but tinyxml can't tell namespaces from names.) - if ( !(p && *p) ) - return 0; - if ( IsAlpha( (unsigned char) *p, encoding ) || *p == '_' ) - { - while( *p && - ( IsAlphaNum( (unsigned char)*p, encoding ) || - *p == '_' || *p == '-' || *p == '.' || *p == ':' ) ) - { - (*name) += *p; - ++p; - } - } - return p; -} - -const char* TiXmlBase::GetEntity( const char* p, char* value, int* length, TiXmlEncoding encoding ) -{ - // Presume an entity, and pull it out. - TIXML_STRING ent; - int i; - *length = 0; - - if ( *(p+1) && *(p+1) == '#' && *(p+2) ) - { - unsigned long ucs = 0; - ptrdiff_t delta = 0; - unsigned mult = 1; - - if ( *(p+2) == 'x' ) - { - // Hexadecimal. - if ( !*(p+3) ) return 0; - - const char* q = p+3; - q = strchr( q, ';' ); - - if ( !q || !*q ) return 0; - - delta = q-p; - --q; - - while ( *q != 'x' ) - { - if ( *q >= '0' && *q <= '9' ) - ucs += mult * (*q - '0'); - else if ( *q >= 'a' && *q <= 'f' ) - ucs += mult * (*q - 'a' + 10); - else if ( *q >= 'A' && *q <= 'F' ) - ucs += mult * (*q - 'A' + 10 ); - else - return 0; - mult *= 16; - --q; - } - } - else - { - // Decimal. - if ( !*(p+2) ) return 0; - - const char* q = p+2; - q = strchr( q, ';' ); - - if ( !q || !*q ) return 0; - - delta = q-p; - --q; - - while ( *q != '#' ) - { - if ( *q >= '0' && *q <= '9' ) - ucs += mult * (*q - '0'); - else - return 0; - mult *= 10; - --q; - } - } - if ( encoding == TIXML_ENCODING_UTF8 ) - { - // convert the UCS to UTF-8 - ConvertUTF32ToUTF8( ucs, value, length ); - } - else - { - *value = (char)ucs; - *length = 1; - } - return p + delta + 1; - } - - // Now try to match it. - for( i=0; iappend( cArr, len ); - } - } - else - { - bool whitespace = false; - - // Remove leading white space: - p = SkipWhiteSpace( p, encoding ); - while ( p && *p && !StringEqual( p, endTag, caseInsensitive, encoding, eof ) ) - { - if ( IsWhiteSpace( *p ) ) - { - whitespace = true; - ++p; - } - else - { - // If we've found whitespace, add it before the - // new character. Any whitespace just becomes a space. - if ( whitespace ) - { - (*text) += ' '; - whitespace = false; - } - int len; - char cArr[4] = { 0, 0, 0, 0 }; - p = GetChar( p, cArr, &len, encoding ); - if ( len == 1 ) - (*text) += cArr[0]; // more efficient - else - text->append( cArr, len ); - } - } - } - - // Check end of data - if (eof && *eof) - return 0; - - /* - p is 0 - GetChar returned 0: invalid data - *p is 0 - End of data. endTag not found - *p is non 0 - endTag found - */ - if (!(p && *p)) { - if (eof) - *eof = (p ? true : false); - return 0; - } - return p + strlen( endTag ); -} - -#ifdef TIXML_USE_STL - -void TiXmlDocument::StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ) -{ - // The basic issue with a document is that we don't know what we're - // streaming. Read something presumed to be a tag (and hope), then - // identify it, and call the appropriate stream method on the tag. - // - // This "pre-streaming" will never read the closing ">" so the - // sub-tag can orient itself. - - if ( !StreamTo( in, '<', tag ) ) - { - SetError( TIXML_ERROR_PARSING_EMPTY, 0, 0, TIXML_ENCODING_UNKNOWN ); - return; - } - - while ( in->good() ) - { - int tagIndex = (int) tag->length(); - while ( in->good() && in->peek() != '>' ) - { - int c = in->get(); - if ( c <= 0 ) - { - SetError( TIXML_ERROR_EMBEDDED_NULL, 0, 0, TIXML_ENCODING_UNKNOWN ); - break; - } - (*tag) += (char) c; - } - - if ( in->good() ) - { - // We now have something we presume to be a node of - // some sort. Identify it, and call the node to - // continue streaming. - TiXmlNode* node = Identify( tag->c_str() + tagIndex, TIXML_DEFAULT_ENCODING ); - - if ( node ) - { - node->StreamIn( in, tag ); - bool isElement = node->ToElement() != 0; - delete node; - node = 0; - - // If this is the root element, we're done. Parsing will be - // done by the >> operator. - if ( isElement ) - { - return; - } - } - else - { - SetError( TIXML_ERROR, 0, 0, TIXML_ENCODING_UNKNOWN ); - return; - } - } - } - // We should have returned sooner. - SetError( TIXML_ERROR, 0, 0, TIXML_ENCODING_UNKNOWN ); -} - -#endif - -const char* TiXmlDocument::Parse( const char* p, TiXmlParsingData* prevData, TiXmlEncoding encoding ) -{ - ClearError(); - - // Parse away, at the document level. Since a document - // contains nothing but other tags, most of what happens - // here is skipping white space. - if ( !p || !*p ) - return p; - - // Note that, for a document, this needs to come - // before the while space skip, so that parsing - // starts from the pointer we are given. - location.Clear(); - if ( prevData ) - { - location.row = prevData->cursor.row; - location.col = prevData->cursor.col; - } - else - { - location.row = 0; - location.col = 0; - } - TiXmlParsingData data( p, TabSize(), location.row, location.col ); - location = data.Cursor(); - - if ( encoding == TIXML_ENCODING_UNKNOWN ) - { - // Check for the Microsoft UTF-8 lead bytes. - const unsigned char* pU = (const unsigned char*)p; - if ( *(pU+0) && *(pU+0) == TIXML_UTF_LEAD_0 - && *(pU+1) && *(pU+1) == TIXML_UTF_LEAD_1 - && *(pU+2) && *(pU+2) == TIXML_UTF_LEAD_2 ) - { - encoding = TIXML_ENCODING_UTF8; - useMicrosoftBOM = true; - } - } - - p = SkipWhiteSpace( p, encoding ); - - while ( p && *p ) - { -// XDebug(DebugAll,"TiXmlDocument::Parse. Parse: '%s'.",p); - TiXmlNode* node = Identify( p, encoding ); - if ( node ) - { - const char* tmp = node->Parse( p, &data, encoding ); - // Check if error ocurred - if (!tmp) { - if (ErrorId() && ErrorId() != TIXML_ERROR_INCOMPLETE) - Debug(DebugWarn, - "TiXmlDocument [%p]. Parse failed for '%s'.",this,p); - delete node; - break; - } -// XDebug(DebugAll, -// "TiXmlDocument::Parse. Node added. Type: %u.", -// node->Type()); - p = tmp; - LinkEndChild( node ); - } - else - { - break; - } - - // Did we get encoding info? - if ( encoding == TIXML_ENCODING_UNKNOWN && node->ToDeclaration() ) - { - TiXmlDeclaration* dec = node->ToDeclaration(); - const char* enc = dec->Encoding(); - assert( enc ); - - if ( *enc == 0 ) - encoding = TIXML_ENCODING_UTF8; - else if ( StringEqual( enc, "UTF-8", true, TIXML_ENCODING_UNKNOWN ) ) - encoding = TIXML_ENCODING_UTF8; - else if ( StringEqual( enc, "UTF8", true, TIXML_ENCODING_UNKNOWN ) ) - encoding = TIXML_ENCODING_UTF8; // incorrect, but be nice - else - encoding = TIXML_ENCODING_LEGACY; - } - - p = SkipWhiteSpace( p, encoding ); - } - - // All is well. - return p; -} - -void TiXmlDocument::SetError( int err, const char* pError, TiXmlParsingData* data, TiXmlEncoding encoding ) -{ - // The first error in a chain is more accurate - don't set again! - if ( error ) - return; - - assert( err > 0 && err < TIXML_ERROR_STRING_COUNT ); - error = true; - errorId = err; - errorDesc = errorString[ errorId ]; - - errorLocation.Clear(); - if ( pError && data ) - { - data->Stamp( pError, encoding ); - errorLocation = data->Cursor(); - } -} - - -TiXmlNode* TiXmlNode::Identify( const char* p, TiXmlEncoding encoding ) -{ - TiXmlNode* returnNode = 0; - - p = SkipWhiteSpace( p, encoding ); - if( !p || !*p ) - return 0; - TiXmlDocument* document = GetDocument(); - // Should have a tag start - if( *p != '<' ) - { - error(document,TIXML_ERROR_PARSING_UNKNOWN,0,0,encoding); - return 0; - } - - // What is this thing? - // - Elements start with a letter or underscore, but xml is reserved. - // - Comments: "; - value = ""; - - bool eof; - if ( !StringEqual( p, startTag, false, encoding, &eof ) ) { - // Check if p has enough data for startTag - if (eof) - return errorIncomplete(document,p,data,encoding); - return error(document,TIXML_ERROR_PARSING_COMMENT,p,data,encoding); - } - p += strlen( startTag ); - eof = false; - p = ReadText( p, &value, false, endTag, false, encoding, &eof ); - if (!p) { - if (eof) - return errorIncomplete(document,p,data,encoding); - return error(document,TIXML_ERROR_PARSING_COMMENT,p,data,encoding); - } - return p; -} - - -const char* TiXmlAttribute::Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ) -{ -// XDebug(DebugAll,"TiXmlAttribute::Parse."); - p = SkipWhiteSpace( p, encoding ); - if ( !p ) - return 0; - if ( !*p ) - return errorIncomplete(document,p,data,encoding); - - int tabsize = 4; - if ( document ) - tabsize = document->TabSize(); - - if ( data ) - { - data->Stamp( p, encoding ); - location = data->Cursor(); - } - // Read the name, the '=' and the value. - const char* pErr = p; - p = ReadName( p, &name, encoding ); - // p = 0 or *p = 0: Reached the end of input data - if ( !p ) - return errorIncomplete(document,pErr,data,encoding); - - p = SkipWhiteSpace( p, encoding ); - if ( !(p && *p) ) - return errorIncomplete(document,p,data,encoding); - if ( *p != '=' ) { - DDebug(DebugWarn, - "TiXmlAttribute::Parse. Parse error. Expect '='."); - return error(document,TIXML_ERROR_READING_ATTRIBUTES,p,data,encoding ); - } - - ++p; // skip '=' - p = SkipWhiteSpace( p, encoding ); - if ( !(p && *p) ) - return errorIncomplete(document,p,data,encoding); - - if ( *p == '\'' || *p == '"' ) - { - const char* end = (*p == '\'') ? "\'" : "\""; - ++p; - bool eof = false; - p = ReadText( p, &value, false, end, false, encoding, &eof ); - // Check end of data or error reading text - if ( !p ) { - if (eof) - return errorIncomplete(document,pErr,data,encoding); - Debug(DebugWarn, - "TiXmlAttribute::Parse. Error reading attribute value."); - return error(document,TIXML_ERROR_READING_ATTRIBUTES,pErr,data,encoding ); - } - } - else - { - // All attribute values should be in single or double quotes. - // But this is such a common error that the parser will try - // its best, even without them. - value = ""; - while ( *p // existence - && !IsWhiteSpace( *p ) && // whitespace - *p != '/' && *p != '>' ) // tag end - { - value += *p; - ++p; - } - } -// XDebug(DebugAll,"TiXmlAttribute::Parse. Attribute read. '%s=%s'", -// name.c_str(),value.c_str()); - return p; -} - -#ifdef TIXML_USE_STL -void TiXmlText::StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ) -{ - if ( cdata ) - { - int c = in->get(); - if ( c <= 0 ) - { - TiXmlDocument* document = GetDocument(); - if ( document ) - document->SetError( TIXML_ERROR_EMBEDDED_NULL, 0, 0, TIXML_ENCODING_UNKNOWN ); - return; - } - - (*tag) += (char) c; - - if ( c == '>' - && tag->at( tag->length() - 2 ) == ']' - && tag->at( tag->length() - 3 ) == ']' ) - { - // All is well. - return; - } - } - else - { - while ( in->good() ) - { - int c = in->peek(); - if ( c == '<' ) - return; - if ( c <= 0 ) - { - TiXmlDocument* document = GetDocument(); - if ( document ) - document->SetError( TIXML_ERROR_EMBEDDED_NULL, 0, 0, TIXML_ENCODING_UNKNOWN ); - return; - } - - (*tag) += (char) c; - in->get(); - } - } -} -#endif - -const char* TiXmlText::Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding ) -{ -// XDebug(DebugAll,"TiXmlText::Parse."); - value = ""; - TiXmlDocument* document = GetDocument(); - - if ( data ) - { - data->Stamp( p, encoding ); - location = data->Cursor(); - } - - const char* const startTag = ""; - - bool eof = false; - - if ( cdata || StringEqual( p, startTag, false, encoding ) ) - { - cdata = true; - - if ( !StringEqual( p, startTag, false, encoding, &eof ) ) - { - if (eof) - return errorIncomplete(document,p,data,encoding); - Debug(DebugWarn,"TiXmlText::Parse. Incorrect start tag."); - return error(document,TIXML_ERROR_PARSING_CDATA,p,data,encoding); - } - - p += strlen( startTag ); - - // Keep all the white space, ignore the encoding, etc. - while ( p && *p && !StringEqual( p, endTag, false, encoding, &eof ) ) - { - value += *p; - ++p; - } - if (eof) - return errorIncomplete(document,p,data,encoding); - - TIXML_STRING dummy; - p = ReadText( p, &dummy, false, endTag, false, encoding, &eof ); - if (eof) - return errorIncomplete(document,p,data,encoding); - return p; - } - - else - { - bool ignoreWhite = true; - - const char* end = "<"; - p = ReadText( p, &value, ignoreWhite, end, false, encoding, &eof ); - if (eof) - return errorIncomplete(document,p,data,encoding); - if ( p ) - return p-1; // don't truncate the '<' - Debug(DebugWarn,"TiXmlText::Parse. Error reading text."); - return 0; - } -} - -#ifdef TIXML_USE_STL -void TiXmlDeclaration::StreamIn( TIXML_ISTREAM * in, TIXML_STRING * tag ) -{ - while ( in->good() ) - { - int c = in->get(); - if ( c <= 0 ) - { - TiXmlDocument* document = GetDocument(); - if ( document ) - document->SetError( TIXML_ERROR_EMBEDDED_NULL, 0, 0, TIXML_ENCODING_UNKNOWN ); - return; - } - (*tag) += (char) c; - - if ( c == '>' ) - { - // All is well. - return; - } - } -} -#endif - -const char* TiXmlDeclaration::Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding _encoding ) -{ -// XDebug(DebugAll,"TiXmlDeclaration::Parse. '%s'.",p); - p = SkipWhiteSpace( p, _encoding ); - // Find the beginning, find the end, and look for - // the stuff in-between. - TiXmlDocument* document = GetDocument(); - - if ( !(p && *p) ) - return errorIncomplete(document,0,0,_encoding); - - bool eof = false; - if ( !StringEqual( p,"Stamp( p, _encoding ); - location = data->Cursor(); - } - - p += 5; - version = ""; - encoding = ""; - standalone = ""; - - while ( !eof && p && *p ) - { - if ( *p == '>' ) - { - ++p; - return p; - } - - p = SkipWhiteSpace( p, _encoding ); - if ( StringEqual( p, "version", true, _encoding ) ) - { - TiXmlAttribute attrib; - p = attrib.Parse( p, data, _encoding ); - version = attrib.Value(); - } - else if (!eof && StringEqual( p, "encoding", true, _encoding ) ) - { - TiXmlAttribute attrib; - p = attrib.Parse( p, data, _encoding ); - encoding = attrib.Value(); - } - else if (!eof && StringEqual( p, "standalone", true, _encoding ) ) - { - TiXmlAttribute attrib; - p = attrib.Parse( p, data, _encoding ); - standalone = attrib.Value(); - } - else - { - // Read over whatever it is. - if (!eof && p) { - while(*p && *p != '>' && !IsWhiteSpace( *p ) ) - ++p; - } - } - } - return errorIncomplete(document,0,0,_encoding); -} - -bool TiXmlText::Blank() const -{ - for ( unsigned i=0; i + +/** + * Holds all Telephony Engine related classes. + */ +namespace TelEngine { + +class XmlSaxParser; +class XmlDomParser; +class XmlDeclaration; +class XmlFragment; +class XmlChild; +class XmlParent; +class XmlDocument; +class XmlElement; +class XmlComment; +class XmlCData; +class XmlText; +class XmlDoctype; +struct XmlEscape; + + +/** + * A Serial Access Parser (SAX) for arbitrary XML data + * @short Serial Access XML Parser + */ +class YATE_API XmlSaxParser : public DebugEnabler +{ +public: + enum Error { + NoError = 0, + NotWellFormed, + Unknown, + IOError, + ElementParse, + ReadElementName, + InvalidElementName, + ReadingAttributes, + CommentParse, + DeclarationParse, + DefinitionParse, + CDataParse, + ReadingEndTag, + Incomplete, + InvalidEncoding, + UnsupportedEncoding, + UnsupportedVersion, + }; + enum Type { + None = 0, + Text = 1, + CData = 2, + Element = 3, + Doctype = 4, + Comment = 5, + Declaration = 6, + Instruction = 7, + EndTag = 8, + Special = 9 + }; + + /** + * Destructor + */ + virtual ~XmlSaxParser(); + + /** + * Get the number of bytes successfully parsed + * @return The number of bytes successfully parsed + */ + inline unsigned int offset() const + { return m_offset; } + + /** + * Get the row where the parser has found an error + * @return The row number + */ + inline unsigned int row() const + { return m_row; } + + /** + * Get the column where the parser has found an error + * @return The column number + */ + inline unsigned int column() const + { return m_column; } + + /** + * Retrieve the parser's buffer + * @return The parser's buffer + */ + inline const String& buffer() const + { return m_buf; } + + /** + * Parse a given string + * @param data The data to parse + * @return True if all data was successfully parsed + */ + bool parse(const char* data); + + /** + * Get the error code found while parsing + * @return Error code + */ + inline Error error() + { return m_error; } + + /** + * Set the error code and destroys a child if error code is not NoError + * @param error The error found + * @param child Child to destroy + * @return False on error + */ + bool setError(Error error, XmlChild* child = 0); + + /** + * Retrieve the error string associated with current error status + * @param defVal Value to return if not found + * @return The error string + */ + inline const char* getError(const char* defVal = "Xml error") + { return getError(m_error,defVal); } + + /** + * @return The last xml type that we were parsing, but we have not finished + */ + inline Type unparsed() + { return m_unparsed; } + + /** + * Set the last xml type that we were parsing, but we have not finished + * @param id The xml type that we haven't finish to parse + */ + inline void setUnparsed(Type id) + { m_unparsed = id;} + + /** + * Reset error flag + */ + virtual void reset(); + + /** + * @return The internal buffer + */ + const String& getBuffer() const + { return m_buf; } + + /** + * Retrieve the error string associated with a given error code + * @param defVal Value to return if not found + * @return The error string + */ + static inline const char* getError(int code, const char* defVal = "Xml error") + { return lookup(code,s_errorString,defVal); } + + /** + * XmlEscape the given text + * @param buf Destination buffer + * @param text The text to escape + */ + static void escape(String& buf, const String& text); + + /** + * Errors dictionary + */ + static TokenDict s_errorString[]; + + /** + * Escaped strings dictionary + */ + static XmlEscape s_escape[]; + +protected: + /** + * Constructor + * @param name Debug name + */ + XmlSaxParser(const char* name = "XmlSaxParser"); + + /** + * Parse an instruction form the main buffer. + * Extracts the parsed string from buffer if returns true + * @return True if the instruction was parsed successfully + */ + bool parseInstruction(); + + /** + * Parse a CData section form the main buffer. + * Extracts the parsed string from buffer if returns true + * @return True if the CData section was parsed successfully + */ + bool parseCData(); + + /** + * Parse a comment form the main buffer. + * Extracts the parsed string from buffer if returns true + * @return True if the comment was parsed successfully + */ + bool parseComment(); + + /** + * Parse an element form the main buffer. + * Extracts the parsed string from buffer if returns true + * @return True if the element was parsed successfully + */ + bool parseElement(); + + /** + * Parse a declaration form the main buffer. + * Extracts the parsed string from buffer if returns true + * @return True if the declaration was parsed successfully + */ + bool parseDeclaration(); + + /** + * Helper method to classify the Xml objects starting with "'; } + + /** + * Reset the error + */ + inline void resetError() + { m_error = NoError; } + + /** + * Reset parsed value and parameters + */ + inline void resetParsed() + { m_parsed.clear(); m_parsed.clearParams(); } + + /** + * Extract the name of an element or instruction + * @return The extracted string or 0 + */ + String* extractName(bool& empty); + + /** + * Extract an attribute + * @return The attribute value or 0 + */ + NamedString* getAttribute(); + + /** + * Verify if the given character is in the range allowed + * to be first character from a xml tag + * @param ch The character to check + * @return True if the character is in range + */ + bool checkFirstNameCharacter(unsigned char ch); + + /** + * Check if the given character is in the range allowed for an xml char + * @param c The character to check + * @return True if the character is in range + */ + bool checkDataChar(unsigned char c); + + /** + * Verify if the given character is in the range allowed for a xml name + * @param ch The character to check + * @return True if the character is in range + */ + bool checkNameCharacter(unsigned char ch); + + /** + * Callback method. Is called when a comment was successfully parsed. + * Default implementation does nothing + * @param text The comment content + */ + virtual void gotComment(const String& text) + { } + + /** + * Callback method. Is called when an instruction was successfully parsed. + * Default implementation does nothing + * @param instr The instruction content + */ + virtual void gotProcessing(const NamedString& instr) + { } + + /** + * Callback method. Is called when a declaration was successfully parsed. + * Default implementation does nothing + * @param decl The declaration content + */ + virtual void gotDeclaration(const NamedList& decl) + { } + + /** + * Callback method. Is called when a text was successfully parsed. + * Default implementation does nothing + * @param text The text content + */ + virtual void gotText(const String& text) + { } + + /** + * Callback method. Is called when a CData section was successfully parsed. + * Default implementation does nothing + * @param data The CData content + */ + virtual void gotCdata(const String& data) + { } + + /** + * Callback method. Is called when an element was successfully parsed. + * Default implementation does nothing + * @param element The element content + * @param empty True if the element does not have attributes + */ + virtual void gotElement(const NamedList& element, bool empty) + { } + + /** + * Callback method. Is called when a end tag was successfully parsed. + * Default implementation does nothing + * @param name The end tag name + */ + virtual void endElement(const String& name) + { } + + /** + * Callback method. Is called when a doctype was successfully parsed. + * Default implementation does nothing + * @param doc The doctype content + */ + virtual void gotDoctype(const String& doc) + { } + + /** + * Callback method. Is called to check if we have an incomplete element. + * Default implementation returns always true + * @return True + */ + virtual bool completed() + { return true; } + + /** + * Check duplicate namespace values. Calls gotElement() if ok. + * Reset parsed if gotElement() was called + * @param list The list element and its attributes + * @param empty True if the element does not have attributes + * @return False if duplicate namespace values were found and the error was set + */ + bool processElement(NamedList& list, bool empty); + + /** + * The offset where the parser was stop + */ + unsigned int m_offset; + + /** + * The row where the parser was stop + */ + unsigned int m_row; + + /** + * The column where the parser was stop + */ + unsigned int m_column; + + /** + * The error code found while parsing data + */ + Error m_error; + + /** + * The main buffer + */ + String m_buf; + + /** + * The parser data holder. + * Keeps the parsed data when an incomplete xml object is found + */ + NamedList m_parsed; + + /** + * The last parsed xml object code + */ + Type m_unparsed; +}; + +/** + * Xml Parent for a Xml child + * @short Xml Parent + */ +class YATE_API XmlParent +{ +public: + /** + * Constructor + */ + XmlParent() + {} + + /** + * Get an XmlDocument object from this XmlParent. + * Default implementation return 0 + * @return 0 + */ + virtual XmlDocument* document() + { return 0; } + + /** + * Get an XmlFragment object from this XmlParent. + * Default implementation return 0 + * @return 0 + */ + virtual XmlFragment* fragment() + { return 0; } + + /** + * Get an XmlElement object from this XmlParent. + * Default implementation return 0 + * @return 0 + */ + virtual XmlElement* element() + { return 0; } + + /** + * Append a new child to this XmlParent + * @param child The child to append + * @return XmlNoError if the child was successfully added + */ + virtual XmlSaxParser::Error addChild(XmlChild* child) = 0; + + /** + * Remove a child + * @param child The child to remove + * @param delObj True to delete the object + * @return XmlChild pointer if found and not deleted + */ + virtual XmlChild* removeChild(XmlChild* child, bool delObj = true) = 0; + + /** + * Reset this xml parent. + * Default implementation does nothing + */ + virtual void reset() + { } + + /** + * Obtain this xml parent children. + * Default implementation returns an empty list + * @return The list of children + */ + virtual const ObjList& getChildren() const + { return ObjList::empty(); } + + /** + * Clear this xml parent children. + * Default implementation does nothing + */ + virtual void clearChildren() + { } +}; + +/** + * A Document Object Model (DOM) parser for XML documents and fragments + * @short Document Object Model XML Parser + */ +class YATE_API XmlDomParser : public XmlSaxParser +{ + friend class XmlChild; +public: + /** + * XmlDomParser constructor + * @param name Debug name + * @param fragment True if this parser needs to parse a piece of a xml document + */ + XmlDomParser(const char* name = "XmlDomParser", bool fragment = false); + + /** + * XmlDomParser constructor + * @param fragment The fragment who should keep the parsed data + */ + XmlDomParser(XmlParent* fragment); + + /** + * Destructor + */ + virtual ~XmlDomParser(); + + /** + * Obtain an XmlDocument from the parsed data + * @return The XmlDocument or 0 + */ + XmlDocument* document() + { return m_data->document(); } + + /** + * Obtain an XmlFragment from the parsed data + * @return The XmlFragment or 0 + */ + XmlFragment* fragment() + { return m_data->fragment(); } + + /** + * Reset parser + */ + virtual void reset(); + + /** + * Check if the current element is the given one + * @param el The element to compare with + * @return True if they are equal + */ + inline bool isCurrent(const XmlElement* el) const + { return el == m_current; } + +protected: + + /** + * Append a xml comment in the xml tree + * @param text The comment content + */ + virtual void gotComment(const String& text); + + /** + * Append a xml instruction in the xml tree + * @param instr The instruction content + */ + virtual void gotProcessing(const NamedString& instr); + + /** + * Append a xml declaration in the xml tree + * @param decl The declaration content + */ + virtual void gotDeclaration(const NamedList& decl); + + /** + * Append a xml text in the xml tree + * @param text The text content + */ + virtual void gotText(const String& text); + + /** + * Append a xml CData in the xml tree + * @param data The CData content + */ + virtual void gotCdata(const String& data); + + /** + * Append a xml element in the xml tree + * @param element The element content + * @param empty True if the element does not have attributes + */ + virtual void gotElement(const NamedList& element, bool empty); + + /** + * Complete current element + * @param name The end tag name + */ + virtual void endElement(const String& name); + + /** + * Append a xml doctype in the xml tree + * @param doc The doctype content + */ + virtual void gotDoctype(const String& doc); + + /** + * Callback method. Is called to check if we have an incomplete element + * @return True if current element is not 0 + */ + virtual bool completed() + { return m_current == 0; } + +private: + XmlElement* m_current; // The current xml element + XmlParent* m_data; // Main xml fragment +}; + +/** + * Xml Child for Xml document + * @short Xml Child + */ +class YATE_API XmlChild : public GenObject +{ + YCLASS(XmlChild,GenObject) + friend class XmlDomParser; +public: + /** + * Constructor + */ + XmlChild(); + + /** + * Set this child's parent + * @param parent Parent of this child + */ + virtual void setParent(XmlParent* parent) + { } + + /** + * Get a Xml element + * @return 0 + */ + virtual XmlElement* xmlElement() + { return 0; } + + /** + * Get a Xml comment + * @return 0 + */ + virtual XmlComment* xmlComment() + { return 0; } + + /** + * Get a Xml CData + * @return 0 + */ + virtual XmlCData* xmlCData() + { return 0; } + + /** + * Get a Xml text + * @return 0 + */ + virtual XmlText* xmlText() + { return 0; } + + /** + * Get a Xml declaration + * @return 0 + */ + virtual XmlDeclaration* xmlDeclaration() + { return 0; } + + /** + * Get a Xml doctype + * @return 0 + */ + virtual XmlDoctype* xmlDoctype() + { return 0; } +}; + + +/** + * Xml Declaration for Xml document + * @short Xml Declaration + */ +class YATE_API XmlDeclaration : public XmlChild +{ + YCLASS(XmlDeclaration,XmlChild) +public: + /** + * Constructor + * @param version XML version attribute + * @param enc Encoding attribute + */ + XmlDeclaration(const char* version = "1.0", const char* enc = "utf-8"); + + /** + * Constructor + * @param decl Declaration attributes + */ + XmlDeclaration(const NamedList& decl); + + /** + * Copy constructor + * @param orig Original XmlDeclaration + */ + XmlDeclaration(const XmlDeclaration& orig); + + /** + * Destructor + */ + ~XmlDeclaration(); + + /** + * Obtain the tag name and attributes list + * @return The declaration + */ + inline const NamedList& getDec() const + { return m_declaration; } + + /** + * Get the Xml declaration + * @return This object + * Reimplemented from XmlChild + */ + virtual XmlDeclaration* xmlDeclaration() + { return this; } + + /** + * Build a String from this XmlDeclaration + * @param dump The string where to append representation + * @param escape True if the attributes values need to be escaped + */ + void toString(String& dump, bool escape = true) const; + +private: + NamedList m_declaration; // The declaration +}; + +/** + * Xml Fragment a fragment from a Xml document + * @short Xml Fragment + */ +class YATE_API XmlFragment : public XmlParent +{ +public: + + /** + * Constructor + */ + XmlFragment(); + + /** + * Copy constructor + * @param orig Original XmlFragment + */ + XmlFragment(const XmlFragment& orig); + + /** + * Destructor + */ + virtual ~XmlFragment(); + + /** + * Get an Xml Fragment + * @return This + */ + virtual XmlFragment* fragment() + { return this; } + + /** + * Get the list of children + * @return The children list + */ + virtual const ObjList& getChildren() const + { return m_list; } + + /** + * Append a new xml child to this fragment + * @param child the child to append + * @return An error code if an error was detected + */ + virtual XmlSaxParser::Error addChild(XmlChild* child); + + /** + * Reset this Xml Fragment + */ + virtual void reset(); + + /** + * Remove the first child from list and returns it + * @return XmlChild pointer or 0 + */ + inline XmlChild* pop() + { return static_cast(m_list.remove(false)); } + + /** + * Remove a child. Reset the parent of not deleted xml element + * @param child The child to remove + * @param delObj True to delete the object + * @return XmlChild pointer if found and not deleted + */ + virtual XmlChild* removeChild(XmlChild* child, bool delObj = true); + + /** + * Clear the list of children + */ + virtual void clearChildren() + { m_list.clear(); } + + /** + * Build a String from this XmlFragment + * @param dump The string where to append representation + * @param escape True if the attributes values need to be escaped + * @param indent Spaces for output + * @param origindent Original indent + * @param completeOnly True to build only if complete + * @param auth Optional list of tag and attribute names to be replaced with '***'. This + * parameter can be used when the result will be printed to output to avoid printing + * authentication data to output. The array must end with an empty string + * @param parent Optional parent element whose tag will be searched in the auth list + */ + void toString(String& dump, bool escape = true, const String& indent = String::empty(), + const String& origindent = String::empty(), bool completeOnly = true, + const String* auth = 0, const XmlElement* parent = 0) const; + + /** + * Find a completed xml element in a list + * @param list The list to search for the element + * @param name Optional element tag to match + * @param ns Optional element namespace to match + * @return XmlElement pointer or 0 if not found + */ + static XmlElement* findElement(ObjList* list, const String* name, const String* ns); + +private: + ObjList m_list; // The children list +}; + +/** + * Xml Document + * @short Xml Document + */ +class YATE_API XmlDocument : public XmlParent +{ +public: + + /** + * The Constructor + */ + XmlDocument(); + + /** + * Destructor + */ + virtual ~XmlDocument(); + + /** + * Get an Xml Document + * @return This + */ + virtual XmlDocument* document() + { return this; } + + /** + * Append a new child to this document. + * Set the root to an XML element if not already set. If we already have a completed root + * the element will be added to the root, otherwise an error will be returned. + * If we don't have a root non xml elements (other then text) will be added the list + * of elements before root + * @param child The child to append + * @return An error code if an error was detected + */ + virtual XmlSaxParser::Error addChild(XmlChild* child); + + /** + * Retrieve the document declaration + * @return XmlDeclaration pointer or 0 if not found + */ + XmlDeclaration* declaration() const; + + /** + * Retrieve the root element + * @param completed True to retrieve the root element if is not completed + * @return Root pointer or 0 if not found or is not completed + */ + XmlElement* root(bool completed = false) const; + + /** + * Reset this Xml Document + */ + virtual void reset(); + + /** + * Remove a child + * @param child The child to remove + * @param delObj True to delete the object + * @return XmlChild pointer if found and not deleted + */ + virtual XmlChild* removeChild(XmlChild* child, bool delObj = true) + { return m_beforeRoot.removeChild(child,delObj); } + + /** + * Load this document from data stream and parse it. + * @param stream The input stream + * @param error Optional pointer to data to be filled with error if IOError is returned + * @return Parser error (NoError on success) + */ + virtual XmlSaxParser::Error read(Stream& in, int* error = 0); + + /** + * Write this document to a data stream. + * A indent + n * origindent will be added before each xml child, + * where n is the imbrication level, starting with 0. + * A indent + (n + 1) * origindent will be added before each attribute + * @param stream The output stream + * @param escape True if the attributes values need to be escaped + * @param indent Line indent + * @param origindent Original indent + * @param completeOnly True to build only if complete + * @return Written bytes, negative on error + */ + virtual int write(Stream& out, bool escape = true, + const String& indent = String::empty(), const String& origindent = String::empty(), + bool completeOnly = true) const; + + /** + * Load an file an parse it + + * Reset the document + + * @param file The file to load + * @param error Pointer to data to be filled with file error if IOError is returned + * @return Parser error (NoError on success) + */ + XmlSaxParser::Error loadFile(const char* file, int* error = 0); + + /** + * Save this xml document in the specified file. Create a new fle if not found. + * Truncate an existing one + * @param file The file to save or will be used the file used on load + * @param escape True if the attributes values need to be escaped + * @param indent Spaces for output + * @param completeOnly True to build only if complete + * @return 0 on success, error code on failure + */ + int saveFile(const char* file = 0, bool escape = true, + const String& indent = String::empty(), bool completeOnly = true) const; + + /** + * Build a String from this XmlDocument + * @param dump The string where to append representation + * @param escape True if the attributes values need to be escaped + * @param indent Spaces for output + * @param origindent Original indent + */ + void toString(String& dump, bool escape = true, const String& indent = String::empty(), + const String& origindent = String::empty()) const; + +private: + XmlElement* m_root; // The root element + XmlFragment m_beforeRoot; // XML children before root (declaration ...) + String m_file; // The file name used on load +}; + + +/** + * Xml Element from a Xml document + * @short Xml Element + */ + +class YATE_API XmlElement : public XmlChild, public XmlParent +{ + YCLASS(XmlElement,XmlChild) +public: + /** + * Constructor + * @param element The NamedList name represent the element name and the param the attributes + * @param empty False if has children + * @param parent The parent of this element + */ + XmlElement(const NamedList& element, bool empty, XmlParent* parent = 0); + + /** + * Constructor. Creates a new complete and empty element + * @param name The name of the element + * @param complete False to build an incomplete element + */ + XmlElement(const char* name, bool complete = true); + + /** + * Copy constructor + * @param orig Original XmlElement + */ + XmlElement(const XmlElement& orig); + + /** + * Destructor + */ + virtual ~XmlElement(); + + /** + * Retrieve the element's tag + * @return The element's tag + */ + inline const char* tag() const + { return m_element; } + + /** + * Check if this element must be processed in the default namespace (its tag is not prefixed) + * @return True if this element must be processed in the default namespace + */ + inline bool isDefaultNs() const + { return m_prefixed == 0; } + + /** + * Retrieve the element's tag unprefixed (namespace prefix removed) + * @return The element's tag unprefixed + */ + inline const String& unprefixedTag() const + { return !m_prefixed ? m_element : m_prefixed->name(); } + + /** + * Retrieve the element's tag (without prefix) + * @param tag Pointer to element tag + * @return Element tag + */ + const String& getTag() const + { return !m_prefixed ? m_element : m_prefixed->name(); } + + /** + * Retrieve the element's tag (without prefix) and namespace + * @param tag Pointer to element tag + * @param ns Pointer to element's namespace (may be 0 for unprefixed tags) + * @return True if a namespace was found for the element tag or the tag is not prefixed + */ + bool getTag(const String*& tag, const String*& ns) const; + + /** + * Get an XmlElement from this XmlChild + * @return This object + */ + virtual XmlElement* xmlElement() + { return this; } + + /** + * Get an XmlElement from this XmlParent + * @return This object + */ + virtual XmlElement* element() + { return this; } + + /** + * Append a new child of this element + * @param child The child to append + */ + virtual XmlSaxParser::Error addChild(XmlChild* child); + + /** + * Remove a child + * @param child The child to remove + * @param delObj True to delete the object + * @return XmlChild pointer if found and not deleted + */ + virtual XmlChild* removeChild(XmlChild* child, bool delObj = true); + + /** + * Notification for this element that is complete + */ + virtual void setCompleted() + { m_complete = true; } + + /** + * @return True if this element is completed + */ + inline bool completed() const + { return m_complete; } + + /** + * @return True if this element is empty + */ + inline bool empty() const + { return m_empty; } + + /** + * Retrieve an XmlElement parent of this one + * @return XmlElement pointer or 0 + */ + inline XmlElement* parent() const + { return m_parent ? m_parent->element() : 0; } + + /** + * @return The parent of this element + */ + virtual XmlParent* getParent() + { return m_parent; } + + /** + * Set this element's parent. Update inherited namespaces + * @return The parent of this element + */ + virtual void setParent(XmlParent* parent); + + /** + * @return The name of this element + */ + virtual const String& getName() const + { return m_element; } + + /** + * @return The held element + */ + virtual const NamedList& getElement() const + { return m_element; } + + /** + * Helper method to obtain the children list + * @return The children list + */ + inline const ObjList& getChildren() const + { return m_children.getChildren(); } + + /** + * Helper method to clear the children list + */ + inline void clearChildren() + { return m_children.clearChildren(); } + + /** + * Retrieve the list of inherited namespaces + * @return The list of inherited namespaces (or 0) + */ + inline const NamedList* inheritedNs() const + { return m_inheritedNs; } + + /** + * Set inherited namespaces from a given element. Reset them anyway + * @param xml The source element used to set inherited namespaces + * @param inherit Copy element's inherited namespaces if it doesn't have a parent + */ + void setInheritedNs(const XmlElement* xml = 0, bool inherit = true); + + /** + * Add inherited namespaces from a list + * @param The list of namespaces + */ + void addInheritedNs(const NamedList& list); + + /** + * Extract the first child element + * @return XmlElement pointer or 0 + */ + inline XmlElement* pop() { + XmlElement* x = findFirstChild(); + if (x) + m_children.removeChild(x,false); + return x; + } + + /** + * Retrieve the element tag + * @return The element tag + */ + virtual const String& toString() const + { return m_element; } + + /** + * Build (append to) a String from this XmlElement + * @param dump The destination string + * @param escape True if the attributes values need to be escaped + * @param indent Spaces for output + * @param origindent Original indent + * @param completeOnly True to build only if complete + * @param auth Optional list of tag and attribute names to be replaced with '***'. This + * parameter can be used when the result will be printed to output to avoid printing + * authentication data to output. The array must end with an empty string + */ + void toString(String& dump, bool escape = true, const String& indent = String::empty(), + const String& origindent = String::empty(), bool completeOnly = true, + const String* auth = 0) const; + + /** + * Find the first child of this XmlElement + * @param name Optional name of the child + * @param ns Optional child namespace + * @return The first child element meeting the requested conditions + */ + inline XmlElement* findFirstChild(const String* name = 0, const String* ns = 0) const + { return XmlFragment::findElement(getChildren().skipNull(),name,ns); } + + /** + * Finds next child of this XmlElement + * @param prev Previous child + * @param name Optional name of the child + * @param ns Optional child namespace + * @return The next found child if prev exists else the first + */ + inline XmlElement* findNextChild(XmlElement* prev = 0, const String* name = 0, + const String* ns = 0) const { + if (!prev) + return findFirstChild(name,ns); + ObjList* start = getChildren().find(prev); + return start ? XmlFragment::findElement(start->skipNext(),name,ns) : 0; + } + + /** + * @return The first XmlText found in this XmlElement children + */ + const String& getText(); + + /** + * Add a text child + * @param text Non empty text to add + */ + void addText(const char* text); + + /** + * Retrieve the list of attributes + * @return Element attributes + */ + inline const NamedList& attributes() const + { return m_element; } + + /** + * Add or replace an attribute + * @param name Attribute name + * @param value Attribute value + */ + inline void setAttribute(const String& name, const char* value) + { m_element.setParam(name,value); } + + /** + * Add or replace an attribute. Clears it if value is empty + * @param name Attribute name + * @param value Attribute value + */ + inline void setAttributeValid(const String& name, const char* value) { + if (!TelEngine::null(value)) + m_element.setParam(name,value); + else + removeAttribute(name); + } + + /** + * Obtain an attribute value for the given name + * @param name The name of the attribute + * @return Attribute value + */ + inline const char* attribute(const String& name) const + { return m_element.getValue(name); } + + /** + * Obtain an attribute value for the given name + * @param name The name of the attribute + * @return String pointer or 0 if not found + */ + inline String* getAttribute(const String& name) const + { return m_element.getParam(name); } + + /** + * Check if the element has an attribute with a requested value + * @param name The name of the attribute + * @param value The value to check + * @return True if the element has an attribute with the requested value + */ + inline bool hasAttribute(const String& name, const String& value) const { + String* a = getAttribute(name); + return a && *a == value; + } + + /** + * Remove an attribute + * @param name Attribute name + */ + inline void removeAttribute(const String& name) + { m_element.clearParam(name); } + + /** + * Retrieve the element's namespace + * @return Element's namespace or 0 if not found + */ + inline String* xmlns() const { + if (!m_prefixed) + return xmlnsAttribute(s_ns); + return xmlnsAttribute(s_nsPrefix + *m_prefixed); + } + + /** + * Retrieve a namespace attribute. Search in parent or inherited for it + * @return String pointer or 0 if not found + */ + String* xmlnsAttribute(const String& name) const; + + /** + * Verify if this element belongs to the given namespace + * @param ns The namespace to compare with + * @return True if this element belongs to the given namespace + */ + inline bool hasXmlns(const String& ns) const { + const String* x = xmlns(); + return x && *x == ns; + } + + /** + * Set the element's namespace + * @param name The namespace name (element prefix). Can be the default + * namespace attribute name (empty means the default one) + * @param addAttr True to add a non empty, not repeating, namespace attribute to the list + * @param value Namespace value (ignored if addAttr is false) + * @return True on success, false if another namespace exists with the same value + */ + bool setXmlns(const String& name = String::empty(), bool addAttr = false, + const String& value = String::empty()); + + /** + * Check if a string represents a namespace attribute name + * @param str The string to check + * @return True if the given string is the default namespace attribute name or + * a namespace attribute name prefix + */ + static inline bool isXmlns(const String& str) + { return str == s_ns || str.startsWith(s_nsPrefix); } + + /** + * Default namespace attribute name + */ + static const String s_ns; + + /** + * Namespace attribute name perfix + */ + static const String s_nsPrefix; + +private: + // Set prefixed data (tag and prefix) + inline void setPrefixed() { + TelEngine::destruct(m_prefixed); + int pos = m_element.find(":"); + if (pos != -1) + m_prefixed = new NamedString(m_element.substr(pos + 1),m_element.substr(0,pos)); + } + + XmlFragment m_children; // Children of this element + NamedList m_element; // The element + NamedString* m_prefixed; // Splitted prefixed tag (the value is the namespace prefix) + XmlParent* m_parent; // The XmlElement who holds this element + NamedList* m_inheritedNs; // Inherited namespaces (if parent is 0) + bool m_empty; // True if this element does not have any children + bool m_complete; // True if the end element tag war reported +}; + +/** + * A Xml Comment from Xml document + * @short Xml Comment + */ +class YATE_API XmlComment : public XmlChild +{ + YCLASS(XmlComment,XmlChild) +public: + /** + * Constructor + * @param comm The comment content + */ + XmlComment(const String& comm); + + /** + * Copy constructor + * @param orig Original XmlComment + */ + XmlComment(const XmlComment& orig); + + /** + * Destructor + */ + virtual ~XmlComment(); + + /** + * Get the text contained by this comment + * @return The comment + */ + inline const String& getComment() const + { return m_comment; } + + /** + * Build a String from this XmlComment + * @param dump The string where to append representation + * @param indent Spaces for output + */ + void toString(String& dump, const String& indent = String::empty()) const; + + /** + * Get the Xml comment + * @return This object + */ + virtual XmlComment* xmlComment() + { return this; } + +private: + String m_comment; // The comment +}; + +/** + * A Xml CData from Xml document + * @short Xml Declaration + */ +class YATE_API XmlCData : public XmlChild +{ + YCLASS(XmlCData,XmlChild) +public: + + /** + * Constructor + * @param data The CData content + */ + XmlCData(const String& data); + + /** + * Copy constructor + * @param orig Original XmlCData + */ + XmlCData(const XmlCData& orig); + + /** + * Destructor + */ + virtual ~XmlCData(); + + /** + * Get the CData content + * @return The content of this xml object + */ + inline const String& getCData() const + { return m_data;} + + /** + * Build a String from this XmlCData + * @param dump The string where to append representation + * @param indent Spaces for output + */ + void toString(String& dump, const String& indent = String::empty()) const; + + /** + * Get the Xml CData + * @return This object + */ + virtual XmlCData* xmlCData() + { return this; } + +private: + String m_data; // The data +}; + +/** + * A Xml Declaration for Xml document + * @short Xml Declaration + */ +class YATE_API XmlText : public XmlChild +{ + YCLASS(XmlText,XmlChild) +public: + /** + * Constructor + * @param text The text + */ + XmlText(const String& text); + + /** + * Copy constructor + * @param orig Original XmlText + */ + XmlText(const XmlText& orig); + + /** + * Destructor + */ + virtual ~XmlText(); + + /** + * @return The text kept by this Xml Text + */ + inline const String& getText() const + { return m_text; } + + /** + * Build a String from this XmlText + * @param dump The string where to append representation + * @param escape True if the text need to be escaped + * @param indent Spaces for output + * @param auth Optional list of tag and attribute names to be replaced with '***'. This + * parameter can be used when the result will be printed to output to avoid printing + * authentication data to output. The array must end with an empty string + * @param parent Optional parent element whose tag will be searched in the auth list + */ + void toString(String& dump, bool escape = true, const String& indent = String::empty(), + const String* auth = 0, const XmlElement* parent = 0) const; + + /** + * Get the Xml text + * @return This object + */ + virtual XmlText* xmlText() + { return this; } + +private: + String m_text; // The text +}; + +class YATE_API XmlDoctype : public XmlChild +{ + YCLASS(XmlDoctype,XmlChild) +public: + /** + * Constructor + * @param doctype The doctype + */ + XmlDoctype(const String& doctype); + + /** + * Copy constructor + * @param orig Original XmlDoctype + */ + XmlDoctype(const XmlDoctype& orig); + + /** + * Destructor + */ + virtual ~XmlDoctype(); + + /** + * Get the doctype held by this Xml doctype + * @return The content of this Xml doctype + */ + inline const String& getDoctype() const + { return m_doctype; } + + /** + * Get the Xml doctype + * @return This object + */ + virtual XmlDoctype* xmlDoctype() + { return this; } + + /** + * Build a String from this XmlDoctype + * @param dump The string where to append representation + * @param indent Spaces for output + */ + void toString(String& dump, const String& indent = String::empty()) const; + +private: + String m_doctype; // The document type +}; + +struct XmlEscape { + /** + * Value to match + */ + const char* value; + + /** + * Character replacement for value + */ + char replace; +}; + +}; // namespace TelEngine + +#endif /* __YATEXML_H */ + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/Makefile.in b/modules/Makefile.in index 39828ac9..52a74d90 100644 --- a/modules/Makefile.in +++ b/modules/Makefile.in @@ -30,7 +30,7 @@ PROGS := cdrbuild.yate cdrfile.yate regexroute.yate \ yrtpchan.yate ystunchan.yate \ ysipchan.yate \ yiaxchan.yate \ - yjinglechan.yate jingle/jinglefeatures.yate \ + yjinglechan.yate jabber/jabberserver.yate jabber/jbfeatures.yate \ ysockschan.yate filetransfer.yate \ server/pbxassist.yate server/dbpbx.yate server/lateroute.yate \ server/park.yate server/queues.yate server/queuesnotify.yate \ @@ -44,7 +44,10 @@ PROGS := cdrbuild.yate cdrfile.yate regexroute.yate \ server/mrcpspeech.yate \ server/ysigchan.yate \ server/ciscosm.yate \ + server/presence.yate server/subscription.yate \ + server/users.yate \ server/analog.yate server/analogdetect.yate \ + client/jabberclient.yate \ callgen.yate analyzer.yate rmanager.yate msgsniff.yate LIBS := DIRS := client server @@ -243,9 +246,9 @@ yiaxchan.yate: ../libs/yiax/libyateiax.a yiaxchan.yate: LOCALFLAGS = -I@top_srcdir@/libs/yiax yiaxchan.yate: LOCALLIBS = -L../libs/yiax -lyateiax -yjinglechan.yate jingle/jinglefeatures.yate: ../libyatejingle.so -yjinglechan.yate jingle/jinglefeatures.yate: LOCALFLAGS = -I@top_srcdir@/libs/yxml -I@top_srcdir@/libs/yjingle -yjinglechan.yate jingle/jinglefeatures.yate: LOCALLIBS = -lyatejingle +yjinglechan.yate jabber/jabberserver.yate jabber/jbfeatures.yate client/jabberclient.yate: ../libyatejabber.so +yjinglechan.yate jabber/jabberserver.yate jabber/jbfeatures.yate client/jabberclient.yate: LOCALFLAGS = -I@top_srcdir@/libs/yxml -I@top_srcdir@/libs/yjabber +yjinglechan.yate jabber/jabberserver.yate jabber/jbfeatures.yate client/jabberclient.yate: LOCALLIBS = -lyatejabber server/dbpbx.yate server/pbxassist.yate: ../libs/ypbx/libyatepbx.a server/dbpbx.yate server/pbxassist.yate: LOCALFLAGS = -I@top_srcdir@/libs/ypbx @@ -320,8 +323,8 @@ qt4/updater.yate: LOCALLIBS = @QT4_LIB_NET@ ../libs/yxml/libyatexml.a: $(MAKE) -C ../libs/yxml -../libyatejingle.so ../libs/yjingle/libyatejingle.a: - $(MAKE) -C ../libs/yjingle +../libyatejabber.so ../libs/yjabber/libyatejabber.a: + $(MAKE) -C ../libs/yjabber ../libs/ypbx/libyatepbx.a: $(MAKE) -C ../libs/ypbx diff --git a/modules/client/jabberclient.cpp b/modules/client/jabberclient.cpp new file mode 100644 index 00000000..cc7352cc --- /dev/null +++ b/modules/client/jabberclient.cpp @@ -0,0 +1,1417 @@ +/** + * jabberclient.cpp + * This file is part of the YATE Project http://YATE.null.ro + * + * Jabber Client module + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include +#include +#include + +using namespace TelEngine; + +namespace { // anonymous + +class YStreamReceive; // Stream receive thread +class YStreamSetReceive; // A list of stream receive threads +class YStreamProcess; // Stream process (getEvent()) thread +class YStreamSetProcess; // A list of stream process threads +class YJBConnectThread; // Stream connect thread +class YJBEntityCapsList; // Entity capbilities +class YJBEngine; // Jabber engine +class StreamData; // Data attached to a stream +class JBMessageHandler; // Module message handlers +class JBModule; // The module + + +/* + * Stream receive thread + */ +class YStreamReceive : public JBStreamSetReceive, public Thread +{ +public: + inline YStreamReceive(JBStreamSetList* owner, Thread::Priority prio = Thread::Normal) + : JBStreamSetReceive(owner), Thread("JBStreamReceive",prio) + {} + virtual bool start() + { return Thread::startup(); } + virtual void stop() + { Thread::cancel(); } +protected: + virtual void run() + { JBStreamSetReceive::run(); } +}; + +/* + * A list of stream receive threads + */ +class YStreamSetReceive : public JBStreamSetList +{ +public: + inline YStreamSetReceive(JBEngine* engine, unsigned int max, const char* name) + : JBStreamSetList(engine,max,0,name) + {} +protected: + virtual JBStreamSet* build() + { return new YStreamReceive(this); } +}; + +/* + * Stream process (getEvent()) thread + */ +class YStreamProcess : public JBStreamSetProcessor, public Thread +{ +public: + inline YStreamProcess(JBStreamSetList* owner, Thread::Priority prio = Thread::Normal) + : JBStreamSetProcessor(owner), Thread("JBStreamProcess",prio) + {} + virtual bool start() + { return Thread::startup(); } + virtual void stop() + { Thread::cancel(); } +protected: + virtual void run() + { JBStreamSetProcessor::run(); } +}; + +/* + * A list of stream process threads + */ +class YStreamSetProcess : public JBStreamSetList +{ +public: + inline YStreamSetProcess(JBEngine* engine, unsigned int max, const char* name) + : JBStreamSetList(engine,max,0,name) + {} +protected: + virtual JBStreamSet* build() + { return new YStreamProcess(this); } +}; + +/* + * Stream connect thread + */ +class YJBConnectThread : public JBConnect, public Thread +{ +public: + inline YJBConnectThread(const JBStream& stream) + : JBConnect(stream), Thread("YJBConnectThread") + {} + virtual void stopConnect() + { cancel(false); } + virtual void run() + { JBConnect::connect(); } +}; + +/* + * Entity capability + */ +class YJBEntityCapsList : public JBEntityCapsList +{ +public: + // Load the entity caps file + void load(); +protected: + inline void getEntityCapsFile(String& file) { + file = Engine::configPath(); + if (!file.endsWith(Engine::pathSeparator())) + file << Engine::pathSeparator(); + file << "jabberentitycaps.xml"; + } + // Notify changes and save the entity caps file + virtual void capsAdded(JBEntityCaps* caps); +}; + +/* + * Data attached to a stream + */ +class StreamData : public NamedList +{ +public: + inline StreamData(JBClientStream& m_owner, bool requestRoster) + : NamedList(m_owner.local().bare()), + m_requestRoster(requestRoster), m_presence(0) + {} + ~StreamData() + { TelEngine::destruct(m_presence); } + // Retrieve a contact + inline NamedList* contact(const String& name) { + ObjList* o = find(name); + return o ? static_cast(o->get()) : 0; + } + // Append a contact (if not found) + // This method is thread safe + inline NamedList* addContact(const String& name) { + NamedList* c = contact(name); + if (!c) { + c = new NamedList(name); + m_contacts.append(c); + } + return c; + } + // Remove a a contact (if not found) + // This method is thread safe + inline void removeContact(const String& name) { + ObjList* o = find(name); + if (o) + o->remove(); + } + // Append or update a resource (user own resource if contact is empty) + inline void setResource(const String& name, const String& capsid, + const String& cn = String::empty()) { + if (!name) + return; + NamedList* c = cn ? contact(cn) : this; + if (c) + c->setParam(name,capsid); + } + // Remove a resource (user own resource if contact is empty) + // Remove all of them if resource name is empty + inline void removeResource(const String& name, const String& cn = String::empty()) { + NamedList* c = cn ? contact(cn) : this; + if (!c) + return; + if (name) + c->clearParam(name); + else + c->clearParams(); + } + // Set presence params + void setPresence(const char* prio, const char* show, const char* status); + // Retrieve a contact + ObjList* find(const String& name); + // Build an online presence element + static XmlElement* buildPresence(StreamData* d = 0, const char* to = 0); + + // Request roster when connected + bool m_requestRoster; + // Presence data + NamedList* m_presence; + // Contacts and their resources + ObjList m_contacts; +}; + +/* + * Jabber engine + */ +class YJBEngine : public JBClientEngine +{ +public: + YJBEngine(); + ~YJBEngine(); + // Find a c2s stream by account + inline JBClientStream* find(const String& name) { + JBStream* s = findStream(name); + return s ? s->clientStream() : 0; + } + // Retrieve stream data from an event's stream + inline StreamData* streamData(JBEvent* ev) { + JBClientStream* s = ev ? ev->clientStream() : 0; + return s ? static_cast(s->userData()) : 0; + } + // (Re)initialize the engine + void initialize(const NamedList* params, bool first = false); + // Process events + virtual void processEvent(JBEvent* ev); + // Start stream TLS + virtual void encryptStream(JBStream* stream); + // Connect an outgoing stream + virtual void connectStream(JBStream* stream); + // Process 'user.roster' messages + bool handleUserRoster(Message& msg, const String& line); + // Process 'user.update' messages + bool handleUserUpdate(Message& msg, const String& line); + // Process 'jabber.iq' messages + bool handleJabberIq(Message& msg, const String& line); + // Process 'jabber.account' messages + bool handleJabberAccount(Message& msg, const String& line); + // Process 'resource.subscribe' messages + bool handleResSubscribe(Message& msg, const String& line); + // Process 'resource.notify' messages + bool handleResNotify(Message& msg, const String& line); + // Process 'msg.execute' messages + bool handleMsgExecute(Message& msg, const String& line); + // Process 'user.login' messages + bool handleUserLogin(Message& msg, const String& line); + // Handle 'presence' stanzas + // The given event is always valid and carry a valid stream and xml element + void processPresenceStanza(JBEvent* ev); + // Handle 'iq' stanzas + // The given event is always valid and carry a valid stream and xml element + void processIqStanza(JBEvent* ev); + // Process stream Running, Destroy, Terminated events + // The given event is always valid and carry a valid stream + void processStreamEvent(JBEvent* ev, bool ok); + // Process stream register result events + // The given event has a valid element and stream + void processRegisterEvent(JBEvent* ev, bool ok); + // Process received roster elements + void processRoster(JBEvent* ev, XmlElement* service, int tag, int iqType); + // Fill module status + void statusParams(String& str); + unsigned int statusDetail(String& str); + void statusDetail(String& str, const String& name); + // Complete stream detail + void streamDetail(String& str, JBStream* stream); + // Complete stream name starting with partWord + void completeStreamName(String& str, const String& partWord); +private: + String m_progName; // Program name to be advertised on request + String m_progVersion; // Program version to be advertised on request + XMPPFeatureList m_features; // Client features +}; + +/* + * Module message handlers + */ +class JBMessageHandler : public MessageHandler +{ +public: + // Message handlers + // Non-negative enum values will be used as handler priority + enum { + ResSubscribe = -1, // YJBEngine::handleResSubscribe() + ResNotify = -2, // YJBEngine::handleResNotify() + UserRoster = -3, // YJBEngine::handleUserRoster() + UserUpdate = -4, // YJBEngine::handleUserUpdate() + UserLogin = -5, // YJBEngine::handleUserLogin() + JabberAccount = -6, // YJBEngine::handleJabberAccount() + JabberIq = 150, // YJBEngine::handleJabberIq() + }; + JBMessageHandler(int handler); +protected: + virtual bool received(Message& msg); +private: + int m_handler; +}; + +/* + * The module + */ +class JBModule : public Module +{ + friend class TcpListener; // Add/remove to/from list +public: + JBModule(); + virtual ~JBModule(); + // Inherited methods + virtual void initialize(); + // Check if a message was sent by us + inline bool isModule(const Message& msg) const { + String* module = msg.getParam("module"); + return module && *module == name(); + } + // Build a Message. Complete module, protocol and line parameters + inline Message* message(const char* msg, JBStream* stream = 0) { + Message* m = new Message(msg); + m->addParam("module",name()); + m->addParam("protocol","jabber"); + if (stream) + m->addParam("account",stream->name()); + return m; + } + // Retrieve the line (account) from a message + inline String* getLine(Message& msg) { + String* tmp = msg.getParam("line"); + return tmp ? tmp : msg.getParam("account"); + } + // Check if this module handles a given protocol + inline bool canHandleProtocol(const String& proto) + { return proto == "jabber"; } +protected: + // Inherited methods + virtual bool received(Message& msg, int id); + virtual void statusParams(String& str); + virtual void statusDetail(String& str); + virtual bool commandComplete(Message& msg, const String& partLine, + const String& partWord); + virtual bool commandExecute(String& retVal, const String& line); +private: + bool m_init; + ObjList m_handlers; // Message handlers list +}; + + +/* + * Local data + */ +INIT_PLUGIN(JBModule); // The module +YJBEntityCapsList s_entityCaps; +static YJBEngine* s_jabber = 0; +static String s_priority = "20"; // Default priority for generated presence +static String s_rosterQueryId = "roster-query"; + +// Commands help +static const char* s_cmdStatus = " status jabberclient stream_name"; +static const char* s_cmdDropStream = " jabberclient drop stream_name|*|all"; +static const char* s_cmdDebug = " jabberclient debug stream_name [debug_level|on|off]"; + +// Commands handled by this module (format module_name command [params]) +static const String s_cmds[] = { + "drop", + "debug", + "" +}; + +// Message handlers installed by the module +static const TokenDict s_msgHandler[] = { + {"resource.subscribe", JBMessageHandler::ResSubscribe}, + {"resource.notify", JBMessageHandler::ResNotify}, + {"user.roster", JBMessageHandler::UserRoster}, + {"user.update", JBMessageHandler::UserUpdate}, + {"user.login", JBMessageHandler::UserLogin}, + {"jabber.account", JBMessageHandler::JabberAccount}, + {"jabber.iq", JBMessageHandler::JabberIq}, + {0,0} +}; + +// Add xml data parameter to a message +static inline void addValidParam(NamedList& list, const char* param, const char* value) +{ + if (!TelEngine::null(value)) + list.addParam(param,value); +} + +// Get a space separated word from a buffer +// Return false if empty +static inline bool getWord(String& buf, String& word) +{ + XDebug(&__plugin,DebugAll,"getWord(%s)",buf.c_str()); + int pos = buf.find(" "); + if (pos >= 0) { + word = buf.substr(0,pos); + buf = buf.substr(pos + 1); + } + else { + word = buf; + buf = ""; + } + if (!word) + return false; + return true; +} + +// Request the roster on a given stream +// Set stream RosterRequested flag +static bool requestRoster(JBStream* stream) +{ + if (!stream || stream->flag(JBStream::RosterRequested)) + return false; + XmlElement* xml = XMPPUtils::createIq(XMPPUtils::IqGet,0,0,s_rosterQueryId); + xml->addChild(XMPPUtils::createElement(XmlTag::Query,XMPPNamespace::Roster)); + if (stream->sendStanza(xml)) { + stream->setRosterRequested(true); + return true; + } + return false; +} + +// Request the roster on a given stream +// Set stream RosterRequested flag +static bool sendPresence(JBStream* stream, bool ok, XmlElement* xml) +{ + if (!(stream && xml)) + return false; + if (stream->sendStanza(xml)) { + stream->setAvailableResource(ok); + return true; + } + return false; +} + + + +/* + * YJBEntityCapsList + */ +// Load the entity caps file +void YJBEntityCapsList::load() +{ + if (!m_enable) + return; + String file; + getEntityCapsFile(file); + loadXmlDoc(file,s_jabber); +} + +// Notify changes and save the entity caps file +void YJBEntityCapsList::capsAdded(JBEntityCaps* caps) +{ + if (!caps) + return; + // Save the file + String file; + getEntityCapsFile(file); + saveXmlDoc(file,s_jabber); +} + + +/* + * YJBEngine + */ +YJBEngine::YJBEngine() +{ + m_receive = new YStreamSetReceive(this,0,"recv"); + m_process = new YStreamSetProcess(this,0,"process"); + // Features + m_features.add(XMPPNamespace::DiscoInfo); + m_features.add(XMPPNamespace::DiscoItems); + m_features.add(XMPPNamespace::Jingle); + m_features.add(XMPPNamespace::JingleError); + 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::Roster); + m_features.add(XMPPNamespace::IqPrivate); + m_features.add(XMPPNamespace::VCard); + m_features.add(XMPPNamespace::IqVersion); + m_features.add(XMPPNamespace::Session); + m_features.add(XMPPNamespace::Register); + m_features.add(XMPPNamespace::EntityCaps); + m_features.m_identities.append(new JIDIdentity("client","im")); + m_features.updateEntityCaps(); +} + +YJBEngine::~YJBEngine() +{ +} + +// (Re)initialize engine +void YJBEngine::initialize(const NamedList* params, bool first) +{ + NamedList dummy(""); + if (!params) + params = &dummy; + + lock(); + // Program name and version to be advertised on request + if (!m_progName) { + m_progName = "Yate"; + m_progVersion.clear(); + m_progVersion << Engine::runParams().getValue("version") << "" << + Engine::runParams().getValue("release"); + // TODO: set program name and version for server identities + } + unlock(); + JBEngine::initialize(*params); +} + +// Process events +void YJBEngine::processEvent(JBEvent* ev) +{ + if (!(ev && ev->stream())) { + if (ev && !ev->stream()) + Debug(this,DebugStub,"Event (%p,'%s') without stream",ev,ev->name()); + TelEngine::destruct(ev); + return; + } + Debug(this,DebugInfo,"Processing event (%p,%s)",ev,ev->name()); + switch (ev->type()) { + case JBEvent::Message: + if (ev->element()) { + Message* m = __plugin.message("msg.execute",ev->stream()); + m->addParam("type",ev->stanzaType()); + m->addParam("caller",ev->from().bare()); + addValidParam(*m,"caller_instance",ev->from().resource()); + XmlElement* xml = ev->releaseXml(); + addValidParam(*m,"subject",XMPPUtils::subject(*xml)); + addValidParam(*m,"body",XMPPUtils::body(*xml)); + m->addParam(new NamedPointer("xml",xml)); + Engine::enqueue(m); + } + break; + case JBEvent::Presence: + if (ev->element()) + processPresenceStanza(ev); + break; + case JBEvent::Iq: + if (ev->element()) + processIqStanza(ev); + break; + case JBEvent::Running: + case JBEvent::Destroy: + case JBEvent::Terminated: + processStreamEvent(ev,ev->type() == JBEvent::Running); + break; + case JBEvent::RegisterOk: + case JBEvent::RegisterFailed: + if (ev->element()) + processRegisterEvent(ev,ev->type() == JBEvent::RegisterOk); + break; + default: + returnEvent(ev,XMPPError::ServiceUnavailable); + return; + } + TelEngine::destruct(ev); +} + +// Start stream TLS +void YJBEngine::encryptStream(JBStream* stream) +{ + if (!stream) + return; + DDebug(this,DebugAll,"encryptStream(%p,'%s')",stream,stream->toString().c_str()); + Message msg("socket.ssl"); + msg.userData(stream); + msg.addParam("server",String::boolText(stream->incoming())); + if (stream->incoming()) + msg.addParam("domain",stream->local().domain()); + if (!Engine::dispatch(msg)) + stream->terminate(0,stream->incoming(),0,XMPPError::Internal,"SSL start failure"); +} + +// Connect an outgoing stream +void YJBEngine::connectStream(JBStream* stream) +{ + if (Engine::exiting() || exiting()) + return; + if (stream && stream->outgoing()) + (new YJBConnectThread(*stream))->startup(); +} + +// Process 'user.roster' messages +bool YJBEngine::handleUserRoster(Message& msg, const String& line) +{ + Debug(this,DebugStub,"YJBEngine::handleUserRoster() not implemented!"); + return false; +} + +// Process 'user.update' messages +bool YJBEngine::handleUserUpdate(Message& msg, const String& line) +{ + String* oper = msg.getParam("operation"); + if (TelEngine::null(oper)) + return false; + JBClientStream* s = find(line); + if (!s) + return false; + bool ok = false; + if (*oper == "update") { + Debug(this,DebugStub,"YJBEngine::handleUserUpdate(update) not implemented!"); + } + else if (*oper == "delete") { + Debug(this,DebugStub,"YJBEngine::handleUserUpdate(delete) not implemented!"); + } + else if (*oper == "query") + ok = requestRoster(s); + TelEngine::destruct(s); + return ok; +} + +// Process 'jabber.iq' messages +bool YJBEngine::handleJabberIq(Message& msg, const String& line) +{ + Debug(this,DebugStub,"YJBEngine::handleJabberIq() not implemented!"); + return false; +} + +// Process 'jabber.account' messages +bool YJBEngine::handleJabberAccount(Message& msg, const String& line) +{ + JBClientStream* s = find(line); + if (!s) + return false; + if (msg.getBoolValue("query")) { + // TODO fill stream data + // local bare jid, resource + // Get available audio for a contact + } + TelEngine::destruct(s); + return true; +} + +// Process 'resource.subscribe' messages +bool YJBEngine::handleResSubscribe(Message& msg, const String& line) +{ + Debug(this,DebugStub,"YJBEngine::handleResSubscribe() not implemented!"); + return false; +} + +// Process 'resource.notify' messages +bool YJBEngine::handleResNotify(Message& msg, const String& line) +{ + Debug(this,DebugStub,"YJBEngine::handleResNotify() not implemented!"); + return false; +} + +// Process 'msg.execute' messages +bool YJBEngine::handleMsgExecute(Message& msg, const String& line) +{ + Debug(this,DebugStub,"YJBEngine::handleMsgExecute() not implemented!"); + return false; +} + +// Process 'user.login' messages +bool YJBEngine::handleUserLogin(Message& msg, const String& line) +{ + String* proto = msg.getParam("protocol"); + if (!(proto || !__plugin.canHandleProtocol(*proto))) + return false; + + // Check operation + NamedString* oper = msg.getParam("operation"); + bool login = !oper || *oper == "login" || *oper == "create"; + if (!login && (!oper || (*oper != "logout" && *oper != "delete"))) + return false; + + Debug(&__plugin,DebugAll,"handleUserLogin(%s) account=%s", + String::boolText(login),line.c_str()); + + JBClientStream* stream = s_jabber->find(line); + bool ok = false; + if (login) { + if (!stream) { + stream = s_jabber->create(line,msg); + if (stream) { + // Build user data and set it + Lock lock(stream); + StreamData* d = new StreamData(*stream, + msg.getBoolValue("request_roster",true)); + if (msg.getBoolValue("send_presence",true)) + d->setPresence(msg.getValue("priority",s_priority), + msg.getValue("show"),msg.getValue("status")); + stream->userData(d); + } + } + else + msg.setParam("error","User already logged in"); + ok = (0 != stream) && stream->state() != JBStream::Destroy; + } + else if (stream) { + if (stream->state() == JBStream::Running) { + XmlElement* xml = XMPPUtils::createPresence(0,0,XMPPUtils::Unavailable); + stream->sendStanza(xml); + } + const char* reason = msg.getValue("reason"); + if (!reason) + reason = Engine::exiting() ? "" : "Logout"; + XMPPError::Type err = Engine::exiting() ? XMPPError::Shutdown : XMPPError::NoError; + stream->terminate(-1,true,0,err,reason); + ok = true; + } + TelEngine::destruct(stream); + return ok; +} + +// Handle 'presence' stanzas +// The given event is always valid and carry a valid stream and xml element +void YJBEngine::processPresenceStanza(JBEvent* ev) +{ + DDebug(this,DebugAll,"Processing presence type=%s from=%s", + ev->stanzaType().c_str(),ev->from().c_str()); + if (!ev->from()) + return; + XMPPUtils::Presence pres = XMPPUtils::presenceType(ev->stanzaType()); + bool online = pres == XMPPUtils::PresenceNone; + if (online || pres == XMPPUtils::Unavailable) { + String capsId; + if (online) { + if (!ev->from().resource()) + return; + s_entityCaps.processCaps(capsId,ev->element(),ev->stream(),ev->to(),ev->from()); + } + // Update contact list resources + Lock lock(ev->stream()); + StreamData* sdata = streamData(ev); + if (!sdata) + return; + const String& c = (ev->from().bare() != ev->to().bare()) ? String::empty() : + ev->from().bare(); + if (online) + sdata->setResource(ev->from().resource(),capsId,c); + else + sdata->removeResource(ev->from().resource(),c); + lock.drop(); + // Notify + Message* m = __plugin.message("resource.notify",ev->stream()); + m->addParam("operation",online ? "online" : "offline"); + m->addParam("contact",ev->from().bare()); + if (ev->from().resource()) + m->addParam("instance",ev->from().resource()); + if (online) { + m->addParam("uri",ev->from()); + m->addParam("priority",String(XMPPUtils::priority(*ev->element()))); + const String& ns = XMPPUtils::s_ns[ev->stream()->xmlns()]; + String s("show"); + XmlElement* tmp = ev->element()->findFirstChild(&s,&ns); + if (tmp) + addValidParam(*m,"show",tmp->getText()); + s = "status"; + tmp = ev->element()->findFirstChild(&s,&ns); + if (tmp) + addValidParam(*m,"status",tmp->getText()); + if (capsId) + s_entityCaps.addCaps(*m,capsId); + // TODO: add arbitrary children texts + } + Engine::enqueue(m); + return; + } + bool subReq = (pres == XMPPUtils::Subscribe); + if (subReq || pres == XMPPUtils::Unsubscribe) { + Message* m = __plugin.message("resource.subscribe",ev->stream()); + m->addParam("operation",ev->stanzaType()); + m->addParam("subscriber",ev->from().bare()); + Engine::enqueue(m); + return; + } + // Ignore XMPPUtils::Subscribed, XMPPUtils::Unsubscribed, XMPPUtils::Probe, + // XMPPUtils::PresenceError +} + +// Handle 'iq' stanzas +// The given event is always valid and carry a valid stream and xml element +void YJBEngine::processIqStanza(JBEvent* ev) +{ + XmlElement* service = ev->child(); + XMPPUtils::IqType type = XMPPUtils::iqType(ev->stanzaType()); + bool rsp = type == XMPPUtils::IqResult || type == XMPPUtils::IqError; + // Don't accept requests without child + if (!(rsp || service)) { + ev->sendStanzaError(XMPPError::ServiceUnavailable); + return; + } + int t = XmlTag::Count; + int n = XMPPNamespace::Count; + if (service) + XMPPUtils::getTag(*service,t,n); + switch (n) { + case XMPPNamespace::Roster: + processRoster(ev,service,t,type); + return; + } + // Check responses without child + if (rsp) { + if (ev->id() == s_rosterQueryId) { + processRoster(ev,service,t,type); + return; + } + } + // TODO: enqueue custom message (respond with error if not handled) + Debug(this,DebugStub,"processIqStanza() not implemented!"); +} + +// Process stream Running, Destroy, Terminated events +// The given event is always valid and carry a valid stream +void YJBEngine::processStreamEvent(JBEvent* ev, bool ok) +{ + if (ok) { + // Connected: + // request the roster, send presence + // TODO: request vcard, private data + bool reqRoster = true; + XmlElement* pres = 0; + ev->stream()->lock(); + StreamData* sdata = streamData(ev); + if (sdata) { + reqRoster = sdata->m_requestRoster; + if (sdata->m_presence) + pres = StreamData::buildPresence(sdata); + } + else + pres = StreamData::buildPresence(); + ev->stream()->unlock(); + if (reqRoster) + requestRoster(ev->stream()); + if (pres) + sendPresence(ev->stream(),true,pres); + } + + Message* m = __plugin.message("user.notify",ev->stream()); + m->addParam("username",ev->stream()->local().node()); + m->addParam("server",ev->stream()->local().domain()); + m->addParam("jid",ev->stream()->local()); + m->addParam("registered",String::boolText(ok)); + if (ok) + m->addParam("instance",ev->stream()->local().resource()); + else if (ev->text()) + m->addParam("reason",ev->text()); + bool restart = (ev->stream()->state() != JBStream::Destroy && + !ev->stream()->flag(JBStream::NoAutoRestart)); + m->addParam("autorestart",String::boolText(restart)); + Engine::enqueue(m); +} + +// Process stream register result events +// The given event has a valid element and stream +void YJBEngine::processRegisterEvent(JBEvent* ev, bool ok) +{ + Debug(this,DebugStub,"processRegisterEvent() not implemented!"); + + if (ok) { + return; + } + + // Check for instructions + if (ev->stanzaType() == "result") { + XmlElement* query = XMPPUtils::findFirstChild(*ev->element(),XmlTag::Query, + XMPPNamespace::IqRegister); + const char* url = 0; + const char* info = 0; + if (query) { + String x("x"); + XmlElement* tmp = query->findFirstChild(&x, + &XMPPUtils::s_ns[XMPPNamespace::XOob]); + if (tmp) { + x = "url"; + tmp = tmp->findFirstChild(&x); + if (tmp) + url = tmp->getText(); + x = "instructions"; + tmp = query->findFirstChild(&x); + if (tmp) + info = tmp->getText(); + } + } + if (url || info) { + DDebug(this,DebugAll,"Account '%s' got register info '%s' url='%s'", + ev->stream()->toString().c_str(),info,url); + } + } +} + +// Add a roster item to a list +static void addRosterItem(NamedList& list, XmlElement& x, const String& id, + int index, bool del = false) +{ + String pref("contact."); + pref << index; + list.addParam(pref,id); + if (del) + return; + pref << "."; + addValidParam(list,pref + "name",x.attribute("name")); + addValidParam(list,pref + "subscription",x.attribute("subscription")); + NamedString* groups = new NamedString("groups"); + list.addParam(groups); + // Groups and other children + const String* ns = &XMPPUtils::s_ns[XMPPNamespace::Roster]; + for (XmlElement* c = x.findFirstChild(0,ns); c; c = x.findNextChild(c,0,ns)) { + if (XMPPUtils::isUnprefTag(*c,XmlTag::Group)) + groups->append(c->getText(),","); + else + list.append(pref + c->unprefixedTag(),c->getText()); + } +} + +// Process received roster elements +void YJBEngine::processRoster(JBEvent* ev, XmlElement* service, int tag, int iqType) +{ + // Server roster push + if (iqType == XMPPUtils::IqSet) { + // Accept 'query' on streams that already requested the roster + if (!service || tag != XmlTag::Query || + !ev->stream()->flag(JBStream::RosterRequested)) { + ev->sendStanzaError(XMPPError::ServiceUnavailable); + return; + } + XmlElement* x = XMPPUtils::findFirstChild(*service,XmlTag::Item,XMPPNamespace::Roster); + if (!x) + return; + String* jid = x->getAttribute("jid"); + if (TelEngine::null(jid)) + return; + Message* m = __plugin.message("user.roster",ev->stream()); + String* sub = x->getAttribute("subscription"); + bool upd = !sub || *sub != "remove"; + ev->stream()->lock(); + StreamData* sdata = streamData(ev); + if (sdata) { + if (*jid != ev->stream()->local().bare()) { + if (upd) + sdata->addContact(*jid); + else + sdata->removeContact(*jid); + } + } + ev->stream()->unlock(); + m->addParam("operation",upd ? "update" : "delete"); + m->addParam("contact.count","1"); + addRosterItem(*m,*x,*jid,1,!upd); + Engine::enqueue(m); + return; + } + // Ignore responses for now (except for roster query) + // The client shouldn't expect the result (the server will push changes) + if (iqType == XMPPUtils::IqResult) { + // Handle 'query' responses + if (!service || tag != XmlTag::Query || ev->id() != s_rosterQueryId) + return; + Message* m = __plugin.message("user.roster",ev->stream()); + m->addParam("operation","update"); + NamedString* count = new NamedString("contact.count"); + m->addParam(count); + int n = 0; + XmlElement* x = 0; + while (0 != (x = XMPPUtils::findNextChild(*service,x,XmlTag::Item,XMPPNamespace::Roster))) { + String* jid = x->getAttribute("jid"); + if (!TelEngine::null(jid)) + addRosterItem(*m,*x,*jid,++n); + } + *count = String(n); + Engine::enqueue(m); + return; + } + if (iqType == XMPPUtils::IqError) + return; + ev->sendStanzaError(XMPPError::ServiceUnavailable); +} + +// Fill module status params +void YJBEngine::statusParams(String& str) +{ + lock(); + unsigned int c2s = m_receive ? m_receive->streamCount() : 0; + unlock(); + str << "count=" << c2s; +} + +// Fill module status detail +unsigned int YJBEngine::statusDetail(String& str) +{ + XDebug(this,DebugAll,"statusDetail('%s','%s')", + lookup(t,JBStream::s_typeName),TelEngine::c_safe(remote)); + lock(); + RefPointer list = m_receive; + unlock(); + str << "format=Direction|Status|Local|Remote"; + if (!list) + return 0; + unsigned int n = 0; + list->lock(); + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + JBStream* stream = static_cast(s->get()); + Lock lock(stream); + n++; + streamDetail(str,stream); + } + } + list->unlock(); + list = 0; + return n; +} + +// Complete stream details +void YJBEngine::statusDetail(String& str, const String& name) +{ + XDebug(this,DebugAll,"statusDetail(%s)",name.c_str()); + JBStream* stream = findStream(name); + if (!stream) + return; + Lock lock(stream); + str.append("name=",";"); + str << stream->toString(); + str << ",direction=" << (stream->incoming() ? "incoming" : "outgoing"); + str << ",state=" << stream->stateName(); + str << ",local=" << stream->local(); + str << ",remote=" << stream->remote(); + String buf; + XMPPUtils::buildFlags(buf,stream->flags(),JBStream::s_flagName); + str << ",options=" << buf; +} + +// Complete stream detail +void YJBEngine::streamDetail(String& str, JBStream* stream) +{ + str << ";" << stream->toString() << "="; + str << (stream->incoming() ? "incoming" : "outgoing"); + str << "|" << stream->stateName(); + str << "|" << stream->local(); + str << "|" << stream->remote(); +} + +// Complete stream name starting with partWord +void YJBEngine::completeStreamName(String& str, const String& partWord) +{ + lock(); + RefPointer list = m_receive; + unlock(); + if (!list) + return; + list->lock(); + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + JBStream* stream = static_cast(s->get()); + Lock lock(stream); + if (!partWord || stream->toString().startsWith(partWord)) + Module::itemComplete(str,stream->toString(),partWord); + } + } + list->unlock(); + list = 0; +} + + +/* + * StreamData + */ +// Set presence params +void StreamData::setPresence(const char* prio, const char* show, const char* status) +{ + if (!m_presence) + m_presence = new NamedList(""); + if (!TelEngine::null(prio)) + m_presence->setParam("priority",prio); + else + m_presence->clearParam("priority"); + if (!TelEngine::null(show)) + m_presence->setParam("show",show); + else + m_presence->clearParam("show"); + m_presence->setParam("status",status); +} + +// Retrieve a contact +ObjList* StreamData::find(const String& name) +{ + for (ObjList* o = m_contacts.skipNull(); o; o = o->skipNext()) { + NamedList* c = static_cast(o->get()); + if (*c == name) + return o; + } + return 0; +} + +// Build an online presence element +XmlElement* StreamData::buildPresence(StreamData* d, const char* to) +{ + XmlElement* xml = XMPPUtils::createPresence(0,to); + if (d) { + if (!d->m_presence) { + TelEngine::destruct(xml); + return 0; + } + unsigned int n = d->m_presence->count(); + for (unsigned int i = 0; i < n; i++) { + NamedString* p = d->m_presence->getParam(i); + if (p && p->name()) + xml->addChild(XMPPUtils::createElement(p->name(),*p)); + } + // TODO: Build data or module default caps + } + else { + if (s_priority) + XMPPUtils::setPriority(*xml,s_priority); + // TODO: Build module default caps + } + xml->addChild(XMPPUtils::createEntityCapsGTalkV1()); + return xml; +} + + +/* + * JBMessageHandler + */ +JBMessageHandler::JBMessageHandler(int handler) + : MessageHandler(lookup(handler,s_msgHandler),handler < 0 ? 100 : handler), + m_handler(handler) +{ +} + +bool JBMessageHandler::received(Message& msg) +{ + XDebug(&__plugin,DebugAll,"JBMessageHandler(%s)",msg.c_str()); + if (__plugin.isModule(msg)) + return false; + String* line = __plugin.getLine(msg); + if (TelEngine::null(line)) + return false; + switch (m_handler) { + case JabberIq: + return s_jabber->handleJabberIq(msg,*line); + case ResNotify: + return s_jabber->handleResNotify(msg,*line); + case ResSubscribe: + return s_jabber->handleResSubscribe(msg,*line); + case UserRoster: + return s_jabber->handleUserRoster(msg,*line); + case UserLogin: + return s_jabber->handleUserLogin(msg,*line); + case UserUpdate: + return s_jabber->handleUserUpdate(msg,*line); + case JabberAccount: + return s_jabber->handleJabberAccount(msg,*line); + default: + Debug(&__plugin,DebugStub,"JBMessageHandler(%s) not handled!",msg.c_str()); + } + return false; +} + + +/* + * JBModule + */ +// Early load, late unload: we own the jabber engine +JBModule::JBModule() + : Module("jabberclient","misc",true), + m_init(false) +{ + Output("Loaded module Jabber Client"); +} + +JBModule::~JBModule() +{ + Output("Unloading module Jabber Client"); + TelEngine::destruct(s_jabber); +} + +void JBModule::initialize() +{ + Output("Initializing module Jabber Client"); + Configuration cfg(Engine::configFile("jabberclient")); + + if (!m_init) { + m_init = true; + setup(); + installRelay(Halt); + installRelay(Help); + installRelay(ImExecute); + s_jabber = new YJBEngine; + s_jabber->debugChain(this); + // Install handlers + for (const TokenDict* d = s_msgHandler; d->token; d++) { + JBMessageHandler* h = new JBMessageHandler(d->value); + Engine::install(h); + m_handlers.append(h); + } + // Load entity caps file + s_entityCaps.m_enable = cfg.getBoolValue("general","entitycaps",true); + if (s_entityCaps.m_enable) + s_entityCaps.load(); + else + Debug(this,DebugAll,"Entity capability is disabled"); + } + // Init the engine + s_jabber->initialize(cfg.getSection("general"),!m_init); +} + +// Message handler +bool JBModule::received(Message& msg, int id) +{ + if (id == ImExecute) { + if (isModule(msg)) + return false; + String* line = getLine(msg); + return !TelEngine::null(line) && s_jabber->handleMsgExecute(msg,*line); + } + if (id == Status) { + String target = msg.getValue("module"); + // Target is the module + if (!target || target == name()) + return Module::received(msg,id); + // Check additional commands + if (!target.startSkip(name(),false)) + return false; + target.trimBlanks(); + if (!target) + return Module::received(msg,id); + // Handle: status jabberclient stream_name + statusModule(msg.retValue()); + s_jabber->statusDetail(msg.retValue(),target); + msg.retValue() << "\r\n"; + return true; + } + if (id == Help) { + String line = msg.getValue("line"); + if (line.null()) { + msg.retValue() << s_cmdStatus << "\r\n"; + msg.retValue() << s_cmdDropStream << "\r\n"; + msg.retValue() << s_cmdDebug << "\r\n"; + return false; + } + if (line != name()) + return false; + msg.retValue() << s_cmdStatus << "\r\n"; + msg.retValue() << "Show stream status\r\n"; + msg.retValue() << s_cmdDropStream << "\r\n"; + msg.retValue() << "Terminate a stream or all of them\r\n"; + msg.retValue() << s_cmdDebug << "\r\n"; + msg.retValue() << "Show or set the debug level for a stream.\r\n"; + return true; + } + if (id == Halt) { + s_jabber->setExiting(); + // Uninstall message handlers + for (ObjList* o = m_handlers.skipNull(); o; o = o->skipNext()) { + JBMessageHandler* h = static_cast(o->get()); + Engine::uninstall(h); + } + s_jabber->cleanup(); + DDebug(this,DebugAll,"Halted"); + return Module::received(msg,id); + } + if (id == Timer) + s_entityCaps.expire(msg.msgTime().msec()); + return Module::received(msg,id); +} + +// Fill module status params +void JBModule::statusParams(String& str) +{ + s_jabber->statusParams(str); +} + +// Fill module status detail +void JBModule::statusDetail(String& str) +{ + s_jabber->statusDetail(str); +} + +// Handle command complete requests +bool JBModule::commandComplete(Message& msg, const String& partLine, + const String& partWord) +{ + if (partLine.null() && partWord.null()) + return false; + XDebug(this,DebugAll,"commandComplete() partLine='%s' partWord=%s", + partLine.c_str(),partWord.c_str()); + + // No line or 'help': complete module name + if (partLine.null() || partLine == "help") + return Module::itemComplete(msg.retValue(),name(),partWord); + // Line is module name: complete module commands + if (partLine == name()) { + for (const String* list = s_cmds; list->length(); list++) + Module::itemComplete(msg.retValue(),*list,partWord); + return true; + } + + String line = partLine; + String word; + getWord(line,word); + + if (word == name()) { + // Line is module name: complete module commands and parameters + getWord(line,word); + // Check for a known command + for (const String* list = s_cmds; list->length(); list++) { + if (*list != word) + continue; + if (*list == "drop") { + // Handle: jabberclient drop stream_name|*|all + if (line) + return true; + Module::itemComplete(msg.retValue(),"*",partWord); + Module::itemComplete(msg.retValue(),"all",partWord); + s_jabber->completeStreamName(msg.retValue(),partWord); + } + else if (*list == "debug") { + // Handle: jabberclient debug stream_name [debug_level] + if (line) + return true; + s_jabber->completeStreamName(msg.retValue(),partWord); + } + return true; + } + // Complete module commands + for (const String* list = s_cmds; list->length(); list++) + Module::itemComplete(msg.retValue(),*list,partWord); + return true; + } + if (word == "status") { + // Handle: status jabberclient stream_name + getWord(line,word); + if (word != name()) + return Module::commandComplete(msg,partLine,partWord); + getWord(line,word); + if (word) { + if (line) + return false; + s_jabber->completeStreamName(msg.retValue(),partWord); + } + else + s_jabber->completeStreamName(msg.retValue(),partWord); + return true; + } + return Module::commandComplete(msg,partLine,partWord); +} + +// Handle command request +bool JBModule::commandExecute(String& retVal, const String& line) +{ + String l = line; + String word; + getWord(l,word); + if (word != name()) + return false; + getWord(l,word); + DDebug(this,DebugAll,"Executing command '%s' params '%s'",word.c_str(),l.c_str()); + if (word == "drop") { + Debug(this,DebugAll,"Executing '%s' command line=%s",word.c_str(),line.c_str()); + if (l == "all" || l == "*") + retVal << "Dropped " << s_jabber->dropAll() << " stream(s)"; + else { + // Handle: jabberclient drop stream_name + JBStream* stream = s_jabber->findStream(l); + if (stream) { + stream->terminate(-1,true,0,XMPPError::NoError); + TelEngine::destruct(stream); + retVal << "Dropped stream '" << l << "'"; + } + else + retVal << "Stream '" << l << "' not found"; + } + } + else if (word == "debug") { + Debug(this,DebugAll,"Executing '%s' command line=%s",word.c_str(),line.c_str()); + getWord(l,word); + JBStream* stream = s_jabber->findStream(word); + if (stream) { + retVal << "Stream '" << word << "' debug"; + if (l) { + int level = l.toInteger(-1); + if (level >= 0) { + stream->debugLevel(level); + retVal << " at level " << stream->debugLevel(); + } + else if (l.isBoolean()) { + stream->debugEnabled(l.toBoolean()); + retVal << " is " << (stream->debugEnabled() ? "on" : "off"); + } + } + else + retVal << " at level " << stream->debugLevel(); + TelEngine::destruct(stream); + } + else + retVal << "Stream '" << word << "' not found"; + } + else + return false; + retVal << "\r\n"; + return true; +} + +}; // anonymous namespace + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/jabber/jabberserver.cpp b/modules/jabber/jabberserver.cpp new file mode 100644 index 00000000..bd2883a5 --- /dev/null +++ b/modules/jabber/jabberserver.cpp @@ -0,0 +1,3107 @@ +/** + * jabberserver.cpp + * This file is part of the YATE Project http://YATE.null.ro + * + * Jabber Server module + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +// TODO: +// - Fix stream termination on shutdown +// - Notify 'offline' on closing server streams +// - Remove offline messages from database when succesfully sent (not when enqueued in the stream) + + +#include +#include +#include + +using namespace TelEngine; + +namespace { // anonymous + +class YStreamReceive; // Stream receive thread +class YStreamSetReceive; // A list of stream receive threads +class YStreamProcess; // Stream process (getEvent()) thread +class YStreamSetProcess; // A list of stream process threads +class YJBConnectThread; // Stream connect thread +class YJBEntityCapsList; // Entity capbilities +class YJBEngine; // Jabber engine +class JBPendingJob; // A pending stanza waiting to be routed/processed +class JBPendingWorker; // A thread processing pending jobs containing stanzas with + // the same from/to hash +class UserAuthMessage; // 'user.auth' message enqueued when a stream requires user password +class JBMessageHandler; // Module message handlers +class TcpListener; // Incoming connection listener +class JBModule; // The module + + +/* + * Stream receive thread + */ +class YStreamReceive : public JBStreamSetReceive, public Thread +{ +public: + inline YStreamReceive(JBStreamSetList* owner, Thread::Priority prio = Thread::Normal) + : JBStreamSetReceive(owner), Thread("JBStreamReceive",prio) + {} + virtual bool start() + { return Thread::startup(); } + virtual void stop() + { Thread::cancel(); } +protected: + virtual void run() + { JBStreamSetReceive::run(); } +}; + +/* + * A list of stream receive threads + */ +class YStreamSetReceive : public JBStreamSetList +{ +public: + inline YStreamSetReceive(JBEngine* engine, unsigned int max, const char* name) + : JBStreamSetList(engine,max,0,name) + {} +protected: + virtual JBStreamSet* build() + { return new YStreamReceive(this); } +}; + +/* + * Stream process (getEvent()) thread + */ +class YStreamProcess : public JBStreamSetProcessor, public Thread +{ +public: + inline YStreamProcess(JBStreamSetList* owner, Thread::Priority prio = Thread::Normal) + : JBStreamSetProcessor(owner), Thread("JBStreamProcess",prio) + {} + virtual bool start() + { return Thread::startup(); } + virtual void stop() + { Thread::cancel(); } +protected: + virtual void run() + { JBStreamSetProcessor::run(); } +}; + +/* + * A list of stream process threads + */ +class YStreamSetProcess : public JBStreamSetList +{ +public: + inline YStreamSetProcess(JBEngine* engine, unsigned int max, const char* name) + : JBStreamSetList(engine,max,0,name) + {} +protected: + virtual JBStreamSet* build() + { return new YStreamProcess(this); } +}; + +/* + * Stream connect thread + */ +class YJBConnectThread : public JBConnect, public Thread +{ +public: + inline YJBConnectThread(const JBStream& stream) + : JBConnect(stream), Thread("YJBConnectThread") + {} + virtual void stopConnect() + { cancel(false); } + virtual void run() + { JBConnect::connect(); } +}; + +/* + * Entity capability + */ +class YJBEntityCapsList : public JBEntityCapsList +{ +public: + // Load the entity caps file + void load(); +protected: + inline void getEntityCapsFile(String& file) { + file = Engine::configPath(); + if (!file.endsWith(Engine::pathSeparator())) + file << Engine::pathSeparator(); + file << "jabberentitycaps.xml"; + } + // Notify changes and save the entity caps file + virtual void capsAdded(JBEntityCaps* caps); +}; + +/* + * Jabber engine + */ +class YJBEngine : public JBServerEngine +{ +public: + YJBEngine(); + ~YJBEngine(); + // (Re)initialize the engine + void initialize(const NamedList* params, bool first = false); + // Process events + virtual void processEvent(JBEvent* ev); + // Build an internal stream name from node name and stream index + virtual void buildStreamName(String& name); + // Start stream TLS + virtual void encryptStream(JBStream* stream); + // Connect an outgoing stream + virtual void connectStream(JBStream* stream); + // Build a dialback key + virtual void buildDialbackKey(const String& id, String& key); + // Check if a domain is serviced by this engine + virtual bool hasDomain(const String& domain); + // Get the first domain in the list + inline void firstDomain(String& domain) { + Lock lock(this); + ObjList* o = m_domains.skipNull(); + if (o) + domain = o->get()->toString(); + } + // Replace the list of domains service by this engine + void setDomains(const String& list); + // Check if a resource name is retricted + bool restrictedResource(const String& name); + // Process 'user.roster' notification messages + void handleUserRoster(Message& msg); + // Process 'user.update' messages + void handleUserUpdate(Message& msg); + // Process 'jabber.iq' messages + bool handleJabberIq(Message& msg); + // Process 'resource.subscribe' messages + bool handleResSubscribe(Message& msg); + // Process 'resource.notify' messages + bool handleResNotify(Message& msg); + // Process 'msg.execute' messages + bool handleMsgExecute(Message& msg); + // Process 'jabber.item' messages + bool handleJabberItem(Message& msg); + // Handle 'presence' stanzas + // s2s: Destination domain was already checked by the lower layer + // The given event is always valid and carry a valid stream and xml element + void processPresenceStanza(JBEvent* ev); + // Process a stream start element received by an incoming stream + // The given event is always valid and carry a valid stream + // Set local domain and stream features to advertise to remote party + void processStartIn(JBEvent* ev); + // Process Auth events from incoming streams + // The given event is always valid and carry a valid stream + void processAuthIn(JBEvent* ev); + // Process Bind events + // The given event is always valid and carry a valid stream + void processBind(JBEvent* ev); + // Process stream Running, Destroy, Terminated events + // The given event is always valid and carry a valid stream + void processStreamEvent(JBEvent* ev); + // Process stream DbResult events + // The given event is always valid and carry a valid stream + void processDbResult(JBEvent* ev); + // Process stream DbVerify events + // The given event is always valid and carry a valid stream + void processDbVerify(JBEvent* ev); + // Process all incoming jabber:iq:roster + // The given event is always valid and carry a valid element + // Return the response + XmlElement* processIqRoster(JBEvent* ev, JBStream::Type sType, XMPPUtils::IqType t); + // Process all incoming vcard-temp with target in our domain(s) + // The given event is always valid and carry a valid element + // Return the response + XmlElement* processIqVCard(JBEvent* ev, JBStream::Type sType, XMPPUtils::IqType t); + // Process all incoming jabber:iq:private + // The given event is always valid and carry a valid element + // Return the response + XmlElement* processIqPrivate(JBEvent* ev, JBStream::Type sType, XMPPUtils::IqType t); + // Process all incoming jabber:iq:register stanzas + // The given event is always valid and carry a valid element + XmlElement* processIqRegister(JBEvent* ev, JBStream::Type sType, XMPPUtils::IqType t, + const String& domain, int flags); + // Handle disco info requests addressed to the server + XmlElement* discoInfo(JBEvent* ev, JBStream::Type sType); + // Handle disco items requests addressed to the server + XmlElement* discoItems(JBEvent* ev); + // Send an XML element to list of client streams. + // The given pointers will be consumed and zeroed + // Return true if the element was succesfully sent on at least one stream + bool sendStanza(XmlElement*& xml, ObjList*& streams); + // Find a server stream used to send stanzas from local domain to remote + // Build a new one if not found + JBStream* getServerStream(const JabberID& from, const JabberID& to); + // Notify online/offline presence from client streams + void notifyPresence(JBClientStream& cs, bool online, XmlElement* xml, + const String& capsId); + // Notify directed online/offline presence + void notifyPresence(const JabberID& from, const JabberID& to, bool online, + XmlElement* xml, bool fromRemote, bool toRemote, const String& capsId); + // Build a jabber.feature message + Message* jabberFeature(XmlElement* xml, XMPPNamespace::Type t, JBStream::Type sType, + const char* from, const char* to = 0, const char* operation = 0); + // Build a xmpp.iq message + Message* xmppIq(JBEvent* ev, const char* service); + // Build an user.(un)register message + Message* userRegister(JBStream& stream, bool reg, const char* instance = 0); + // Fill module status + void statusParams(String& str); + unsigned int statusDetail(String& str, JBStream::Type t = JBStream::TypeCount, + JabberID* remote = 0); + void statusDetail(String& str, const String& name); + // Complete stream detail + void streamDetail(String& str, JBStream* stream); + // Complete remote party jid starting with partWord + void completeStreamRemote(String& str, const String& partWord, JBStream::Type t); + // Complete stream name starting with partWord + void completeStreamName(String& str, const String& partWord); + // Program name and version to be advertised on request + String m_progName; + String m_progVersion; +private: + // Notify an incoming s2s stream about a dialback verify response + void notifyDbVerifyResult(const JabberID& local, const JabberID& remote, + const String& id, bool ok); + + bool m_c2sTlsRequired; // TLS is required on c2s streams + ObjList m_domains; // Domains serviced by this engine + ObjList m_restrictedResources; // Resource names the users can't use + ObjList m_items; + XMPPFeatureList m_c2sFeatures; // Server features to advertise on c2s streams + XMPPFeatureList m_features; // Server features to advertise on non c2s streams + String m_dialbackSecret; // Server dialback secret used to build keys +}; + +/* + * A pending stanza waiting to be routed/processed + * It is used to serialize stanzas sent by an user to another one + */ +class JBPendingJob : public GenObject +{ +public: + JBPendingJob(JBEvent* ev); + inline ~JBPendingJob() + { TelEngine::destruct(m_event); } + // Retrieve the stream from jabber engine + JBStream* getStream(); + // Retrieve the stream from jabber engine. Send the given stanza + // Set regular=false to use JBStream::sendStreamXml() + // The pointer will be consumed and zeroed + void sendStanza(XmlElement*& xml, bool regular = true); + // Build and send an iq result stanza + inline void sendIqResultStanza(XmlElement* child = 0) { + XmlElement* xml = m_event->buildIqResult(false,child); + sendStanza(xml); + } + // Build and send an iq error stanza + inline void sendIqErrorStanza(XMPPError::Type error, + XMPPError::ErrorType type = XMPPError::TypeModify) { + XmlElement* xml = m_event->buildIqError(false,error,0,type); + sendStanza(xml); + } + // Build and send a message error stanza + inline void sendChatErrorStanza(XMPPError::Type error, + XMPPError::ErrorType type = XMPPError::TypeModify) { + XmlElement* xml = XMPPUtils::createMessage("error",0,0,m_event->id(),0); + xml->addChild(XMPPUtils::createError(type,error)); + sendStanza(xml); + } + + // Release memory + virtual void destruct() { + TelEngine::destruct(m_event); + GenObject::destruct(); + } + + JBEvent* m_event; + String m_stream; // The id of the stream receiving the stanza + JBStream::Type m_streamType; // The type of the stream + String m_local; // Stream local domain + int m_flags; // Stream flags + bool m_serverTarget; // The recipient is the server itself +}; + +/* + * A thread processing pending jobs + * s2s streams: The hash is built from the 'from and 'to' attributes + * Otherwise: The hash is built from the 'from' attribute + */ +class JBPendingWorker : public Thread, public Mutex +{ +public: + JBPendingWorker(unsigned int index, Thread::Priority prio); + virtual void cleanup(); + virtual void run(); + + // Initialize (start) the worker threads + static void initialize(unsigned int threads, Thread::Priority prio = Thread::Normal); + // Cancel worker threads. Wait for them to terminate + static void stop(); + // Add a job to one of the threads + static bool add(JBEvent* ev); + + static JBPendingWorker** s_threads; // Worker threads + static unsigned int s_threadCount; // Worker threads count + static Mutex s_mutex; // Mutex used to protect the public global data + +private: + // Process chat jobs + void processChat(JBPendingJob& job); + // Process iq jobs + void processIq(JBPendingJob& job); + // Reset the global index + bool resetIndex(); + + ObjList m_jobs; // The list is currently processing the first job + unsigned int m_index; // Thread index in global list +}; + +/* + * 'user.auth' message enqueued when a stream requires user password + */ +class UserAuthMessage : public Message +{ +public: + // Fill the message with authentication data + UserAuthMessage(JBEvent* ev); +protected: + // Check accepted and returned value. Calls stream's authenticated() method + virtual void dispatched(bool accepted); + + String m_stream; + JBStream::Type m_streamType; +}; + +/* + * Module message handlers + */ +class JBMessageHandler : public MessageHandler +{ +public: + // Message handlers + // Non-negative enum values will be used as handler priority + enum { + ResSubscribe = -1, // YJBEngine::handleResSubscribe() + ResNotify = -2, // YJBEngine::handleResNotify() + UserRoster = -3, // YJBEngine::handleUserRoster() + UserUpdate = -4, // YJBEngine::handleUserUpdate() + JabberItem = -5, // YJBEngine::handleJabberItem() + JabberIq = 150, // YJBEngine::handleJabberIq() + }; + JBMessageHandler(int handler); +protected: + virtual bool received(Message& msg); +private: + int m_handler; +}; + +/* + * Incoming connection listener + */ +class TcpListener : public Thread, public String +{ +public: + // engine The engine to be notified when a connection request is received + // t Expected connection type as stream type enumeration + // addr Address to bind + // port Port to bind + // backlog Maximum length of the queue of pending connections, 0 for system maximum + // prio Thread priority + inline TcpListener(const char* name, JBEngine* engine, JBStream::Type t, + const char* addr, int port, unsigned int backlog = 0, + Thread::Priority prio = Thread::Normal) + : Thread("TcpListener",prio), String(name), + m_engine(engine), m_type(t), m_address(addr), m_port(port), + m_backlog(backlog) + {} + // Remove from plugin + virtual ~TcpListener(); +protected: + // Add to plugin. Bind and start listening. Notify the jabber engine + // on incoming connections + virtual void run(); + // Terminate the socket. Show an error debug message if context is not null + void terminateSocket(const char* context = 0); +private: + JBEngine* m_engine; + JBStream::Type m_type; + Socket m_socket; + String m_address; + int m_port; + unsigned int m_backlog; // Pending connections queue length +}; + +/* + * The module + */ +class JBModule : public Module +{ + friend class TcpListener; // Add/remove to/from list +public: + JBModule(); + virtual ~JBModule(); + // Inherited methods + virtual void initialize(); + // Cancel a given listener or all listeners if name is empty + void cancelListener(const String& name = String::empty()); + // Check if a message was sent by us + inline bool isModule(const Message& msg) const { + String* module = msg.getParam("module"); + return module && *module == name(); + } + // Build a Message. Complete module and protocol parameters + inline Message* message(const char* msg) { + Message* m = new Message(msg); + complete(*m); + return m; + } + // Complete module and/or protocol parameters + inline void complete(Message& msg) { + msg.addParam("module",name()); + msg.addParam("protocol","jabber"); + } +protected: + // Inherited methods + virtual bool received(Message& msg, int id); + virtual void statusParams(String& str); + virtual void statusDetail(String& str); + virtual bool commandComplete(Message& msg, const String& partLine, + const String& partWord); + virtual bool commandExecute(String& retVal, const String& line); + // Build a listener from a list of parameters. Add it to the list and start it + bool buildListener(const String& name, NamedList& p); + // Add or remove a listener to/from list + void listener(TcpListener* l, bool add); +private: + bool m_init; + ObjList m_handlers; // Message handlers list + String m_domain; // Default domain served by the jabber engine + ObjList m_streamListeners; +}; + + +/* + * Local data + */ +JBPendingWorker** JBPendingWorker::s_threads = 0; +unsigned int JBPendingWorker::s_threadCount = 0; +Mutex JBPendingWorker::s_mutex(false,"JBPendingWorker"); +INIT_PLUGIN(JBModule); // The module +static YJBEntityCapsList s_entityCaps; +static YJBEngine* s_jabber = 0; + +// Commands help +static const char* s_cmdStatus = " status jabber [stream_name|{c2s|s2s} [remote_jid]]"; +static const char* s_cmdCreate = " jabber create remote_domain [local_domain]"; +static const char* s_cmdDropStreamName = " jabber drop stream_name"; +static const char* s_cmdDropStream = " jabber drop {c2s|s2s|*|all} [remote_jid]"; +static const char* s_cmdDropAll = " jabber drop {stream_name|{c2s|s2s|*|all} [remote_jid]}"; +static const char* s_cmdDebug = " jabber debug stream_name [debug_level|on|off]"; + +// Commands handled by this module (format module_name command [params]) +static const String s_cmds[] = { + "drop", + "create", + "debug", + "" +}; + +// Message handlers installed by the module +static const TokenDict s_msgHandler[] = { + {"resource.subscribe", JBMessageHandler::ResSubscribe}, + {"resource.notify", JBMessageHandler::ResNotify}, + {"user.roster", JBMessageHandler::UserRoster}, + {"user.update", JBMessageHandler::UserUpdate}, + {"jabber.iq", JBMessageHandler::JabberIq}, + {"jabber.item", JBMessageHandler::JabberItem}, + {0,0} +}; + +// Add xml data parameter to a message +static inline void addValidParam(NamedList& list, const char* param, const char* value) +{ + if (!TelEngine::null(value)) + list.addParam(param,value); +} + +// Add xml data parameter to a message +static void addXmlParam(Message& msg, XmlElement* xml) +{ + if (!xml) + return; + xml->removeAttribute("xmlns"); + xml->removeAttribute("from"); + xml->removeAttribute("to"); + NamedString* data = new NamedString("data"); + xml->toString(*data); + msg.addParam(data); +} + +// Build a response to an 'iq' get/set event +static inline XmlElement* buildIqResponse(JBEvent* ev, bool ok, XMPPUtils::IqType t, + XmlTag::Type xmlType, XMPPNamespace::Type ns) +{ + if (ok) { + if (t == XMPPUtils::IqGet) + return ev->buildIqResult(false,XMPPUtils::createElement(xmlType,ns)); + return ev->buildIqResult(false); + } + return ev->buildIqError(false,XMPPError::ServiceUnavailable); +} + +// Retrieve a presence from a Message +// Build one if not found +// Make sure the 'from' attribute is set +static inline XmlElement* getPresenceXml(Message& msg, const char* from, + XMPPUtils::Presence presType) +{ + XmlElement* xml = XMPPUtils::getPresenceXml(msg,"xml","data",presType); + xml->setAttribute("from",from); + return xml; +} + +// Get a space separated word from a buffer +// Return false if empty +static inline bool getWord(String& buf, String& word) +{ + XDebug(&__plugin,DebugAll,"getWord(%s)",buf.c_str()); + int pos = buf.find(" "); + if (pos >= 0) { + word = buf.substr(0,pos); + buf = buf.substr(pos + 1); + } + else { + word = buf; + buf = ""; + } + if (!word) + return false; + return true; +} + +// Add a 'subscription' and, optionally, an 'ask' attribute to a roster item +static inline void addSubscription(XmlElement& dest, const String& sub) +{ + XMPPDirVal d(sub); + if (d.test(XMPPDirVal::PendingOut)) + dest.setAttribute("ask","subscribe"); + String tmp; + d.toSubscription(tmp); + dest.setAttribute("subscription",tmp); +} + +// Build a roster item XML element from an array row +static XmlElement* buildRosterItem(NamedList& list, unsigned int index) +{ + String prefix("contact."); + prefix << index; + const char* contact = list.getValue(prefix); + XDebug(&__plugin,DebugAll,"buildRosterItem(%s,%u) contact=%s", + list.c_str(),index,contact); + if (TelEngine::null(contact)) + return 0; + XmlElement* item = new XmlElement("item"); + item->setAttribute("jid",contact); + prefix << "."; + ObjList* groups = 0; + unsigned int n = list.length(); + for (unsigned int i = 0; i < n; i++) { + NamedString* param = list.getParam(i); + if (!(param && param->name().startsWith(prefix))) + continue; + String name = param->name(); + name.startSkip(prefix,false); + if (name == "name") + item->setAttribute("name",*param); + else if (name == "subscription") + addSubscription(*item,*param); + else if (name == "groups") { + if (!groups) + groups = param->split(',',false); + } + else + item->addChild(XMPPUtils::createElement(name,*param)); + } + if (!item->getAttribute("subscription")) + addSubscription(*item,String::empty()); + for (ObjList* o = groups ? groups->skipNull() : 0; o; o = o->skipNext()) { + String* grp = static_cast(o->get()); + item->addChild(XMPPUtils::createElement("group",*grp)); + } + TelEngine::destruct(groups); + return item; +} + +// Complete stream type +static void completeStreamType(String& buf, const String& part, bool addAll = false) +{ + static const String t[] = {"c2s","s2s",""}; + static const String all[] = {"all","*",""}; + for (const String* d = t; !d->null(); d++) + Module::itemComplete(buf,*d,part); + if (addAll) + for (const String* d = all; !d->null(); d++) + Module::itemComplete(buf,*d,part); +} + + +/* + * YJBEntityCapsList + */ +// Load the entity caps file +void YJBEntityCapsList::load() +{ + if (!m_enable) + return; + String file; + getEntityCapsFile(file); + loadXmlDoc(file,s_jabber); +} + +// Notify changes and save the entity caps file +void YJBEntityCapsList::capsAdded(JBEntityCaps* caps) +{ + if (!caps) { + // TODO: Notify all + return; + } + // Notify + Message* m = __plugin.message("resource.notify"); + m->addParam("operation","updatecaps"); + m->addParam("id",caps->toString()); + addCaps(*m,*caps); + Engine::enqueue(m); + // Save the file + String file; + getEntityCapsFile(file); + saveXmlDoc(file,s_jabber); +} + + +/* + * YJBEngine + */ +YJBEngine::YJBEngine() + : m_c2sTlsRequired(false) +{ + m_c2sReceive = new YStreamSetReceive(this,10,"c2s/recv"); + m_c2sProcess = new YStreamSetProcess(this,10,"c2s/process"); + m_s2sReceive = new YStreamSetReceive(this,0,"s2s/recv"); + m_s2sProcess = new YStreamSetProcess(this,0,"s2s/process"); + // c2s features + m_c2sFeatures.add(XMPPNamespace::DiscoInfo); + m_c2sFeatures.add(XMPPNamespace::DiscoItems); + m_c2sFeatures.add(XMPPNamespace::Roster); + m_c2sFeatures.add(XMPPNamespace::IqPrivate); + m_c2sFeatures.add(XMPPNamespace::VCard); + m_c2sFeatures.add(XMPPNamespace::MsgOffline); + m_c2sFeatures.add(XMPPNamespace::IqVersion); + m_c2sFeatures.add(XMPPNamespace::Session); + m_c2sFeatures.add(XmlTag::Register,XMPPNamespace::Register); + m_c2sFeatures.m_identities.append(new JIDIdentity("server","im")); + m_c2sFeatures.updateEntityCaps(); + // Non c2s features + m_features.add(XMPPNamespace::DiscoInfo); + m_features.add(XMPPNamespace::DiscoItems); + m_features.add(XMPPNamespace::VCard); + m_features.add(XMPPNamespace::MsgOffline); + m_features.add(XMPPNamespace::IqVersion); + m_features.m_identities.append(new JIDIdentity("server","im")); + m_features.updateEntityCaps(); +} + +YJBEngine::~YJBEngine() +{ +} + +// (Re)initialize engine +void YJBEngine::initialize(const NamedList* params, bool first) +{ + NamedList dummy(""); + lock(); + if (!params) + params = &dummy; + // Serviced domains + setDomains(params->getValue("domains")); + // Restricted resources + String* res = params->getParam("restricted_resources"); + m_restrictedResources.clear(); + if (res) { + ObjList* list = res->split(',',false); + for (ObjList* o = list->skipNull(); o; o = o->skipNext()) { + String* tmp = static_cast(o->get()); + if (!m_restrictedResources.find(*tmp)) + m_restrictedResources.append(new String(*tmp)); + } + TelEngine::destruct(list); + } + + if (first) { + m_dialbackSecret = params->getValue("dialback_secret"); + if (!m_dialbackSecret) + m_dialbackSecret << (unsigned int)Time::msecNow() << "_" << (int)::random(); + } + + m_c2sTlsRequired = params->getBoolValue("c2s_tlsrequired"); + + // Update default remote domain + if (params->getBoolValue("s2s_tlsrequired")) + m_remoteDomain.m_flags |= JBStream::TlsRequired; + else + m_remoteDomain.m_flags &= ~JBStream::TlsRequired; + + // Program name and version to be advertised on request + if (!m_progName) { + m_progName = "Yate"; + m_progVersion.clear(); + m_progVersion << Engine::runParams().getValue("version") << "" << + Engine::runParams().getValue("release"); + // TODO: set program name and version for server identities + } + unlock(); + JBEngine::initialize(*params); + + // TODO: update stream sets options +} + +// Process events +void YJBEngine::processEvent(JBEvent* ev) +{ + if (!(ev && ev->stream())) { + if (ev && !ev->stream()) + Debug(this,DebugStub,"Event (%p,'%s') without stream",ev,ev->name()); + TelEngine::destruct(ev); + return; + } + XDebug(this,DebugInfo,"Processing event (%p,%s)",ev,ev->name()); + switch (ev->type()) { + case JBEvent::Message: + JBPendingWorker::add(ev); + break; + case JBEvent::Presence: + if (ev->element()) + processPresenceStanza(ev); + break; + case JBEvent::Iq: + JBPendingWorker::add(ev); + break; + case JBEvent::Start: + if (ev->stream()->incoming()) + processStartIn(ev); + break; + case JBEvent::Auth: + if (ev->stream()->incoming()) + processAuthIn(ev); + break; + case JBEvent::Bind: + processBind(ev); + break; + case JBEvent::Running: + case JBEvent::Destroy: + case JBEvent::Terminated: + processStreamEvent(ev); + break; + case JBEvent::DbResult: + processDbResult(ev); + break; + case JBEvent::DbVerify: + processDbVerify(ev); + break; + default: + returnEvent(ev,XMPPError::ServiceUnavailable); + return; + } + TelEngine::destruct(ev); +} + +// Build an internal stream name from node name and stream index +void YJBEngine::buildStreamName(String& name) +{ +// name << Engine::nodeName() << "/"; + JBServerEngine::buildStreamName(name); +} + +// Start stream TLS +void YJBEngine::encryptStream(JBStream* stream) +{ + if (!stream) + return; + DDebug(this,DebugAll,"encryptStream(%p,'%s')",stream,stream->toString().c_str()); + Message msg("socket.ssl"); + msg.userData(stream); + msg.addParam("server",String::boolText(stream->incoming())); + if (stream->incoming()) + msg.addParam("domain",stream->local().domain()); + if (!Engine::dispatch(msg)) + stream->terminate(0,stream->incoming(),0,XMPPError::Internal,"SSL start failure"); +} + +// Connect an outgoing stream +void YJBEngine::connectStream(JBStream* stream) +{ + if (Engine::exiting() || exiting()) + return; + if (stream && stream->outgoing()) + (new YJBConnectThread(*stream))->startup(); +} + +// Build a dialback key +void YJBEngine::buildDialbackKey(const String& id, String& key) +{ + MD5 md5; + md5 << id << m_dialbackSecret; + key = md5.hexDigest(); +} + +// Check if a domain is serviced by this engine +bool YJBEngine::hasDomain(const String& domain) +{ + Lock lock(this); + return domain && m_domains.find(domain); +} + +// Replace the list of domains service by this engine +void YJBEngine::setDomains(const String& list) +{ + // TODO: check if an existing domain is no longer accepted + // Terminate all streams having local party the deleted domain + Lock lock(this); + m_domains.clear(); + ObjList* l = list.split(',',false); + for (ObjList* o = l->skipNull(); o; o = l->skipNull()) { + String* s = static_cast(o->remove(false)); + s->toLower(); + m_domains.append(s); + } + TelEngine::destruct(l); + if (m_domains.skipNull()) { + if (debugAt(DebugAll)) { + String tmp; + for (ObjList* o = m_domains.skipNull(); o; o = o->skipNext()) + tmp.append(o->get()->toString(),","); + DDebug(this,DebugAll,"Configured domains='%s'",tmp.c_str()); + } + } + else + Debug(this,DebugGoOn,"No domains configured"); +} + +// Check if a resource name is retricted +bool YJBEngine::restrictedResource(const String& name) +{ + Lock lock(this); + for (ObjList* o = m_restrictedResources.skipNull(); o; o = o->skipNext()) { + String* s = static_cast(o->get()); + if (s->startsWith(name)) + return true; + } + return false; +} + +// Process an 'user.roster' messages +void YJBEngine::handleUserRoster(Message& msg) +{ + String* what = msg.getParam("notify"); + if (TelEngine::null(what)) + return; + JabberID to(msg.getValue("username")); + if (!to.node()) + return; + const char* contact = msg.getValue("contact"); + Debug(this,DebugAll,"handleUserRoster(%s,%s) notify=%s", + to.c_str(),contact,what->c_str()); + XmlElement* item = 0; + if (*what == "update") + item = buildRosterItem(msg,1); + else if (*what == "delete") { + JabberID c(contact); + if (!c.node()) + return; + item = new XmlElement("item"); + item->setAttribute("jid",c.bare()); + item->setAttribute("subscription","remove"); + } + if (!item) + return; + XmlElement* query = XMPPUtils::createElement(XmlTag::Query,XMPPNamespace::Roster); + query->addChild(item); + XmlElement* xml = XMPPUtils::createIq(XMPPUtils::IqSet,0,0, + String((unsigned int)msg.msgTime().msec())); + xml->addChild(query); + // RFC 3920bis 2.2: + // Send roster pushes to clients that requested the roster + ObjList* streams = findClientStreams(true,to,JBStream::RosterRequested); + sendStanza(xml,streams); +} + +// Process an 'user.update' messages +void YJBEngine::handleUserUpdate(Message& msg) +{ + JabberID user(msg.getValue("user")); + if (!user) + return; + String* notif = msg.getParam("notify"); + if (TelEngine::null(notif) || *notif != "delete") + return; + // Don't set any error string: the stream might not be authenticated + terminateClientStreams(user,XMPPError::Reg); +} + +// Process a 'jabber.iq' messages +bool YJBEngine::handleJabberIq(Message& msg) +{ + JabberID from(msg.getValue("from")); + JabberID to(msg.getValue("to")); + if (!from.resource()) + from.resource(msg.getValue("from_instance")); + if (!to.resource()) + to.resource(msg.getValue("to_instance")); + if (!(from && to)) + return false; + DDebug(this,DebugAll,"YJBEngine::handleJabberIq() from=%s to=%s",from.c_str(),to.c_str()); + JBStream* stream = 0; + if (hasDomain(to.domain())) { + stream = findClientStream(true,to); + if (!(stream && stream->flag(JBStream::AvailableResource))) + TelEngine::destruct(stream); + } + else + stream = getServerStream(from,to); + if (!stream) + return false; + XmlElement* xml = XMPPUtils::getXml(msg); + bool ok = (xml != 0); + if (xml) { + xml->removeAttribute("xmlns"); + xml->setAttribute("from",from); + xml->setAttribute("to",to); + ok = stream->sendStanza(xml); + } + TelEngine::destruct(stream); + return ok; +} + +// Process an 'resource.subscribe' messages +bool YJBEngine::handleResSubscribe(Message& msg) +{ + String* oper = msg.getParam("operation"); + if (TelEngine::null(oper)) + return false; + XMPPUtils::Presence presType = XMPPUtils::presenceType(*oper); + if (presType != XMPPUtils::Subscribe && presType != XMPPUtils::Unsubscribe) + return false; + JabberID from(msg.getValue("subscriber")); + JabberID to(msg.getValue("notifier")); + if (!(from.node() && to.bare())) + return false; + Debug(this,DebugAll,"Processing %s from=%s to=%s oper=%s", + msg.c_str(),from.bare().c_str(),to.bare().c_str(),oper->c_str()); + XmlElement* xml = getPresenceXml(msg,from.bare(),presType); + bool ok = false; + if (hasDomain(to.domain())) { + xml->removeAttribute("to"); + // RFC 3921: (un)subscribe requests are sent only to available resources + String* instance = msg.getParam("instance"); + if (!TelEngine::null(instance)) { + to.resource(*instance); + JBClientStream* s = findClientStream(true,to); + if (s && s->flag(JBStream::AvailableResource)) + ok = s->sendStanza(xml); + TelEngine::destruct(s); + } + else { + ObjList* list = findClientStreams(true,to,JBStream::AvailableResource); + ok = sendStanza(xml,list); + } + } + else { + // Make sure the 'to' attribute is correct + xml->setAttribute("to",to.bare()); + JBStream* stream = getServerStream(from,to); + ok = stream && stream->sendStanza(xml); + TelEngine::destruct(stream); + } + TelEngine::destruct(xml); + return ok; +} + +// Process an 'resource.notify' messages +bool YJBEngine::handleResNotify(Message& msg) +{ + String* oper = msg.getParam("operation"); + if (TelEngine::null(oper)) + return false; + JabberID from(msg.getValue("from")); + JabberID to(msg.getValue("to")); + if (!(from.node() && to.node())) + return false; + Debug(this,DebugAll,"Processing %s from=%s to=%s oper=%s", + msg.c_str(),from.c_str(),to.c_str(),oper->c_str()); + XmlElement* xml = 0; + bool c2s = hasDomain(to.domain()); + bool online = (*oper == "online" || *oper == "update"); + if (online || *oper == "offline" || *oper == "delete") { + if (!from.resource()) + from.resource(msg.getValue("from_instance")); + if (!from.resource() && online) + return false; + if (!to.resource()) + to.resource(msg.getValue("to_instance")); + xml = getPresenceXml(msg,from, + online ? XMPPUtils::PresenceNone : XMPPUtils::Unavailable); + } + else { + bool sub = (*oper == "subscribed"); + if (sub || *oper == "unsubscribed") { + // Don't sent (un)subscribed to clients + if (c2s) + return false; + // Make sure 'to' is a bare jid + to.resource(""); + xml = getPresenceXml(msg,from.bare(), + sub ? XMPPUtils::Subscribed : XMPPUtils::Unsubscribed); + } + else if (*oper == "probe") { + // Don't sent probe to clients + if (c2s) + return false; + // Make sure 'to' is a bare jid + to.resource(""); + xml = getPresenceXml(msg,from.bare(),XMPPUtils::Probe); + } + else if (*oper == "error") { + if (!from.resource()) + from.resource(msg.getValue("from_instance")); + if (!to.resource()) + to.resource(msg.getValue("to_instance")); + if (!(from.resource() && to.resource())) + return false; + xml = getPresenceXml(msg,from,XMPPUtils::PresenceError); + } + else + return false; + } + bool ok = false; + if (c2s) { + // We don't need to send the 'to' attribute + // Remove it to make sure we don't send a wrong value (trust the + // 'to' parameter received with the message) + xml->removeAttribute("to"); + // Ignore streams whose clients didn't sent the initial presence + if (to.resource()) { + JBClientStream* s = findClientStream(true,to); + ok = s && s->flag(JBStream::AvailableResource) && s->sendStanza(xml); + TelEngine::destruct(s); + } + else { + ObjList* list = findClientStreams(true,to,JBStream::AvailableResource); + ok = sendStanza(xml,list); + } + } + else { + // Make sure the 'to' attribute is correct + xml->setAttribute("to",to); + JBStream* stream = getServerStream(from,to); + ok = stream && stream->sendStanza(xml); + TelEngine::destruct(stream); + } + TelEngine::destruct(xml); + return ok; +} + +// Process a 'msg.execute' messages +bool YJBEngine::handleMsgExecute(Message& msg) +{ + JabberID caller(msg.getValue("caller")); + JabberID called(msg.getValue("called")); + if (!caller.resource()) + caller.resource(msg.getValue("caller_instance")); + if (!(caller.resource())) + return false; + DDebug(this,DebugAll,"handleMsgExecute() caller=%s called=%s",caller.c_str(),called.c_str()); + if (hasDomain(called.domain())) { + // RFC 3921 11.1: Send chat only to clients with non-negative resource priority + bool ok = false; + unsigned int n = msg.getIntValue("instance.count"); + int flags = JBStream::AvailableResource | JBStream::PositivePriority; + if (n) { + ObjList resources; + for (unsigned int i = 1; i <= n; i++) { + String prefix("instance."); + prefix << i; + String* tmp = msg.getParam(prefix); + if (TelEngine::null(tmp)) + continue; + resources.append(new String(tmp->c_str())); + } + ObjList* streams = findClientStreams(true,called,resources,flags); + if (streams) { + XmlElement* xml = XMPPUtils::getChatXml(msg); + if (xml) { + called.resource(""); + xml->setAttribute("from",caller); + xml->setAttribute("to",called); + } + ok = sendStanza(xml,streams); + } + } + else { + // Directed chat + if (!called.resource()) + called.resource(msg.getValue("called_resource")); + JBClientStream* stream = called.resource() ? findClientStream(true,called) : 0; + bool ok = stream && stream->flag(flags); + if (ok) { + XmlElement* xml = XMPPUtils::getChatXml(msg); + if (xml) { + xml->setAttribute("from",caller); + xml->setAttribute("to",called); + ok = stream->sendStanza(xml); + } + else + ok = false; + } + TelEngine::destruct(stream); + } + return ok; + } + + // Remote domain + JBStream* stream = s_jabber->getServerStream(caller,called); + if (!stream) + return false; + bool ok = false; + XmlElement* xml = XMPPUtils::getChatXml(msg); + if (xml) { + xml->setAttribute("from",caller); + xml->setAttribute("to",called); + ok = stream->sendStanza(xml); + } + TelEngine::destruct(stream); + return ok; +} + +// Process 'jabber.item' messages +bool YJBEngine::handleJabberItem(Message& msg) +{ + Lock lock(this); + const char* jid = msg.getValue("jid"); + ObjList* o = m_items.find(jid); + if (msg.getBoolValue("remove")) { + if (!o) + return false; + o->remove(); + Debug(this,DebugAll,"Removed item '%s'",jid); + } + else if (!o) { + m_items.append(new String(jid)); + Debug(this,DebugAll,"Added item '%s'",jid); + } + return false; +} + +// Handle 'presence' stanzas +// s2s: Destination domain was already checked by the lower layer +// The given event is always valid and carry a valid stream and xml element +void YJBEngine::processPresenceStanza(JBEvent* ev) +{ + Debug(this,DebugAll,"Processing presence type=%s from=%s to=%s stream=%s", + ev->stanzaType().c_str(),ev->from().c_str(),ev->to().c_str(), + ev->stream()->typeName()); + JBServerStream* s2s = ev->serverStream(); + JBClientStream* c2s = ev->clientStream(); + if (!(c2s || s2s)) { + Debug(this,DebugStub, + "processPresenceStanza(%s) not implemented for stream type '%s'", + ev->stanzaType().c_str(),lookup(ev->stream()->type(),JBStream::s_typeName)); + return; + } + if (c2s && c2s->outgoing()) { + Debug(this,DebugStub, + "processPresenceStanza(%s) not implemented for outgoing client streams", + ev->stanzaType().c_str()); + ev->sendStanzaError(XMPPError::ServiceUnavailable); + } + XMPPUtils::Presence pres = XMPPUtils::presenceType(ev->stanzaType()); + bool online = false; + String capsId; + switch (pres) { + case XMPPUtils::PresenceNone: + online = true; + // Update caps + s_entityCaps.processCaps(capsId,ev->element(),ev->stream(),ev->to(),ev->from()); + case XMPPUtils::Unavailable: + if (c2s) { + bool offlinechat = false; + if (!ev->to()) { + if (!c2s->remote().resource()) + break; + // Presence broadcast + int prio = XMPPUtils::priority(*ev->element()); + offlinechat = c2s->setAvailableResource(online,prio >= 0) && + online && c2s->flag(JBStream::PositivePriority); + notifyPresence(*c2s,online,ev->element(),capsId); + } + else + notifyPresence(ev->from(),ev->to(),online,ev->element(), + false,hasDomain(ev->to().domain()),capsId); + if (offlinechat) { + Message* m = jabberFeature(0,XMPPNamespace::MsgOffline, + JBStream::c2s,ev->from(),0,"query"); + if (m && Engine::dispatch(*m)) { + unsigned int n = m->length(); + bool ok = false; + for (unsigned int i = 0; i < n; i++) { + NamedString* p = m->getParam(i); + if (p && p->name() == "xml") { + XmlElement* xml = XMPPUtils::getXml(p); + if (xml) + ok = c2s->sendStanza(xml) && ok; + } + } + if (ok) + Engine::enqueue(jabberFeature(0,XMPPNamespace::MsgOffline, + JBStream::c2s,ev->from(),0,"delete")); + } + TelEngine::destruct(m); + } + return; + } + if (s2s) { + notifyPresence(ev->from(),ev->to(),online,ev->element(),true,false,capsId); + return; + } + break; + case XMPPUtils::Subscribe: + case XMPPUtils::Unsubscribe: + if (ev->to()) { + Message* m = __plugin.message("resource.subscribe"); + m->addParam("operation",ev->stanzaType()); + m->addParam("subscriber",ev->from().bare()); + m->addParam("subscriber_local",String::boolText(c2s != 0)); + m->addParam("notifier",ev->to().bare()); + m->addParam("notifier_local",String::boolText(hasDomain(ev->to().domain()))); + addXmlParam(*m,ev->element()); + Engine::enqueue(m); + return; + } + break; + case XMPPUtils::Subscribed: + case XMPPUtils::Unsubscribed: + case XMPPUtils::Probe: + if (ev->to()) { + Message* m = __plugin.message("resource.notify"); + m->addParam("operation",ev->stanzaType()); + m->addParam("from",ev->from().bare()); + m->addParam("from_local",String::boolText(c2s != 0)); + m->addParam("to",ev->to().bare()); + m->addParam("to_local",String::boolText(hasDomain(ev->to().domain()))); + addXmlParam(*m,ev->element()); + Engine::enqueue(m); + return; + } + break; + case XMPPUtils::PresenceError: + // TODO: enqueue some message + return; + } + ev->sendStanzaError(XMPPError::ServiceUnavailable); +} + +// Process a stream start element received by an incoming stream +// The given event is always valid and carry a valid stream +// Set local domain and stream features to advertise to remote party +void YJBEngine::processStartIn(JBEvent* ev) +{ + static const char* node = "http://yate.null.ro/yate/server/caps"; + + // Set c2s stream TLS required flag + if (ev->stream()->type() == JBStream::c2s) + ev->stream()->setTlsRequired(m_c2sTlsRequired); + + // Don't advertise any features if version is not 1 + if (!ev->stream()->flag(JBStream::StreamRemoteVer1)) { + ev->stream()->start(); + return; + } + + // Set stream features + XMPPFeatureList features; + // Add TLS if not secured + if (!ev->stream()->flag(JBStream::StreamSecured)) + features.add(XmlTag::Starttls,XMPPNamespace::Tls, + ev->stream()->flag(JBStream::TlsRequired)); + // Done for s2s streams + if (ev->stream()->type() == JBStream::s2s) { + ev->stream()->start(&features); + return; + } + XMPPFeature* tls = features.get(XMPPNamespace::Tls); + bool addReg = ev->stream()->type() == JBStream::c2s && + m_c2sFeatures.get(XMPPNamespace::Register); + bool addCaps = false; + if (!(tls && tls->required())) { + addCaps = true; + // Stream secured + // Add SASL if stream is not authenticated + if (!ev->stream()->flag(JBStream::StreamAuthenticated)) { + int mech = 0; + switch (ev->stream()->type()) { + case JBStream::c2s: + mech = XMPPUtils::AuthMD5 | XMPPUtils::AuthPlain; + break; + case JBStream::s2s: + mech = XMPPUtils::AuthMD5 | XMPPUtils::AuthPlain; + break; + } + if (mech) + features.add(new XMPPFeatureSasl(mech,true)); + } + // Add register if enabled + if (addReg) + features.add(XmlTag::Register,XMPPNamespace::Register); + XMPPFeature* sasl = features.get(XMPPNamespace::Sasl); + if (!(sasl && sasl->required())) { + if (ev->stream()->type() == JBStream::c2s) { + // TLS and/or SASL are missing or not required: add bind + features.add(XmlTag::Bind,XMPPNamespace::Bind,true); + } + } + } + else if (addReg && tls && !tls->required()) { + // Stream not secured, TLS not required: add register + features.add(XmlTag::Register,XMPPNamespace::Register); + } + ev->releaseStream(); + XmlElement* caps = 0; + if (ev->stream()->type() == JBStream::c2s && addCaps) + caps = XMPPUtils::createEntityCaps(m_features.m_entityCapsHash,node); + ev->stream()->start(&features,caps); +} + +// Process Auth events from incoming streams +// The given event is always valid and carry a valid stream +void YJBEngine::processAuthIn(JBEvent* ev) +{ + ev->stream()->lock(); + bool plain = ev->stream()->m_sasl && ev->stream()->m_sasl->m_plain && + !ev->stream()->flag(JBStream::StreamTls); + ev->stream()->unlock(); + if (plain) { + // TODO: check if the remote party is allowed to use plain + // password auth on unsecured transport + ev->releaseStream(); + ev->stream()->authenticated(false,String::empty(),XMPPError::EncryptionRequired); + return; + } + Engine::enqueue(new UserAuthMessage(ev)); +} + +// Process Bind events +// The given event is always valid and carry a valid stream +void YJBEngine::processBind(JBEvent* ev) +{ + JBClientStream* c2s = ev->clientStream(); + if (!(c2s && c2s->incoming() && ev->child())) { + ev->sendStanzaError(XMPPError::ServiceUnavailable); + return; + } + String resource; + XmlElement* res = XMPPUtils::findFirstChild(*ev->child(),XmlTag::Resource); + if (res) { + // Lock the engine to prevent other stream to bind the same resource + lock(); + resource = res->getText(); + if (resource) { + if (restrictedResource(resource)) + resource.clear(); + else { + Message* m = __plugin.message("resource.notify"); + m->addParam("operation","query"); + m->addParam("nodata",String::boolText(true)); + m->addParam("contact",c2s->remote().bare()); + m->addParam("instance",resource); + if (Engine::dispatch(*m)) + resource.clear(); + TelEngine::destruct(m); + } + } + unlock(); + } + if (!resource) { + MD5 md5(c2s->id()); + resource = md5.hexDigest(); + } + Message* m = userRegister(*c2s,true,resource); + if (Engine::dispatch(m)) + c2s->bind(resource,ev->id()); + else + ev->sendStanzaError(XMPPError::NotAuthorized); + TelEngine::destruct(m); +} + +// Process stream Running, Destroy, Terminated events +// The given event is always valid and carry a valid stream +void YJBEngine::processStreamEvent(JBEvent* ev) +{ + XDebug(this,DebugAll,"YJBEngine::processStreamEvent(%p,%s)",ev,ev->name()); + JBStream* s = ev->stream(); + bool in = s->incoming(); + bool reg = ev->type() == JBEvent::Running; + Message* m = 0; + if (in) { + if (reg) { + // Client streams are registered when a resource is bound to the stream + if (s->type() != JBStream::c2s) + m = userRegister(*s,true); + } + else { + bool changed = s->setAvailableResource(false); + s->setRosterRequested(false); + if (s->type() == JBStream::c2s) { + // Notify 'offline' for client streams that forgot to send 'unavailable' + if (changed && s->remote().resource()) + notifyPresence(*(static_cast(s)),false,0,String::empty()); + // Unregister + m = userRegister(*s,false); + } + else { + // TODO: notify offline for all users in remote domain + m = userRegister(*s,false); + } + } + } + else { + // Notify verify failure on dialback streams + if (!reg) { + JBServerStream* s2s = ev->serverStream(); + NamedString* db = s2s ? s2s->takeDb() : 0; + if (db) { + notifyDbVerifyResult(s2s->local(),s2s->remote(),db->name(),false); + TelEngine::destruct(db); + } + } + m = __plugin.message("user.notify"); + m->addParam("account",s->name()); + if (s->type() == JBStream::c2s) + m->addParam("username",s->local().node()); + m->addParam("server",s->local().domain()); + m->addParam("jid",s->local()); + m->addParam("registered",String::boolText(reg)); + if (!reg && ev->text()) + m->addParam("error",ev->text()); + bool restart = (s->state() != JBStream::Destroy && !s->flag(JBStream::NoAutoRestart)); + m->addParam("autorestart",String::boolText(restart)); + } + Engine::enqueue(m); +} + +// Process stream DbResult events +// The given event is always valid and carry a valid stream +void YJBEngine::processDbResult(JBEvent* ev) +{ + JBServerStream* stream = ev->serverStream(); + const char* id = stream ? stream->id().c_str() : 0; + if (id && ev->text() && stream && ev->to() && hasDomain(ev->to()) && ev->from()) { + // Check if we already have a stream to the remote domain + // Build a dialback only outgoing stream if found + JBServerStream* s = findServerStream(ev->to(),ev->from(),true); + bool dbOnly = (s != 0); + TelEngine::destruct(s); + s = createServerStream(ev->to(),ev->from(),id,ev->text(),dbOnly); + if (s) { + TelEngine::destruct(s); + return; + } + } + Debug(this,DebugWarn, + "Failed to authenticate dialback request from=%s to=%s id=%s key=%s", + ev->from().c_str(),ev->to().c_str(),id,ev->text().c_str()); + if (stream) + stream->sendDbResult(ev->to(),ev->from(),false); +} + +// Process stream DbVerify events +// The given event is always valid and carry a valid stream +void YJBEngine::processDbVerify(JBEvent* ev) +{ + JBServerStream* stream = ev->serverStream(); + if (!(stream && ev->element())) + return; + String id(ev->element()->getAttribute("id")); + // Incoming: this is a verify request for an outgoing stream + if (stream->incoming()) { + String key; + if (id) + buildDialbackKey(id,key); + bool valid = key && key == ev->element()->getText(); + stream->sendDbVerify(ev->to(),ev->from(),id,valid); + return; + } + // Outgoing: we have an incoming stream to authenticate + // Remove the dialback request from the stream and check it + bool valid = false; + NamedString* db = stream->takeDb(); + XMPPError::Type err = XMPPError::NoError; + if (id && db && db->name() == id) + valid = String("valid") == ev->element()->getAttribute("type"); + else + err = XMPPError::InvalidId; + TelEngine::destruct(db); + // Terminate dialback only streams and notify the incoming one + if (stream->dialback()) + stream->terminate(-1,true,0,err); + notifyDbVerifyResult(ev->to(),ev->from(),id,valid); +} + +// Process all incoming jabber:iq:roster stanzas +// The given event is always valid and carry a valid element +XmlElement* YJBEngine::processIqRoster(JBEvent* ev, JBStream::Type sType, + XMPPUtils::IqType t) +{ + if (sType != JBStream::c2s) { + Debug(this,DebugInfo,"processIqRoster(%p) type=%s on non-client stream", + ev,ev->stanzaType().c_str()); + // Roster management not allowed from other servers + if (t == XMPPUtils::IqGet && t == XMPPUtils::IqSet) + return ev->buildIqError(false,XMPPError::NotAllowed); + return 0; + } + // Ignore responses + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) + return 0; + DDebug(this,DebugInfo,"processIqRoster type=%s",ev->stanzaType().c_str()); + Message* m = jabberFeature(ev->releaseXml(),XMPPNamespace::Roster,sType,ev->from(),ev->to()); + bool ok = Engine::dispatch(m); + XmlElement* rsp = XMPPUtils::getXml(*m,"response"); + TelEngine::destruct(m); + if (rsp) + return rsp; + return buildIqResponse(ev,ok,t,XmlTag::Query,XMPPNamespace::Roster); +} + +// Process all incoming vcard-temp with target in our domain(s) +// The given event is always valid and carry a valid element +// Return the response +// XEP-0054 vcard-temp +XmlElement* YJBEngine::processIqVCard(JBEvent* ev, JBStream::Type sType, + XMPPUtils::IqType t) +{ + DDebug(this,DebugAll,"processIqVCard(%p) type=%s from=%s", + ev,ev->stanzaType().c_str(),ev->from().c_str()); + // Ignore responses + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) + return 0; + // Make sure we have a 'from' + if (!ev->from().bare()) + return ev->buildIqError(false,XMPPError::ServiceUnavailable); + Message* m = 0; + if (t == XMPPUtils::IqSet) { + // Only the connected client can set its vcard + if (sType != JBStream::c2s) + return ev->buildIqError(false,XMPPError::ServiceUnavailable); + if (ev->to() && ev->to() != ev->from().domain()) + return ev->buildIqError(false,XMPPError::ServiceUnavailable); + m = jabberFeature(ev->releaseXml(),XMPPNamespace::VCard,sType,ev->from()); + } + else if (!ev->to() || ev->to() == ev->from().domain()) + m = jabberFeature(ev->releaseXml(),XMPPNamespace::VCard,sType,ev->from()); + else + m = jabberFeature(ev->releaseXml(),XMPPNamespace::VCard,sType,ev->from(),ev->to()); + bool ok = Engine::dispatch(m); + XmlElement* rsp = XMPPUtils::getXml(*m,"response"); + TelEngine::destruct(m); + if (rsp) + return rsp; + return buildIqResponse(ev,ok,t,XmlTag::VCard,XMPPNamespace::VCard); +} + +// Process all incoming jabber:iq:private +// The given event is always valid and carry a valid element +// Return the response +// XEP-0049 Private XML storage +XmlElement* YJBEngine::processIqPrivate(JBEvent* ev, JBStream::Type sType, + XMPPUtils::IqType t) +{ + if (sType != JBStream::c2s) { + Debug(this,DebugInfo,"processIqPrivate(%p) type=%s on non-client stream", + ev,ev->stanzaType().c_str()); + // User private data management not allowed from other servers + if (t == XMPPUtils::IqGet || t == XMPPUtils::IqSet) + return ev->buildIqError(false,XMPPError::NotAllowed); + return 0; + } + DDebug(this,DebugAll,"processIqPrivate(%p) type=%s from=%s", + ev,ev->stanzaType().c_str(),ev->from().c_str()); + // Ignore responses + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) + return 0; + // Make sure the client don't request/set another user's private data + if (ev->to() && ev->to().bare() != ev->from().bare()) + return ev->buildIqError(false,XMPPError::Forbidden); + if (!ev->from().resource()) + return ev->buildIqError(false,XMPPError::ServiceUnavailable); + Message* m = jabberFeature(ev->releaseXml(),XMPPNamespace::IqPrivate,sType,ev->from()); + bool ok = Engine::dispatch(m); + XmlElement* rsp = XMPPUtils::getXml(*m,"response"); + TelEngine::destruct(m); + if (rsp) + return rsp; + return buildIqResponse(ev,ok,t,XmlTag::Query,XMPPNamespace::IqPrivate); +} + +// Process all incoming jabber:iq:register stanzas +// The given event is always valid and carry a valid element +// XEP-0077 In-Band Registration +XmlElement* YJBEngine::processIqRegister(JBEvent* ev, JBStream::Type sType, + XMPPUtils::IqType t, const String& domain, int flags) +{ + if (sType != JBStream::c2s) { + Debug(this,DebugInfo,"processIqRegister(%p) type=%s on non-client stream", + ev,ev->stanzaType().c_str()); + // In-band registration not allowed from other servers + if (t == XMPPUtils::IqGet || t == XMPPUtils::IqSet) + return ev->buildIqError(false,XMPPError::NotAllowed); + return 0; + } + DDebug(this,DebugAll,"processIqRegister(%p) type=%s",ev,ev->stanzaType().c_str()); + // Ignore responses + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) + return 0; + Message* m = jabberFeature(ev->releaseXml(),XMPPNamespace::IqRegister,sType,ev->from()); + m->addParam("stream_domain",domain); + m->addParam("stream_flags",String(flags)); + Engine::dispatch(m); + XmlElement* rsp = XMPPUtils::getXml(*m,"response"); + TelEngine::destruct(m); + return rsp; +} + +// Handle disco info requests addressed to the server +XmlElement* YJBEngine::discoInfo(JBEvent* ev, JBStream::Type sType) +{ + XMPPError::Type error = XMPPError::NoError; + if (ev->stanzaType() == "get" && + XMPPUtils::isUnprefTag(*ev->child(),XmlTag::Query)) { + String* node = ev->child()->getAttribute("node"); + bool ok = TelEngine::null(node); + Lock lock(this); + if (!ok && ev->to().domain() && node->startsWith(ev->to().domain())) { + char c = node->at(ev->to().domain().length()); + if (!c) + ok = true; + else if (c == '#') { + String hash(node->substr(ev->to().domain().length() + 1)); + if (sType == JBStream::c2s) + ok = hash == m_c2sFeatures.m_entityCapsHash; + else + ok = hash == m_features.m_entityCapsHash; + } + else + ok = true; + } + if (ok) { + if (sType == JBStream::c2s) + return m_c2sFeatures.buildDiscoInfo(0,0,ev->id()); + return m_features.buildDiscoInfo(0,0,ev->id()); + } + error = XMPPError::ItemNotFound; + } + else + error = XMPPError::ServiceUnavailable; + return ev->buildIqError(false,error); +} + +// Handle disco items requests addressed to the server +XmlElement* YJBEngine::discoItems(JBEvent* ev) +{ + XMPPError::Type error = XMPPError::NoError; + if (ev->stanzaType() == "get" && + XMPPUtils::isUnprefTag(*ev->child(),XmlTag::Query)) { + const char* node = ev->child()->attribute("node"); + if (!node) { + XmlElement* query = XMPPUtils::createElement(XmlTag::Query, + XMPPNamespace::DiscoItems); + lock(); + for (ObjList* o = m_items.skipNull(); o; o = o->skipNext()) { + String* s = static_cast(o->get()); + XmlElement* item = new XmlElement("item"); + item->setAttribute("jid",*s); + query->addChild(item); + } + unlock(); + return ev->buildIqResult(false,query); + } + else + error = XMPPError::ItemNotFound; + } + else + error = XMPPError::ServiceUnavailable; + return ev->buildIqError(false,error); +} + +// Send an XML element to list of client streams. +// The given pointers will be consumed and zeroed +// Return true if the element was succesfully sent on at least one stream +bool YJBEngine::sendStanza(XmlElement*& xml, ObjList*& streams) +{ + DDebug(this,DebugAll,"sendStanza(%p,%p)",xml,streams); + bool ok = false; + if (streams && xml) { + ObjList* o = streams->skipNull(); + while (o) { + JBClientStream* stream = static_cast(o->get()); + o = o->skipNext(); + // Last stream in the list: send the xml (release it) + // Otherwise: send a copy of the element + if (!o) + ok = stream->sendStanza(xml) || ok; + else { + XmlElement* tmp = new XmlElement(*xml); + ok = stream->sendStanza(tmp) || ok; + } + } + } + TelEngine::destruct(streams); + TelEngine::destruct(xml); + return ok; +} + +// Find a server stream used to send stanzas from local domain to remote +// Build a new one if not found +JBStream* YJBEngine::getServerStream(const JabberID& from, const JabberID& to) +{ + // Avoid streams to internal components + if (m_items.find(to.domain()) || !hasDomain(from.domain())) + return 0; + JBServerStream* s = findServerStream(from.domain(),to.domain(),true); + if (s) + return s; + return createServerStream(from.domain(),to.domain()); +} + +// Notify online/offline presence from client streams +void YJBEngine::notifyPresence(JBClientStream& cs, bool online, XmlElement* xml, + const String& capsId) +{ + Message* m = __plugin.message("resource.notify"); + m->addParam("operation",online ? "online" : "offline"); + m->addParam("contact",cs.remote().bare()); + m->addParam("instance",cs.remote().resource()); + if (online) { + if (xml) + m->addParam("priority",String(XMPPUtils::priority(*xml))); + if (capsId) + s_entityCaps.addCaps(*m,capsId); + } + addXmlParam(*m,xml); + Engine::enqueue(m); +} + +// Notify directed online/offline presence +void YJBEngine::notifyPresence(const JabberID& from, const JabberID& to, bool online, + XmlElement* xml, bool fromRemote, bool toRemote, const String& capsId) +{ + Message* m = __plugin.message("resource.notify"); + m->addParam("operation",online ? "online" : "offline"); + m->addParam("from",from.bare()); + addValidParam(*m,"from_instance",from.resource()); + if (fromRemote) + m->addParam("from_local",String::boolText(false)); + m->addParam("to",to.bare()); + addValidParam(*m,"to_instance",to.resource()); + if (toRemote) + m->addParam("to_local",String::boolText(false)); + if (online) { + if (xml) + m->addParam("priority",String(XMPPUtils::priority(*xml))); + if (capsId) + s_entityCaps.addCaps(*m,capsId); + } + addXmlParam(*m,xml); + Engine::enqueue(m); +} + +// Build a jabber.feature message +Message* YJBEngine::jabberFeature(XmlElement* xml, XMPPNamespace::Type t, JBStream::Type sType, + const char* from, const char* to, const char* operation) +{ + Message* m = __plugin.message("jabber.feature"); + m->addParam("feature",XMPPUtils::s_ns[t]); + addValidParam(*m,"operation",operation); + m->addParam("stream_type",lookup(sType,JBStream::s_typeName)); + m->addParam("from",from); + addValidParam(*m,"to",to); + if (xml) + m->addParam(new NamedPointer("xml",xml)); + return m; +} + +// Build a xmpp.iq message +Message* YJBEngine::xmppIq(JBEvent* ev, const char* xmlns) +{ + Message* m = __plugin.message("xmpp.iq"); + m->addParam(new NamedPointer("xml",ev->releaseXml())); + addValidParam(*m,"to",ev->to()); + addValidParam(*m,"from",ev->from()); + addValidParam(*m,"id",ev->id()); + addValidParam(*m,"type",ev->stanzaType()); + addValidParam(*m,"xmlns",xmlns); +} + +// Build an user.(un)register message +Message* YJBEngine::userRegister(JBStream& stream, bool reg, const char* instance) +{ + Message* m = __plugin.message(reg ? "user.register" : "user.unregister"); + if (stream.type() == JBStream::c2s) + m->addParam("username",stream.remote().bare()); + else + m->addParam("server",String::boolText(true)); + JabberID data(stream.remote().node(),stream.remote().domain(),instance); + m->addParam("data",data); + if (reg) { + SocketAddr addr; + if (stream.remoteAddr(addr)) { + m->addParam("ip_host",addr.host()); + m->addParam("ip_port",String(addr.port())); + } + } + return m; +} + +// Fill module status params +void YJBEngine::statusParams(String& str) +{ + lock(); + unsigned int c2s = m_c2sReceive ? m_c2sReceive->streamCount() : 0; + unsigned int s2s = m_s2sReceive ? m_s2sReceive->streamCount() : 0; + unlock(); + str << lookup(JBStream::c2s,JBStream::s_typeName) << "=" << c2s; + str << "," << lookup(JBStream::s2s,JBStream::s_typeName) << "=" << s2s; +} + +// Fill module status detail +unsigned int YJBEngine::statusDetail(String& str, JBStream::Type t, JabberID* remote) +{ + XDebug(this,DebugAll,"statusDetail('%s','%s')", + lookup(t,JBStream::s_typeName),TelEngine::c_safe(remote)); + RefPointer list[JBStream::TypeCount]; + getStreamLists(list,t); + str << "format=Direction|Type|Status|Local|Remote"; + unsigned int n = 0; + for (unsigned int i = 0; i < JBStream::TypeCount; i++) { + if (!list[i]) + continue; + list[i]->lock(); + for (ObjList* o = list[i]->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + JBStream* stream = static_cast(s->get()); + Lock lock(stream); + if (!remote || (i == JBStream::c2s && stream->remote().match(*remote)) || + (i == JBStream::s2s && stream->remote() == *remote)) { + n++; + streamDetail(str,stream); + } + } + } + list[i]->unlock(); + list[i] = 0; + } + return n; +} + +// Complete stream details +void YJBEngine::statusDetail(String& str, const String& name) +{ + XDebug(this,DebugAll,"statusDetail(%s)",name.c_str()); + JBStream* stream = findStream(name); + if (!stream) + return; + Lock lock(stream); + str.append("name=",";"); + str << stream->toString(); + str << ",direction=" << (stream->incoming() ? "incoming" : "outgoing"); + str << ",type=" << stream->typeName(); + str << ",state=" << stream->stateName(); + str << ",local=" << stream->local(); + str << ",remote=" << stream->remote(); + String buf; + XMPPUtils::buildFlags(buf,stream->flags(),JBStream::s_flagName); + str << ",options=" << buf; +} + +// Complete stream detail +void YJBEngine::streamDetail(String& str, JBStream* stream) +{ + str << ";" << stream->toString() << "="; + str << (stream->incoming() ? "incoming" : "outgoing"); + str << "|" << stream->typeName(); + str << "|" << stream->stateName(); + str << "|" << stream->local(); + str << "|" << stream->remote(); +} + +// Complete remote party jid starting with partWord +void YJBEngine::completeStreamRemote(String& str, const String& partWord, JBStream::Type t) +{ + lock(); + RefPointer list = 0; + if (t == JBStream::c2s) + list = m_c2sReceive; + else if (t == JBStream::s2s) + list = m_s2sReceive; + unlock(); + if (!list) + return; + list->lock(); + for (ObjList* o = list->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + JBStream* stream = static_cast(s->get()); + Lock lock(stream); + Module::itemComplete(str,stream->remote(),partWord); + } + } + list->unlock(); + list = 0; +} + +// Complete stream id starting with partWord +void YJBEngine::completeStreamName(String& str, const String& partWord) +{ + lock(); + RefPointer list[2] = {m_c2sReceive,m_s2sReceive}; + unlock(); + for (unsigned int i = 0; i < 2; i++) { + if (!list[i]) + continue; + list[i]->lock(); + for (ObjList* o = list[i]->sets().skipNull(); o; o = o->skipNext()) { + JBStreamSet* set = static_cast(o->get()); + for (ObjList* s = set->clients().skipNull(); s; s = s->skipNext()) { + JBStream* stream = static_cast(s->get()); + Lock lock(stream); + if (!partWord || stream->toString().startsWith(partWord)) + Module::itemComplete(str,stream->toString(),partWord); + } + } + list[i]->unlock(); + list[i] = 0; + } +} + +// Notify an incoming s2s stream about a dialback verify response +void YJBEngine::notifyDbVerifyResult(const JabberID& local, const JabberID& remote, + const String& id, bool ok) +{ + if (!id) + return; + // Notify the incoming stream + JBServerStream* notify = findServerStream(local,remote,false); + if (notify && notify->id() == id) + notify->sendDbResult(local,remote,ok); + else + Debug(this,DebugNote, + "No incoming s2s stream local=%s remote=%s id='%s' to notify dialback verify result", + local.c_str(),remote.c_str(),id.c_str()); + TelEngine::destruct(notify); +} + + +/* + * JBPendingJob + */ +JBPendingJob::JBPendingJob(JBEvent* ev) + : m_event(ev), + m_stream(ev->stream()->toString()), + m_streamType((JBStream::Type)ev->stream()->type()), + m_local(ev->stream()->local().domain()), + m_flags(ev->stream()->flags()), + m_serverTarget(!ev->to() || ev->to() == ev->stream()->local()) +{ + m_event->releaseStream(true); +} + +// Retrieve the stream from jabber engine +JBStream* JBPendingJob::getStream() +{ + // FIXME: Don't use the stream id when finding a s2s stream + // We can use any stream to a remote domain to send stanzas + if (m_streamType != JBStream::s2s) + return s_jabber->findStream(m_stream,m_streamType); + return s_jabber->getServerStream(m_event->to(),m_event->from()); +} + +// Retrieve the stream from jabber engine +// Send the given stanza +// The pointer will be consumed and zeroed +void JBPendingJob::sendStanza(XmlElement*& xml, bool regular) +{ + if (!xml) + return; + JBStream* stream = getStream(); + XDebug(&__plugin,DebugAll, + "JBPendingJob event=%s from=%s to=%s sending '%s' stream (%p,%s)", + m_event->name(),m_event->from().c_str(),m_event->to().c_str(),xml->tag(), + stream,stream ? stream->toString().c_str() : ""); + if (stream) { + xml->setAttributeValid("from",m_event->to()); + if (stream->type() != JBStream::c2s) + xml->setAttributeValid("to",m_event->from()); + if (regular) + stream->sendStanza(xml); + else { + stream->sendStreamXml(stream->state(),xml); + xml = 0; + } + } + TelEngine::destruct(xml); + TelEngine::destruct(stream); +} + + +/* + * JBPendingWorker + */ +JBPendingWorker::JBPendingWorker(unsigned int index, Thread::Priority prio) + : Thread("JBPendingWorker",prio), + Mutex(true,__plugin.name() + ":JBPendingWorker"), + m_index((unsigned int)-1) +{ + // NOTE: Don't lock non-reentrant global mutex: the thread is created with this mutex locked + if (index < s_threadCount) { + m_index = index; + s_threads[index] = this; + } +} + +void JBPendingWorker::cleanup() +{ + if (resetIndex()) + Debug(&__plugin,DebugWarn,"JBPendingWorker(%u) abnormally terminated! [%p]", + m_index,this); +} + +void JBPendingWorker::run() +{ + Debug(&__plugin,DebugAll,"JBPendingWorker(%u) start running [%p]",m_index,this); + bool processed = false; + while (true) { + if (processed) + Thread::msleep(2,false); + else + Thread::idle(false); + if (Thread::check(false)) + break; + lock(); + JBPendingJob* job = static_cast(m_jobs.remove(false)); + unlock(); + processed = job && job->m_event && job->m_event->element(); + if (processed) { + switch (XMPPUtils::tag(*job->m_event->element())) { + case XmlTag::Message: + processChat(*job); + break; + case XmlTag::Iq: + processIq(*job); + break; + default: + Debug(&__plugin,DebugStub, + "JBPendingWorker unhandled xml tag '%s' [%p]", + job->m_event->element()->tag(),this); + } + } + TelEngine::destruct(job); + } + resetIndex(); + Debug(&__plugin,DebugAll,"JBPendingWorker(%u) terminated [%p]",m_index,this); +} + +// Initialize (start) the worker threads +void JBPendingWorker::initialize(unsigned int threads, Thread::Priority prio) +{ + Lock lock(s_mutex); + if (s_threads) + return; + s_threads = new JBPendingWorker*[threads]; + s_threadCount = threads; + DDebug(&__plugin,DebugAll,"JBPendingWorker::initialize(%u,%u)",threads,prio); + for (unsigned int i = 0; i < s_threadCount; i++) { + s_threads[i] = 0; + (new JBPendingWorker(i,prio))->startup(); + } +} + +// Cancel worker threads. Wait for them to terminate +void JBPendingWorker::stop() +{ + if (!s_threads) + return; + s_mutex.lock(); + unsigned int threads = 0; + for (unsigned int i = 0; i < s_threadCount; i++) { + if (s_threads[i]) { + threads++; + s_threads[i]->cancel(); + } + } + s_mutex.unlock(); + if (!threads) { + delete[] s_threads; + s_threads = 0; + return; + } + DDebug(&__plugin,DebugAll,"Waiting for %u pending worker threads to terminate",threads); + bool haveThreads = false; + while (true) { + haveThreads = false; + s_mutex.lock(); + for (unsigned int i = 0; i < s_threadCount; i++) + if (s_threads[i]) { + haveThreads = true; + break; + } + s_mutex.unlock(); + if (!haveThreads) + break; + Thread::yield(); + } + Debug(&__plugin,DebugAll,"Terminated %u pending worker threads",threads); + Lock lock(s_mutex); + delete[] s_threads; + s_threads = 0; +} + +// Add a job to one of the threads +bool JBPendingWorker::add(JBEvent* ev) +{ + if (!(ev && ev->element() && ev->stream())) + return false; + if (Engine::exiting()) { + ev->sendStanzaError(XMPPError::Shutdown,0,XMPPError::TypeCancel); + return false; + } + if (!ev->ref()) { + ev->sendStanzaError(XMPPError::Internal); + return false; + } + // TODO: avoid locking the global mutex (the thread's job list may be long) + // Add a busy flag used to protect the thread list and protected by the global mutex + Lock lock(s_mutex); + String id(ev->from()); + if (ev->stream()->type() == JBStream::s2s) + id << ev->to(); + unsigned int index = id.hash() % s_threadCount; + JBPendingWorker* th = s_threads[index]; + if (th) { + Lock lock(th); + // Don't move the debug after the append(): event will loose its xml element + XDebug(&__plugin,DebugAll,"JBPendingWorker(%u) added job xml=%s from=%s to=%s [%p]", + th->m_index,ev->element()->tag(),ev->from().c_str(),ev->to().c_str(),th); + th->m_jobs.append(new JBPendingJob(ev)); + return true; + } + lock.drop(); + ev->sendStanzaError(XMPPError::Internal); + TelEngine::destruct(ev); + return false; +} + +// Process chat jobs +void JBPendingWorker::processChat(JBPendingJob& job) +{ + JBEvent* ev = job.m_event; + XDebug(&__plugin,DebugAll,"JBPendingWorker(%u) processChat xml=%p from=%s to=%s [%p]", + m_index,ev->element(),ev->from().c_str(),ev->to().c_str(),this); + if (!ev->to()) { + job.sendChatErrorStanza(XMPPError::ServiceUnavailable); + return; + } + XMPPError::Type error = XMPPError::NoError; + bool localTarget = s_jabber->hasDomain(ev->to().domain()); + Message m("msg.route"); + while (true) { + __plugin.complete(m); + m.addParam("type",ev->stanzaType()); + m.addParam("caller",ev->from().bare()); + addValidParam(m,"called",ev->to().bare()); + addValidParam(m,"caller_instance",ev->from().resource()); + addValidParam(m,"called_instance",ev->to().resource()); + if (localTarget) { + bool ok = Engine::dispatch(m); + if (!ok || (m.retValue() == "-") || (m.retValue() == "error")) { + // Check if an 'instance.count' parameter was returned: + // the target has the source in its roster + if (m.getParam("instance.count")) + error = XMPPError::ItemNotFound; + else + error = XMPPError::ServiceUnavailable; + break; + } + } + // Check route(s) + m = "msg.execute"; +// m.setParam("callto",m.retValue()); + m.clearParam("error"); + m.retValue().clear(); + XmlElement* xml = ev->releaseXml(); + addValidParam(m,"subject",XMPPUtils::subject(*xml)); + addValidParam(m,"body",XMPPUtils::body(*xml)); + m.addParam(new NamedPointer("xml",xml)); + if (!Engine::dispatch(m)) + error = XMPPError::Gone; + break; + } + if (error == XMPPError::NoError) + return; + if (localTarget && error == XMPPError::ItemNotFound) { + // Store offline messages addressed to our users + bool ok = false; + XmlElement* xml = ev->releaseXml(); + if (!xml) + xml = XMPPUtils::getChatXml(m); + if (xml) { + Message* f = s_jabber->jabberFeature(xml,XMPPNamespace::MsgOffline, + job.m_streamType,ev->from(),ev->to()); + f->addParam("time",String(m.msgTime().sec())); + ok = Engine::dispatch(f); + TelEngine::destruct(f); + } + if (ok) + return; + error = XMPPError::ServiceUnavailable; + } + job.sendChatErrorStanza(error); +} + +// Process iq jobs +void JBPendingWorker::processIq(JBPendingJob& job) +{ + JBEvent* ev = job.m_event; + XmlElement* service = ev->child(); + XMPPUtils::IqType t = XMPPUtils::iqType(ev->stanzaType()); + String* xmlns = 0; + int ns = XMPPNamespace::Count; + if (service) { + xmlns = service->xmlns(); + if (xmlns) + ns = XMPPUtils::s_ns[*xmlns]; + } + XDebug(&__plugin,DebugAll, + "JBPendingWorker(%u) processing iq type=%s from=%s to=%s child=%s xmlns=%s stream=%s [%p]", + m_index,ev->stanzaType().c_str(),ev->from().c_str(),ev->to().c_str(), + service ? service->tag() : "",TelEngine::c_safe(xmlns), + lookup(job.m_streamType,JBStream::s_typeName),this); + + // Server entity caps responses + if (ns == XMPPNamespace::DiscoInfo && + (t == XMPPUtils::IqResult || t == XMPPUtils::IqError) && + s_entityCaps.processRsp(ev->element(),ev->id(),t == XMPPUtils::IqResult)) + return; + + XmlElement* rsp = 0; + // Handle here some stanzas addressed to the server + if (job.m_serverTarget) { + // Responses + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) { + // TODO: ? + return; + } + switch (ns) { + // XEP-0030 Service Discovery + case XMPPNamespace::DiscoInfo: + rsp = s_jabber->discoInfo(ev,job.m_streamType); + break; + // Disco items + case XMPPNamespace::DiscoItems: + rsp = s_jabber->discoItems(ev); + break; + // XEP-0092 Software version + case XMPPNamespace::IqVersion: + if (t == XMPPUtils::IqGet && + service->toString() == XMPPUtils::s_tag[XmlTag::Query]) + rsp = XMPPUtils::createIqVersionRes(0,0,ev->id(), + s_jabber->m_progName,s_jabber->m_progVersion); + else + rsp = ev->buildIqError(false,XMPPError::ServiceUnavailable); + break; + // RFC 3921 - session establishment + // Deprecated in RFC 3921 bis + case XMPPNamespace::Session: + if (job.m_streamType == JBStream::c2s && t == XMPPUtils::IqSet && + service->toString() == XMPPUtils::s_tag[XmlTag::Session]) + rsp = ev->buildIqResult(false); + else + rsp = ev->buildIqError(false,XMPPError::ServiceUnavailable); + break; + default: ; + } + } + // Respond ? + if (rsp) { + job.sendStanza(rsp); + return; + } + // Check some other known namespaces + switch (ns) { + // RFC 3921 Roster management + // Namespace restricted for non c2s streams + case XMPPNamespace::Roster: + rsp = s_jabber->processIqRoster(ev,job.m_streamType,t); + if (rsp) + job.sendStanza(rsp); + // Set roster requested flag + if (job.m_streamType == JBStream::c2s && t == XMPPUtils::IqGet) { + JBStream* stream = job.getStream(); + if (stream) { + stream->setRosterRequested(true); + TelEngine::destruct(stream); + } + } + return; + // XEP-0054 vcard-temp + case XMPPNamespace::VCard: + // vcard requests from remote domain + if (job.m_streamType != JBStream::c2s) + break; + // vcard requests to remote domain + if (ev->to() && !s_jabber->hasDomain(ev->to().domain())) + break; + rsp = s_jabber->processIqVCard(ev,job.m_streamType,t); + if (rsp) + job.sendStanza(rsp); + return; + // XEP-0049 Private XML storage + // Namespace restricted for non c2s streams + case XMPPNamespace::IqPrivate: + rsp = s_jabber->processIqPrivate(ev,job.m_streamType,t); + if (rsp) + job.sendStanza(rsp); + return; + // XEP-0199 XMPP Ping + // See Section 4.2 Client to Server ping + // See Section 4.3 Server to Server ping + case XMPPNamespace::Ping: + if (job.m_serverTarget || (job.m_streamType == JBStream::c2s && + ev->to().bare() == ev->from().bare())) { + if (t == XMPPUtils::IqGet && + service->toString() == XMPPUtils::s_tag[XmlTag::Ping]) + job.sendIqResultStanza(); + else + job.sendIqErrorStanza(XMPPError::ServiceUnavailable); + return; + } + break; + // XEP-0077 In-Band Registration + // Namespace restricted for non c2s streams + case XMPPNamespace::IqRegister: + if (job.m_serverTarget) { + rsp = s_jabber->processIqRegister(ev,job.m_streamType,t, + job.m_local,job.m_flags); + job.sendStanza(rsp,false); + } + else + job.sendIqErrorStanza(XMPPError::ServiceUnavailable); + return; + default: ; + } + + bool respond = (t == XMPPUtils::IqGet || t == XMPPUtils::IqSet); + // Destination at local domain: deny the request if the sender is not + // the target's roster + if (s_jabber->hasDomain(ev->to().domain())) { + // Check auth + Message auth("resource.subscribe"); + auth.addParam("module",__plugin.name()); + auth.addParam("operation","query"); + auth.addParam("subscriber",ev->from().bare()); + auth.addParam("notifier",ev->to().bare()); + if (!Engine::dispatch(auth)) { + if (respond) + job.sendIqErrorStanza(XMPPError::ServiceUnavailable); + return; + } + } + // Route the iq + Message m("jabber.iq"); + m.addParam("module",__plugin.name()); + m.addParam("from",ev->from().bare()); + m.addParam("from_instance",ev->from().resource()); + m.addParam("to",ev->to().bare()); + m.addParam("to_instance",ev->to().resource()); + addValidParam(m,"id",ev->id()); + addValidParam(m,"type",ev->stanzaType()); + addValidParam(m,"xmlns",TelEngine::c_safe(xmlns)); + m.addParam(new NamedPointer("xml",ev->releaseXml())); + if (Engine::dispatch(m)) { + if (respond) { + XmlElement* xml = XMPPUtils::getXml(m,"response",0); + if (xml) + job.sendStanza(xml); + else if (m.getBoolValue("respond")) + job.sendIqResultStanza(); + } + return; + } + if (respond) + job.sendIqErrorStanza(XMPPError::ServiceUnavailable); +} + +// Reset the global index +bool JBPendingWorker::resetIndex() +{ + Lock lock(s_mutex); + DDebug(&__plugin,DebugAll,"JBPendingWorker(%u) resetting global list entry [%p]", + m_index,this); + if (s_threads && m_index < s_threadCount && s_threads[m_index]) { + s_threads[m_index] = 0; + return true; + } + return false; +} + + +/* + * UserAuthMessage + */ +// Fill the message with authentication data +UserAuthMessage::UserAuthMessage(JBEvent* ev) + : Message("user.auth"), + m_stream(ev->stream()->toString()), m_streamType((JBStream::Type)ev->stream()->type()) +{ + __plugin.complete(*this); + addParam("streamtype",ev->stream()->typeName()); + ev->stream()->lock(); + if (ev->stream()->m_sasl && ev->stream()->m_sasl->m_params) { + copyParams(*(ev->stream()->m_sasl->m_params)); + const char* username = ev->stream()->m_sasl->m_params->getValue("username"); + String user; + if (username) + user << username << "@"; + user << ev->stream()->local().domain(); + setParam("username",user); + } + ev->stream()->unlock(); + SocketAddr addr; + if (ev->stream()->remoteAddr(addr)) { + addParam("ip_host",addr.host()); + addParam("ip_port",String(addr.port())); + } +} + +// Check accepted and returned value. Calls stream's authenticated() method +void UserAuthMessage::dispatched(bool accepted) +{ + JBStream* stream = s_jabber->findStream(m_stream,m_streamType); + bool ok = false; + String rspValue; + // Use a while() to break to the end + while (stream) { + Lock lock(stream); + if (!stream->m_sasl) + break; + // Returned value '-' means deny + if (accepted && retValue() == "-") + break; + // Empty password returned means authentication succeeded + if (!(accepted || retValue())) + break; + // Returned password works only with username + if (!getValue("username")) + break; + // Check credentials + String* rsp = getParam("response"); + if (rsp) { + if (stream->m_sasl->m_plain) + ok = (*rsp == retValue()); + else { + String digest; + stream->m_sasl->buildMD5Digest(digest,retValue(),true); + ok = (*rsp == digest); + if (ok) + stream->m_sasl->buildMD5Digest(rspValue,retValue(),false); + } + } + break; + } + if (stream) + stream->authenticated(ok,rspValue); + TelEngine::destruct(stream); +} + + +/* + * JBMessageHandler + */ +JBMessageHandler::JBMessageHandler(int handler) + : MessageHandler(lookup(handler,s_msgHandler),handler < 0 ? 100 : handler), + m_handler(handler) +{ +} + +bool JBMessageHandler::received(Message& msg) +{ + switch (m_handler) { + case JabberIq: + return s_jabber->handleJabberIq(msg); + case ResNotify: + return s_jabber->handleResNotify(msg); + case ResSubscribe: + return s_jabber->handleResSubscribe(msg); + case UserRoster: + if (!__plugin.isModule(msg)) + s_jabber->handleUserRoster(msg); + return false; + case UserUpdate: + if (!__plugin.isModule(msg)) + s_jabber->handleUserUpdate(msg); + return false; + case JabberItem: + return s_jabber->handleJabberItem(msg); + default: + Debug(&__plugin,DebugStub,"JBMessageHandler(%s) not handled!",msg.c_str()); + } + return false; +} + + +/* + * TcpListener + */ +// Remove from plugin +TcpListener::~TcpListener() +{ + if (m_socket.valid() && !Engine::exiting()) + Debug(&__plugin,DebugWarn,"Listener(%s) '%s:%d' abnormally terminated [%p]", + c_str(),m_address.safe(),m_port,this); + terminateSocket(); + __plugin.listener(this,false); +} + +// Bind and listen +void TcpListener::run() +{ + __plugin.listener(this,true); + DDebug(&__plugin,DebugAll,"TcpListener(%s) '%s:%d' type='%s' start running [%p]", + c_str(),m_address.safe(),m_port,lookup(m_type,JBStream::s_typeName),this); + // Create the socket + if (!m_socket.create(PF_INET,SOCK_STREAM)) { + terminateSocket("failed to create socket"); + return; + } + m_socket.setReuse(); + // Bind the socket + SocketAddr addr(PF_INET); + addr.host(m_address); + addr.port(m_port); + if (!m_socket.bind(addr)) { + terminateSocket("failed to bind"); + return; + } + m_socket.setBlocking(false); + // Start listening + if (!m_socket.listen(m_backlog)) { + terminateSocket("failed to start listening"); + return; + } + XDebug(&__plugin,DebugAll,"Listener(%s) '%s:%d' start listening [%p]", + c_str(),m_address.safe(),m_port,this); + while (true) { + if (Thread::check(false)) + break; + SocketAddr addr(PF_INET); + Socket* sock = m_socket.accept(addr); + bool processed = false; + if (sock) { + DDebug(&__plugin,DebugAll,"Listener(%s) '%s:%d' got conn from '%s:%d' [%p]", + c_str(),m_address.safe(),m_port,addr.host().c_str(),addr.port(),this); + processed = m_engine && m_engine->acceptConn(sock,addr,m_type); + if (!processed) + delete sock; + } + Thread::idle(); +#if 0 + if (processed) { + if (m_sleepMs) + Thread::msleep(m_sleepMs,false); + } + else if (m_sleepMsNone) + Thread::msleep(m_sleepMsNone,false); + else + Thread::yield(false); +#endif + } + terminateSocket(); + DDebug(&__plugin,DebugAll,"Listener(%s) '%s:%d' terminated [%p]",c_str(), + m_address.safe(),m_port,this); + __plugin.listener(this,false); +} + +// Terminate the socket. Show an error if context is not null +void TcpListener::terminateSocket(const char* context) +{ + if (context) { + String s; + Thread::errorString(s,m_socket.error()); + Debug(&__plugin,DebugWarn,"Listener(%s) '%s:%d' %s. %d: '%s' [%p]", + c_str(),m_address.safe(),m_port,context,m_socket.error(),s.c_str(),this); + } + m_socket.setLinger(-1); + m_socket.terminate(); +} + + +/* + * JBModule + */ +// Early load, late unload: we own the jabber engine +JBModule::JBModule() + : Module("jabber","misc",true), + m_init(false) +{ + Output("Loaded module Jabber Server"); +} + +JBModule::~JBModule() +{ + Output("Unloading module Jabber Server"); + TelEngine::destruct(s_jabber); +} + +void JBModule::initialize() +{ + Output("Initializing module Jabber Server"); + Configuration cfg(Engine::configFile("jabberserver")); + + if (!m_init) { + m_init = true; + setup(); + installRelay(Halt); + installRelay(Help); + installRelay(ImExecute); + s_jabber = new YJBEngine; + s_jabber->debugChain(this); + // Install handlers + for (const TokenDict* d = s_msgHandler; d->token; d++) { + JBMessageHandler* h = new JBMessageHandler(d->value); + Engine::install(h); + m_handlers.append(h); + } + // Start pending job workers + // TODO: add config for worker count and thread priority + JBPendingWorker::initialize(3); + + // Load entity caps file + s_entityCaps.m_enable = cfg.getBoolValue("general","entitycaps",true); + if (s_entityCaps.m_enable) + s_entityCaps.load(); + else + Debug(this,DebugAll,"Entity capability is disabled"); + } + + // Init the engine + s_jabber->initialize(cfg.getSection("general"),!m_init); + + // Listeners + unsigned int n = cfg.length(); + for (unsigned int i = 0; i < n; i++) { + NamedList* p = cfg.getSection(i); + if (!p) + continue; + String name = *p; + name.trimBlanks(); + if (!(name.startSkip("listener ",false) && name)) + continue; + if (p->getBoolValue("enable")) + buildListener(name,*p); + else + cancelListener(name); + } +} + +// Cancel a given listener or all listeners if name is empty +void JBModule::cancelListener(const String& name) +{ + Lock lck(this); + if (!name) { + ObjList* o = m_streamListeners.skipNull(); + if (!o) + return; + Debug(this,DebugInfo,"Cancelling %d listener(s)",m_streamListeners.count()); + for (; o; o = o->skipNext()) { + TcpListener* tmp = static_cast(o->get()); + tmp->cancel(false); + } + } + else { + ObjList* o = m_streamListeners.find(name); + if (!o) + return; + Debug(this,DebugInfo,"Cancelling listener='%s'",name.c_str()); + (static_cast(o->get()))->cancel(false); + } + lck.drop(); + while (true) { + ObjList* tmp = 0; + lock(); + if (!name) + tmp = m_streamListeners.skipNull(); + else + tmp = m_streamListeners.find(name); + unlock(); + if (!tmp) + break; + Thread::yield(true); + } + if (!name) + Debug(this,DebugInfo,"All listeners terminated"); + else + Debug(this,DebugInfo,"Listener '%s' terminated",name.c_str()); +} + +// Message handler +bool JBModule::received(Message& msg, int id) +{ + if (id == ImExecute) + return s_jabber->handleMsgExecute(msg); + if (id == Status) { + String target = msg.getValue("module"); + // Target is the module + if (!target || target == name()) + return Module::received(msg,id); + // Check additional commands + if (!target.startSkip(name(),false)) + return false; + target.trimBlanks(); + if (!target) + return Module::received(msg,id); + // Handle: status jabber {stream_name|{c2s|s2s} [remote_jid]} + String tmp; + if (!getWord(target,tmp)) + return false; + JBStream::Type t = JBStream::lookupType(tmp); + if (t == JBStream::TypeCount) { + statusModule(msg.retValue()); + s_jabber->statusDetail(msg.retValue(),tmp); + msg.retValue() << "\r\n"; + return true; + } + JabberID jid; + if (target) { + if (!getWord(target,tmp)) + return false; + jid.set(tmp); + if (!jid.valid()) + return false; + } + String buf; + unsigned int n = s_jabber->statusDetail(buf,t,jid ? &jid : 0); + statusModule(msg.retValue()); + msg.retValue() << ";count=" << n; + if (n) + msg.retValue() << ";" << buf.c_str(); + msg.retValue() << "\r\n"; + return true; + } + if (id == Help) { + String line = msg.getValue("line"); + if (line.null()) { + msg.retValue() << s_cmdStatus << "\r\n"; + msg.retValue() << s_cmdDropAll << "\r\n"; + msg.retValue() << s_cmdCreate << "\r\n"; + msg.retValue() << s_cmdDebug << "\r\n"; + return false; + } + if (line != name()) + return false; + msg.retValue() << s_cmdStatus << "\r\n"; + msg.retValue() << "Show stream status by type and remote jid or stream name\r\n"; + msg.retValue() << s_cmdDropStreamName << "\r\n"; + msg.retValue() << "Terminate a stream by its name\r\n"; + msg.retValue() << s_cmdDropStream << "\r\n"; + msg.retValue() << "Terminate all streams. Optionally terminate only streams of given type and jid\r\n"; + msg.retValue() << s_cmdCreate << "\r\n"; + msg.retValue() << "Create a server to server stream to a remote domain.\r\n"; + msg.retValue() << s_cmdDebug << "\r\n"; + msg.retValue() << "Show or set the debug level for a stream.\r\n"; + return true; + } + if (id == Halt) { + s_jabber->setExiting(); + // Stop pending job workers + JBPendingWorker::stop(); + // Uninstall message handlers + for (ObjList* o = m_handlers.skipNull(); o; o = o->skipNext()) { + JBMessageHandler* h = static_cast(o->get()); + Engine::uninstall(h); + } + cancelListener(); + s_jabber->cleanup(); + DDebug(this,DebugAll,"Halted"); + return Module::received(msg,id); + } + if (id == Timer) + s_entityCaps.expire(msg.msgTime().msec()); + return Module::received(msg,id); +} + +// Fill module status params +void JBModule::statusParams(String& str) +{ + s_jabber->statusParams(str); +} + +// Fill module status detail +void JBModule::statusDetail(String& str) +{ + s_jabber->statusDetail(str); +} + +// Handle command complete requests +bool JBModule::commandComplete(Message& msg, const String& partLine, + const String& partWord) +{ + if (partLine.null() && partWord.null()) + return false; + XDebug(this,DebugAll,"commandComplete() partLine='%s' partWord=%s", + partLine.c_str(),partWord.c_str()); + + // No line or 'help': complete module name + if (partLine.null() || partLine == "help") + return Module::itemComplete(msg.retValue(),name(),partWord); + // Line is module name: complete module commands + if (partLine == name()) { + for (const String* list = s_cmds; list->length(); list++) + Module::itemComplete(msg.retValue(),*list,partWord); + return true; + } + + String line = partLine; + String word; + getWord(line,word); + if (word == name()) { + // Line is module name: complete module commands and parameters + getWord(line,word); + // Check for a known command + for (const String* list = s_cmds; list->length(); list++) { + if (*list != word) + continue; + if (*list == "drop") { + // Handle: jabber drop {stream_name|{c2s|s2s|*|all} [remote_jid]} + getWord(line,word); + if (line) + return true; + JBStream::Type t = JBStream::lookupType(word); + if (t != JBStream::TypeCount || word == "all" || word == "*") + s_jabber->completeStreamRemote(msg.retValue(),partWord,t); + else { + completeStreamType(msg.retValue(),partWord,true); + s_jabber->completeStreamName(msg.retValue(),partWord); + } + } + if (*list == "debug") { + // Handle: jabber debug stream_name [debug_level] + if (line) + return true; + s_jabber->completeStreamName(msg.retValue(),partWord); + } + return true; + } + // Complete module commands + for (const String* list = s_cmds; list->length(); list++) + Module::itemComplete(msg.retValue(),*list,partWord); + return true; + } + if (word == "status") { + // Handle: status jabber [stream_name|{c2s|s2s} [remote_jid]]"; + getWord(line,word); + if (word != name()) + return Module::commandComplete(msg,partLine,partWord); + getWord(line,word); + if (word) { + if (line) + return false; + JBStream::Type t = JBStream::lookupType(word); + if (t != JBStream::TypeCount) + s_jabber->completeStreamRemote(msg.retValue(),partWord,t); + else { + completeStreamType(msg.retValue(),partWord); + s_jabber->completeStreamName(msg.retValue(),partWord); + } + } + else { + // Complete stream type/name + completeStreamType(msg.retValue(),partWord); + s_jabber->completeStreamName(msg.retValue(),partWord); + } + return true; + } + return Module::commandComplete(msg,partLine,partWord); +} + +// Handle command request +bool JBModule::commandExecute(String& retVal, const String& line) +{ + String l = line; + String word; + getWord(l,word); + if (word != name()) + return false; + getWord(l,word); + DDebug(this,DebugAll,"Executing command '%s' params '%s'",word.c_str(),l.c_str()); + if (word == "drop") { + Debug(this,DebugAll,"Executing '%s' command line=%s",word.c_str(),line.c_str()); + getWord(l,word); + JBStream::Type t = JBStream::lookupType(word); + if (t != JBStream::TypeCount || word == "all" || word == "*") { + // Handle: jabber drop {c2s|s2s|*|all} [remote_jid]" + JabberID remote(l); + unsigned int n = 0; + if (remote.valid()) + n = s_jabber->dropAll(t,JabberID::empty(),remote); + retVal << "Dropped " << n << " stream(s)"; + } + else { + // Handle: jabber drop stream_name + String n(word); + n.append(l," "); + JBStream* stream = s_jabber->findStream(word); + if (stream) { + stream->terminate(-1,true,0,XMPPError::NoError); + TelEngine::destruct(stream); + retVal << "Dropped stream '" << n << "'"; + } + else + retVal << "Stream '" << n << "' not found"; + } + } + else if (word == "create") { + // Handle s2s stream start + String remote; + getWord(l,remote); + String local; + getWord(l,local); + bool hasLocal = true; + if (!local) + s_jabber->firstDomain(local); + else + hasLocal = s_jabber->hasDomain(local); + bool hasRemote = s_jabber->hasDomain(remote); + Debug(this,DebugAll,"Executing '%s' command local=%s remote=%s", + word.c_str(),local.c_str(),remote.c_str()); + if (remote && !hasRemote && local && hasLocal) { + JBStream* s = s_jabber->getServerStream(local.c_str(),remote.c_str()); + retVal << (s ? "Success" : "Failure"); + TelEngine::destruct(s); + } + else if (!remote || hasRemote) + retVal << "Invalid remote domain"; + else + retVal << "Invalid local domain"; + } + else if (word == "debug") { + Debug(this,DebugAll,"Executing '%s' command line=%s",word.c_str(),line.c_str()); + getWord(l,word); + JBStream* stream = s_jabber->findStream(word); + if (stream) { + retVal << "Stream '" << word << "' debug"; + if (l) { + int level = l.toInteger(-1); + if (level >= 0) { + stream->debugLevel(level); + retVal << " at level " << stream->debugLevel(); + } + else if (l.isBoolean()) { + stream->debugEnabled(l.toBoolean()); + retVal << " is " << (stream->debugEnabled() ? "on" : "off"); + } + } + else + retVal << " at level " << stream->debugLevel(); + TelEngine::destruct(stream); + } + else + retVal << "Stream '" << word << "' not found"; + } + else + return false; + retVal << "\r\n"; + return true; +} + +// Build a listener from a list of parameters. Add it to the list and start it +bool JBModule::buildListener(const String& name, NamedList& p) +{ + if (!name) + return false; + Lock lock(this); + if (m_streamListeners.find(name)) + return true; + lock.drop(); + const char* stype = p.getValue("type"); + JBStream::Type t = JBStream::lookupType(stype); + if (t == JBStream::TypeCount) { + Debug(this,DebugWarn,"Can't build listener='%s' with invalid type='%s'", + name.c_str(),stype); + return false; + } + String* sport = p.getParam("port"); + int port = 0; + if (!TelEngine::null(sport)) + port = sport->toInteger(); + else if (t == JBStream::c2s) + port = XMPP_C2S_PORT; + else if (t == JBStream::s2s) + port = XMPP_S2S_PORT; + if (!port) { + Debug(this,DebugWarn,"Can't build listener='%s' with invalid port='%s'", + name.c_str(),c_safe(sport)); + return false; + } + TcpListener* l = new TcpListener(name,s_jabber,t, + p.getValue("address"),port,p.getIntValue("backlog",5)); + if (l->startup()) + return true; + Debug(this,DebugWarn,"Failed to start listener='%s' type='%s' addr='%s' port=%d", + name.c_str(),stype,p.getValue("address"),port); + TelEngine::destruct(l); + return false; +} + +// Add or remove a listener to/from list +void JBModule::listener(TcpListener* l, bool add) +{ + if (!l) + return; + Lock lock(this); + ObjList* found = m_streamListeners.find(l); + if (add == (found != 0)) + return; + if (add) + m_streamListeners.append(l)->setDelete(false); + else + found->remove(false); + Debug(this,DebugAll,"%s listener (%p,'%s')",add ? "Added" : "Removed", + l,l->toString().c_str()); +} + +}; // anonymous namespace + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/jabber/jbfeatures.cpp b/modules/jabber/jbfeatures.cpp new file mode 100644 index 00000000..08bd4398 --- /dev/null +++ b/modules/jabber/jbfeatures.cpp @@ -0,0 +1,821 @@ +/** + * .cpp + * This file is part of the YATE Project http://YATE.null.ro + * + * + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include +#include + + +// TODO: +// implement roster group and name max length when setting it from protocol + +using namespace TelEngine; + +namespace { // anonymous + +class JBFeaturesModule; // The module + + +/* + * The module + */ +class JBFeaturesModule : public Module +{ +public: + enum PrivateRelay { + JabberFeature = Private, + UserUpdate = Private << 1, + }; + JBFeaturesModule(); + virtual ~JBFeaturesModule(); + virtual void initialize(); + // Check if a message was sent by us + inline bool isModule(const Message& msg) const { + String* module = msg.getParam("module"); + return module && *module == name(); + } + // Handle 'jabber.feature' roster management + // RFC 3921 + bool handleFeatureRoster(JabberID& from, Message& msg); + // Handle 'jabber.feature' private data get/set + // XEP-0049 Private XML storage + bool handleFeaturePrivateData(JabberID& from, Message& msg); + // Handle 'jabber.feature' vcard get/set + // XEP-0054 vcard-temp + bool handleFeatureVCard(JabberID& from, Message& msg); + // Handle 'jabber.feature' offline message get/add + bool handleFeatureMsgOffline(JabberID& from, Message& msg); + // Handle 'jabber.feature' in-band register get/set + // XEP-0077 In-Band Registration + bool handleFeatureRegister(JabberID& from, Message& msg); +protected: + virtual bool received(Message& msg, int id); + // Build and dispatch or enqueue a 'database' message + // Return 0 when the message is enqueued or dispatch failure + Message* queryDb(const NamedList& params, const String& account, + const String& query, bool sync = true); +private: + bool m_init; // Module already initialized + String m_defAccount; // Default database account + String m_vcardAccount; // Database vcard account + String m_vcardQueryGet; // vcard 'get' query + String m_vcardQuerySet; // vcard 'set' item query + String m_vcardQueryDel; // vcard 'delete' item query + String m_dataAccount; // Database private data account + String m_dataQueryGet; // Private data 'get' query + String m_dataQuerySet; // Private data 'set' query + String m_dataQueryDel; // Private data 'delete' query + // Offline messages + unsigned int m_maxChatCount; // Maximum number of chat messages to store + unsigned int m_nextCheck; // The next time (in seconds) to run the expire query + unsigned int m_expire; // Chat expiring interval (in seconds) + String m_chatAccount; // Database offline messages account + String m_chatQueryExpire; // Offline messages expire query + String m_chatQueryGet; // Offline messages 'get' query + String m_chatQueryAdd; // Offline messages 'add' query + String m_chatQueryDel; // Offline messages 'delete' query + // In-band user register (XEP-0077) + bool m_regEnable; // Enable user (un)register + bool m_regChange; // Enable user changes (such as password) + bool m_regAllowUnsecure; // Allow user registration support on unsecured streams + String m_regUrl; // URL to send to the user when creation is disabled + String m_regInfo; // Instructions to send along with the url +}; + + +/* + * Local data + */ +INIT_PLUGIN(JBFeaturesModule); // The module + +// Return a safe pointer to config section +static inline const NamedList* getSection(Configuration& cfg, const char* name) +{ + NamedList* sect = cfg.getSection(name); + if (sect) + return sect; + return &NamedList::empty(); +} + +// Add a 'subscription' and, optionally, an 'ask' attribute to a roster item +static inline void addSubscription(XmlElement& dest, const String& sub) +{ + XMPPDirVal d(sub); + if (d.test(XMPPDirVal::PendingOut)) + dest.setAttribute("ask","subscribe"); + String tmp; + d.toSubscription(tmp); + dest.setAttribute("subscription",tmp); +} + +// Build a roster item XML element from message parameters +static XmlElement* buildRosterItem(NamedList& list, unsigned int index) +{ + String prefix("contact."); + prefix << index; + const char* contact = list.getValue(prefix); + XDebug(&__plugin,DebugAll,"buildRosterItem(%s,%u) contact=%s", + list.c_str(),index,contact); + if (TelEngine::null(contact)) + return 0; + XmlElement* item = new XmlElement("item"); + item->setAttribute("jid",contact); + prefix << "."; + ObjList* groups = 0; + unsigned int n = list.length(); + for (unsigned int i = 0; i < n; i++) { + NamedString* param = list.getParam(i); + if (!(param && param->name().startsWith(prefix))) + continue; + String name = param->name(); + name.startSkip(prefix,false); + if (name == "name") + item->setAttribute("name",*param); + else if (name == "subscription") + addSubscription(*item,*param); + else if (name == "groups") { + if (!groups) + groups = param->split(',',false); + } + else + item->addChild(XMPPUtils::createElement(name,*param)); + } + if (!item->getAttribute("subscription")) + addSubscription(*item,String::empty()); + for (ObjList* o = groups ? groups->skipNull() : 0; o; o = o->skipNext()) { + String* grp = static_cast(o->get()); + item->addChild(XMPPUtils::createElement("group",*grp)); + } + TelEngine::destruct(groups); + return item; +} + +// Build a result and set it to a message parameter +// Release the given xml pointer and zero it +// Return true +static bool buildResult(Message& msg, XmlElement*& xml, XmlElement* child = 0) +{ + const char* id = xml ? xml->attribute("id") : 0; + XmlElement* rsp = XMPPUtils::createIqResult(0,0,id); + TelEngine::destruct(xml); + if (child) + rsp->addChild(child); + msg.setParam(new NamedPointer("response",rsp)); + return true; +} + +// Build an error and set it to a message parameter +// Release the given xml pointer and zero it +// Return false +static bool buildError(Message& msg, XmlElement*& xml, + XMPPError::Type error = XMPPError::ServiceUnavailable, + XMPPError::ErrorType type = XMPPError::TypeModify) +{ + const char* id = xml ? xml->attribute("id") : 0; + XmlElement* rsp = XMPPUtils::createIq(XMPPUtils::IqError,0,0,id); + if (TelEngine::null(id) && xml) { + rsp->addChild(xml); + xml = 0; + } + else + TelEngine::destruct(xml); + rsp->addChild(XMPPUtils::createError(type,error)); + msg.setParam(new NamedPointer("response",rsp)); + return false; +} + +// Add xml data to a list +static inline void addXmlData(NamedList& list, XmlElement* xml, const char* param = "xml") +{ + String buf; + if (xml) + xml->toString(buf); + list.addParam(param,buf); +} + + +/* + * JBFeaturesModule + */ +// Early load, late unload +JBFeaturesModule::JBFeaturesModule() + : Module("jbfeatures","misc",true), + m_init(false), + m_maxChatCount(0), m_nextCheck(0), m_expire(0), + m_regEnable(true), m_regChange(true), m_regAllowUnsecure(false) +{ + Output("Loaded module Jabber Server Features"); +} + +JBFeaturesModule::~JBFeaturesModule() +{ + Output("Unloading module Jabber Server Features"); +} + +void JBFeaturesModule::initialize() +{ + Output("Initializing module Jabber Server Features"); + + Configuration cfg(Engine::configFile("jbfeatures")); + NamedList dummy(""); + + const NamedList* reg = getSection(cfg,"register"); + m_regEnable = reg->getBoolValue("allow_management",true); + m_regChange = reg->getBoolValue("allow_change",true); + m_regAllowUnsecure = reg->getBoolValue("allow_unsecure",false); + m_regUrl = reg->getValue("url"); + m_regInfo = reg->getValue("intructions"); + // TODO: Notify feature XMPPNamespace::Register to the jabber server + const NamedList* offlinechat = getSection(cfg,"offline_chat"); + int tmp = offlinechat->getIntValue("maxcount"); + m_maxChatCount = tmp > 0 ? tmp : 0; + tmp = offlinechat->getIntValue("expires"); + if (tmp < 0) + tmp = 0; + else if (tmp && tmp < 30) + tmp = 30; + m_expire = tmp * 60; + if (m_expire) { + if (!m_nextCheck) + m_nextCheck = Time::secNow(); + } + else + m_nextCheck = 0; + + if (m_init) + return; + + m_init = true; + const NamedList* general = getSection(cfg,"general"); + m_defAccount = general->getValue("account"); + const NamedList* vcard = getSection(cfg,"vcard"); + m_vcardAccount = vcard->getValue("account"); + m_vcardQueryGet = vcard->getValue("get"); + m_vcardQuerySet = vcard->getValue("set"); + m_vcardQueryDel = vcard->getValue("clear_user"); + const NamedList* pdata = getSection(cfg,"private_data"); + m_dataAccount = pdata->getValue("account"); + m_dataQueryGet = pdata->getValue("get"); + m_dataQuerySet = pdata->getValue("set"); + m_dataQueryDel = pdata->getValue("clear_user"); + m_chatAccount = offlinechat->getValue("account"); + m_chatQueryExpire = offlinechat->getValue("expire_query"); + m_chatQueryGet = offlinechat->getValue("get"); + m_chatQueryAdd = offlinechat->getValue("add"); + m_chatQueryDel = offlinechat->getValue("clear_user"); + setup(); + installRelay(Halt); + installRelay(JabberFeature,"jabber.feature"); + installRelay(UserUpdate,"user.update"); +} + +// Handle 'jabber.feature' roster management +// RFC 3921 +bool JBFeaturesModule::handleFeatureRoster(JabberID& from, Message& msg) +{ + XmlElement* xml = XMPPUtils::getXml(msg); + DDebug(this,DebugAll,"handleFeatureRoster() from=%s xml=%p",from.c_str(),xml); + if (!xml) + return false; + // Ignore responses + XMPPUtils::IqType t = XMPPUtils::iqType(xml->attribute("type")); + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) { + TelEngine::destruct(xml); + return false; + } + // The client must add it's resource in request + if (!from.resource()) + return buildError(msg,xml); + // The request must be carried by a 'query' tag + XmlElement* child = xml->findFirstChild(); + if (!(child && XMPPUtils::isUnprefTag(*child,XmlTag::Query))) + return buildError(msg,xml); + JabberID contact; + bool get = (t == XMPPUtils::IqGet); + bool set = !get; + if (!get) { + // Set/remove contact: check jid + // Don't allow user to operate on itself + XmlElement* item = XMPPUtils::findFirstChild(*child,XmlTag::Item,XMPPNamespace::Roster); + if (item) { + contact = item->getAttribute("jid"); + String* sub = item->getAttribute("subscription"); + set = !sub || *sub != "remove"; + } + if (!contact) + return buildError(msg,xml,XMPPError::BadRequest); + else if (contact.bare() == from.bare()) + return buildError(msg,xml,XMPPError::NotAllowed); + } + Message m("user.roster"); + m.addParam("module",name()); + m.addParam("operation",set ? "update" : (get ? "query": "delete")); + m.addParam("username",from.bare()); + if (!get) { + m.addParam("contact",contact.bare()); + if (set) { + // We already found the item + XmlElement* item = XMPPUtils::findFirstChild(*child,XmlTag::Item,XMPPNamespace::Roster); + NamedString* params = new NamedString("contact.parameters","name,groups"); + m.addParam(params); + m.addParam("name",item->attribute("name")); + NamedString* groups = new NamedString("groups"); + m.addParam(groups); + // Groups and other children + const String* ns = &XMPPUtils::s_ns[XMPPNamespace::Roster]; + for (XmlElement* c = item->findFirstChild(0,ns); c; c = item->findNextChild(c,0,ns)) { + if (XMPPUtils::isUnprefTag(*c,XmlTag::Group)) + groups->append(c->getText(),","); + else { + params->append(c->tag(),","); + m.addParam(c->tag(),c->getText()); + } + } + } + } + if (Engine::dispatch(m)) { + DDebug(this,DebugAll,"Roster '%s' accepted user='%s'", + m.getValue("operation"),m.getValue("username")); + XmlElement* child = 0; + if (get) { + unsigned int n = m.getIntValue("contact.count"); + child = XMPPUtils::createElement(XmlTag::Query,XMPPNamespace::Roster); + for (unsigned int i = 1; i <= n; i++) + child->addChild(buildRosterItem(m,i)); + } + return buildResult(msg,xml,child); + } + if (m.getParam("error")) + return buildError(msg,xml,XMPPError::ItemNotFound); + return buildError(msg,xml); +} + +// Handle 'jabber.feature' private data get/set +// XEP-0049 Private XML storage +bool JBFeaturesModule::handleFeaturePrivateData(JabberID& from, Message& msg) +{ + XmlElement* xml = XMPPUtils::getXml(msg); + DDebug(this,DebugAll,"handleFeaturePrivateData() from=%s xml=%p",from.c_str(),xml); + if (!xml) + return false; + // Ignore responses + XMPPUtils::IqType t = XMPPUtils::iqType(xml->attribute("type")); + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) { + TelEngine::destruct(xml); + return false; + } + // The request must be carried by a 'query' tag + XmlElement* child = xml->findFirstChild(); + if (!(child && XMPPUtils::isUnprefTag(*child,XmlTag::Query))) + return buildError(msg,xml); + + // XEP-0049 2.3: + // At least one child with a valid namespace must exist + // Iq 'set' may contain more then 1 child qualified by the same namespace + XmlElement* ch = child->findFirstChild(); + String* ns = ch ? ch->xmlns() : 0; + if (TelEngine::null(ns)) + return buildError(msg,xml,XMPPError::BadFormat); + + // TODO handle special jabber:iq:private requests: + // storage:imprefs (seen from Exodus) + // storage:bookmarks (XEP-0048 Bookmark storage) + // storage:metacontacts (seen from Gajim) + // storage:rosternotes (seen from Gajim) + + // Handle 'get' + if (t == XMPPUtils::IqGet) { + String tag(ch->tag()); + // We should have only 1 child + ch = child->findNextChild(ch); + if (ch) + return buildError(msg,xml,XMPPError::NotAcceptable); + NamedList p(""); + p.addParam("username",from.bare()); + p.addParam("tag",tag); + p.addParam("xmlns",*ns); + Message* m = queryDb(p,m_dataAccount,m_dataQueryGet); + XmlElement* query = XMPPUtils::createElement(XmlTag::Query, + XMPPNamespace::IqPrivate); + XmlElement* pdata = 0; + if (m) { + Array* a = static_cast(m->userObject("Array")); + String* data = a ? YOBJECT(String,a->get(0,1)) : 0; + pdata = data ? XMPPUtils::getXml(*data) : 0; + if (pdata) { + // Avoid sending inconsistent tag or namespace + if (tag != pdata->toString() || *ns != pdata->xmlns()) { + Debug(this,DebugMild, + "User %s got invalid private data tag/ns='%s'/'%s' instead of '%s'/'%s'", + from.bare().c_str(),pdata->tag(), + TelEngine::c_safe(pdata->xmlns()),tag.c_str(),ns->c_str()); + TelEngine::destruct(pdata); + } + } + else if (data) + Debug(this,DebugMild,"User %s got invalid xml private data",from.bare().c_str()); + TelEngine::destruct(m); + } + if (!pdata) + pdata = XMPPUtils::createElement(tag,*ns); + query->addChild(pdata); + return buildResult(msg,xml,query); + } + + // Handle 'set' + // All children must share the same namespace + for (; ch; ch = child->findNextChild(ch)) + if (*ns != ch->xmlns()) + return buildError(msg,xml,XMPPError::NotAcceptable); + // Update all data. Return error if at least one item fails or there is no data + for (ch = child->findFirstChild(); ch; ch = child->findNextChild(ch)) { + XDebug(this,DebugAll,"Setting private data for '%s' tag=%s xmlns=%s", + from.bare().c_str(),ch->tag(),ns->c_str()); + NamedList p(""); + p.addParam("username",from.bare()); + p.addParam("tag",ch->tag()); + p.addParam("xmlns",*ns); + addXmlData(p,ch); + Message* m = queryDb(p,m_dataAccount,m_dataQuerySet); + if (!m) + break; + TelEngine::destruct(m); + } + if (!ch) + return buildResult(msg,xml); + return buildError(msg,xml); +} + +// Handle 'jabber.feature' vcard get/set +// XEP-0054 vcard-temp +bool JBFeaturesModule::handleFeatureVCard(JabberID& from, Message& msg) +{ + JabberID to(msg.getValue("to")); + XmlElement* xml = XMPPUtils::getXml(msg); + DDebug(this,DebugAll,"handleFeatureVCard() from=%s to=%s xml=%p", + from.c_str(),to.c_str(),xml); + if (!xml) + return false; + // Ignore responses + XMPPUtils::IqType t = XMPPUtils::iqType(xml->attribute("type")); + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) { + TelEngine::destruct(xml); + return false; + } + NamedList p(""); + bool otherUser = (to && to.bare() != from.bare()); + if (otherUser) { + // Check auth + Message auth("resource.subscribe"); + auth.addParam("operation","query"); + auth.addParam("subscriber",from.bare()); + auth.addParam("notifier",to.bare()); + if (!Engine::dispatch(auth)) + return buildError(msg,xml); + p.addParam("username",to.bare()); + } + else + p.addParam("username",from.bare()); + Message* m = 0; + if (t == XMPPUtils::IqGet) + m = queryDb(p,m_vcardAccount,m_vcardQueryGet); + else { + XmlElement* vcard = xml->findFirstChild(); + addXmlData(p,vcard,"vcard"); + m = queryDb(p,m_vcardAccount,m_vcardQuerySet); + } + // Don't return error on failure if the user requested its vcard + if (!m && (otherUser || t == XMPPUtils::IqSet)) + return buildError(msg,xml); + XmlElement* vcard = 0; + if (t == XMPPUtils::IqGet && m) { + Array* a = static_cast(m->userObject("Array")); + if (a) { + String* vc = YOBJECT(String,a->get(0,1)); + XDebug(this,DebugInfo,"Got vcard for '%s': '%s'",p.getValue("username"), + TelEngine::c_safe(vc)); + if (!TelEngine::null(vc)) { + vcard = XMPPUtils::getXml(*vc); + if (vcard) { + // Avoid sending inconsistent tag + if (!XMPPUtils::isTag(*vcard,XmlTag::VCard,XMPPNamespace::VCard)) { + Debug(this,DebugMild,"Wrong vcard tag='%s' or ns='%s' for '%s'", + vcard->tag(),TelEngine::c_safe(vcard->xmlns()),p.getValue("username")); + TelEngine::destruct(vcard); + } + } + else + Debug(this,DebugMild,"Failed to parse vcard for '%s'",p.getValue("username")); + } + } + } + if (!vcard && t == XMPPUtils::IqGet) + vcard = XMPPUtils::createElement(XmlTag::VCard,XMPPNamespace::VCard); + TelEngine::destruct(m); + return buildResult(msg,xml,vcard); +} + +// Handle 'jabber.feature' offline message get/add +bool JBFeaturesModule::handleFeatureMsgOffline(JabberID& from, Message& msg) +{ + String* oper = msg.getParam("operation"); + DDebug(this,DebugAll,"handleFeatureMsgOffline() oper=%s",TelEngine::c_safe(oper)); + if (!oper || *oper == "add") { + // Store offline message + JabberID user(msg.getValue("to")); + if (!(user && user.valid())) + return false; + XmlElement* xml = XMPPUtils::getXml(msg); + if (!xml) + return false; + XMPPUtils::MsgType t = XMPPUtils::msgType(xml->attribute("type")); + const String& body = XMPPUtils::body(*xml); + bool ok = body && (t == XMPPUtils::Normal || t == XMPPUtils::Chat); + if (ok) { + xml->removeAttribute("to"); + NamedList p(""); + p.addParam("username",user.bare()); + addXmlData(p,xml); + const char* time = msg.getValue("time"); + if (!TelEngine::null(time)) + p.addParam("time",time); + else + p.addParam("time",String(msg.msgTime().sec())); + p.addParam("maxcount",String(m_maxChatCount)); + Message* m = queryDb(p,m_chatAccount,m_chatQueryAdd); + if (m) { + Array* a = static_cast(m->userObject("Array")); + String* res = a ? YOBJECT(String,a->get(0,1)) : 0; + if (res) { + DDebug(this,DebugAll,"Got result %s to add chat",res->c_str()); + ok = (res->toInteger() != 0); + } + else + ok = false; + } + else + ok = false; + TelEngine::destruct(m); + } + if (ok) + TelEngine::destruct(xml); + else + msg.setParam(new NamedPointer("xml",xml)); + return ok; + } + if (*oper == "query") { + // Retrieve offline messages + NamedList p(""); + p.addParam("username",from.bare()); + Message* m = queryDb(p,m_chatAccount,m_chatQueryGet); + if (!m) + return false; + Array* a = static_cast(m->userObject("Array")); + int rows = a ? a->getRows() : 0; + int cols = a ? a->getColumns() : 0; + DDebug(this,DebugAll,"Got %d offline messages for user =%s", + rows ? rows - 1 : 0,from.bare().c_str()); + for (int row = 1; row < rows; row++) { + String* data = 0; + String* time = 0; + for (int col = 0; col < cols; col++) { + String* s = YOBJECT(String,a->get(col,0)); + if (!s) + continue; + if (*s == "xml") + data = YOBJECT(String,a->get(col,row)); + else if (*s == "time") + time = YOBJECT(String,a->get(col,row)); + } + if (TelEngine::null(data)) + continue; + XmlElement* xml = XMPPUtils::getXml(*data); + if (xml) { + if (!TelEngine::null(time)) + xml->addChild(XMPPUtils::createDelay(time->toInteger())); + msg.addParam(new NamedPointer("xml",xml)); + continue; + } + Debug(this,DebugMild,"Invalid database offline chat xml for user=%s", + from.bare().c_str()); + } + return true; + } + if (*oper == "delete") { + // Remove user's offline messages + NamedList p(""); + p.addParam("username",from.bare()); + queryDb(p,m_chatAccount,m_chatQueryDel,false); + return true; + } + return false; +} + +// Handle 'jabber.feature' in-band register get/set +// XEP-0077 In-Band Registration +bool JBFeaturesModule::handleFeatureRegister(JabberID& from, Message& msg) +{ + XmlElement* xml = XMPPUtils::getXml(msg); + DDebug(this,DebugAll,"handleFeatureRegister() from=%s xml=%p",from.c_str(),xml); + if (!xml) + return false; + // Ignore responses + XMPPUtils::IqType t = XMPPUtils::iqType(xml->attribute("type")); + if (t != XMPPUtils::IqGet && t != XMPPUtils::IqSet) { + TelEngine::destruct(xml); + return false; + } + // Handle 'query' elements only + XmlElement* child = xml->findFirstChild(); + if (!(child && XMPPUtils::isUnprefTag(*child,XmlTag::Query))) + return buildError(msg,xml); + // Registration available only on secured streams + int flags = msg.getIntValue("stream_flags"); + if (!(m_regAllowUnsecure || 0 != (flags & JBStream::StreamTls))) + return buildError(msg,xml,XMPPError::EncryptionRequired); + // Set auth or remove the user + if (t == XMPPUtils::IqSet) { + const char* oper = 0; + bool remove = XMPPUtils::remove(*child); + JabberID user; + if (0 == (flags & JBStream::StreamAuthenticated)) { + if (!m_regEnable || remove) + return buildError(msg,xml); + XmlElement* tmp = XMPPUtils::findFirstChild(*child,XmlTag::Username, + XMPPNamespace::IqRegister); + const String& username = tmp ? tmp->getText() : String::empty(); + if (!username) + return buildError(msg,xml,XMPPError::BadRequest); + const char* domain = msg.getValue("stream_domain"); + if (TelEngine::null(domain)) + return buildError(msg,xml,XMPPError::BadRequest); + oper = "add"; + user.set(username,domain); + } + else { + if (!remove) { + if (m_regChange) + oper = "update"; + else + return buildError(msg,xml); + } + else if (m_regEnable) + oper = "delete"; + else + return buildError(msg,xml); + user.set(from.node(),from.domain()); + } + // Update the user + Message m("user.update"); + m.addParam("module",name()); + m.addParam("operation",oper); + m.addParam("user",user); + if (!remove) { + XmlElement* p = XMPPUtils::findFirstChild(*child,XmlTag::Password, + XMPPNamespace::IqRegister); + const String& pwd = p ? p->getText() : String::empty(); + if (!pwd) + return buildError(msg,xml,XMPPError::BadRequest); + m.addParam("password",pwd); + } + if (Engine::dispatch(m)) + return buildResult(msg,xml); + return buildError(msg,xml,XMPPError::NotAllowed); + } + + // Get auth + XmlElement* query = 0; + if (m_regEnable) { + query = XMPPUtils::createElement(XmlTag::Query, + XMPPNamespace::IqRegister); + if (0 == (flags & JBStream::StreamAuthenticated)) { + query->addChild(XMPPUtils::createElement(XmlTag::Username)); + query->addChild(XMPPUtils::createElement(XmlTag::Password)); + } + else + query->addChild(XMPPUtils::createElement(XmlTag::Registered)); + } + else if (m_regUrl) { + // XEP-0077 Section 5 Redirection + query = XMPPUtils::createElement(XmlTag::Query,XMPPNamespace::IqRegister); + if (m_regInfo) + query->addChild(XMPPUtils::createElement("instructions",m_regInfo)); + query->addChild(XMPPUtils::createXOobUrl(m_regUrl)); + } + if (query) + return buildResult(msg,xml,query); + return buildError(msg,xml); +} + +// Message handler +bool JBFeaturesModule::received(Message& msg, int id) +{ + if (id == JabberFeature) { + JabberID from(msg.getValue("from")); + switch (XMPPUtils::s_ns[msg.getValue("feature")]) { + case XMPPNamespace::VCard: + return from && handleFeatureVCard(from,msg); + case XMPPNamespace::Roster: + return from && handleFeatureRoster(from,msg); + case XMPPNamespace::IqPrivate: + return from && handleFeaturePrivateData(from,msg); + case XMPPNamespace::MsgOffline: + return from && handleFeatureMsgOffline(from,msg); + case XMPPNamespace::IqRegister: + return handleFeatureRegister(from,msg); + default: ; + } + return false; + } + if (id == UserUpdate) { + // Handle user deletion: remove vcard, private data, offline messages ... + String* notif = msg.getParam("notify"); + if (TelEngine::null(notif) || *notif != "delete") + return false; + String* user = msg.getParam("user"); + if (TelEngine::null(user)) + return false; + DDebug(this,DebugAll, + "User '%s' deleted: removing vcard, private data, offline messages", + user->c_str()); + NamedList p(""); + p.addParam("username",*user); + queryDb(p,m_vcardAccount,m_vcardQueryDel,false); + queryDb(p,m_dataAccount,m_dataQueryDel,false); + queryDb(p,m_chatAccount,m_chatQueryDel,false); + return false; + } + if (id == Timer) { + unsigned int sec = msg.msgTime().sec(); + if (m_nextCheck && m_nextCheck < sec) { + if (m_expire && m_chatQueryExpire) { + XDebug(this,DebugAll,"Running chat expire query"); + NamedList p(""); + p.addParam("time",String(sec)); + queryDb(p,m_chatAccount,m_chatQueryExpire,false); + m_nextCheck = sec + m_expire / 2; + } + else + m_nextCheck = 0; + } + } + else if (id == Halt) { + uninstallRelays(); + DDebug(this,DebugAll,"Halted"); + } + return Module::received(msg,id); +} + +// Build and dispatch or enqueue a 'database' message +Message* JBFeaturesModule::queryDb(const NamedList& params, const String& account, + const String& query, bool sync) +{ + if (!((account || m_defAccount) && query)) + return 0; + Message* m = new Message("database"); + m->addParam("account",account ? account : m_defAccount); + String tmp = query; + params.replaceParams(tmp,true); + m->addParam("query",tmp); + if (sync) { + if (Engine::dispatch(m)) { + String* error = m->getParam("error"); + if (error) { + DDebug(this,DebugNote,"'database' failed error='%s'",error->c_str()); + TelEngine::destruct(m); + } + } + else + TelEngine::destruct(m); + } + else { + m->addParam("results",String::boolText(false)); + Engine::enqueue(m); + m = 0; + } + return m; +} + +}; // anonymous namespace + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/jingle/jinglefeatures.cpp b/modules/jingle/jinglefeatures.cpp deleted file mode 100644 index 5e8d47da..00000000 --- a/modules/jingle/jinglefeatures.cpp +++ /dev/null @@ -1,833 +0,0 @@ -/** - * jinglefeatures.cpp - * This file is part of the YATE Project http://YATE.null.ro - * - * Additional XMPP features - * - * Yet Another Telephony Engine - a fully featured software PBX and IVR - * Copyright (C) 2004-2006 Null Team - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include - -using namespace TelEngine; -namespace { // anonymous - -/** - * NOTE: - * All responses sent to custom IQs set/get should carry the element - * with the custom application (namespace). This is nedded to match - * the application in the responses. Otherwise the module won't be - * able to detect responses to custom requests/notifications - */ - -class CustomXmppIqMsg; // A custom message built from an xmpp.iq -class YJingleFeatures; // The module - -// A custom message built from an xmpp.iq -// Send a response to received IQ after dispatched -class CustomXmppIqMsg : public Message -{ -public: - // Params: - // iq The received IQ - // child The first child of the received IQ - CustomXmppIqMsg(Message& msg, XMLElement& iq, XMLElement& child); -protected: - virtual void dispatched(bool accepted); -private: - String m_app; - String m_oper; -}; - -// Features module -class YJingleFeatures : public Module -{ -public: - enum PrivateRelay { - XmppIq = Private, - Custom = Private << 1, - UserInfo = Private << 2, - UserRoster = Private << 3, - }; - YJingleFeatures(); - virtual ~YJingleFeatures(); - // Check if a message is sent by this module - inline bool isModule(Message& msg) { - NamedString* m = msg.getParam("module"); - return m && *m == name(); - } - // Build a message. Add the 'module' param - // Copy 'line' and/or 'account' parameters - inline Message* buildMsg(const char* msg, Message* line = 0) { - Message* m = new Message(msg); - m->addParam("module",name()); - if (line) - m->copyParams(*line,"account,line"); - return m; - } - // Inherited methods - virtual bool received(Message& msg, int id); - virtual void initialize(); - // Message handlers - bool handleXmppIq(Message& msg); - bool handleCustom(Message& msg); - bool handleUserRoster(Message& msg); - bool handleUserInfo(Message& msg); - // Build and dispatch an "xmpp.generate" message. Copy error param on failure - // Consume the xml element - bool xmppGenerate(Message& recv, XMLElement* xml, bool rsp = false); - // Uninstall the relays - bool unload(); - // Check if a custom application is handled by the module - inline bool isApplication(const String* app) { - if (!m_apps || null(app)) - return false; - Lock lock(this); - return 0 != m_apps->find(*app); - } - // Build an XML element's children from a list - bool addChildren(NamedList& params, XMLElement& xml); - // Add an XML element's children to a list - // The element's name will be used as message-prefix - bool addChildren(XMLElement& xml, NamedList& params); - -private: - // Check module and target parameters of a received message - // Optionally check the target parameter (if present, it must be one - // of the jingle module aliases) - bool acceptMsg(Message& msg, bool checkTarget); - // Handle dynamic roster data - bool handleXmppIqDynamicRoster(Message& msg, XMLElement& query, XMPPUtils::IqType t, - const JabberID& from, const JabberID& to, const String& id); - // Handle client private iq data - bool handleXmppIqPrivate(Message& msg, XMLElement& query, XMPPUtils::IqType t, - const JabberID& from, const JabberID& to, const String& id); - // Handle a valid vcard element - bool handleXmppIqVCard(Message& msg, XMLElement& vcard, XMPPUtils::IqType t, - const JabberID& from, const JabberID& to, const String& id); - - ObjList* m_apps; // Custom applications -}; - - -INIT_PLUGIN(YJingleFeatures); -static const String s_jingleAlias[] = {"jingle", "xmpp", "jabber", ""}; -static bool s_handleVCard = true; -static bool s_handlePrivate = true; -static bool s_handleAddrbook = true; -// Strings used to compare or build other strings -static const String s_customPrefix = "custom."; -static const String s_group = "group"; - -static TokenDict s_customIqType[] = { - {"request", XMPPUtils::IqGet}, - {"update", XMPPUtils::IqSet}, - {"notify", XMPPUtils::IqResult}, - {"error", XMPPUtils::IqError}, - {0,0}, -}; - - -UNLOAD_PLUGIN(unloadNow) -{ - if (unloadNow && !__plugin.unload()) - return false; - return true; -} - - -// Check if a text is one of the jingle module's alias -static bool isJingleAlias(const String& name) -{ - for (int i = 0; s_jingleAlias[i].length(); i++) - if (name == s_jingleAlias[i]) - return true; - return false; -} - -// Check if the message source is the jingle module -static inline bool isJingleMsg(Message& msg) -{ - NamedString* module = msg.getParam("module"); - return module && isJingleAlias(*module); -} - -// Find an xml element's child text -static const char* getChildText(XMLElement& xml, const char* name, - XMLElement* start = 0) -{ - XMLElement* child = xml.findNextChild(start,name); - const char* text = child ? child->getText() : 0; - TelEngine::destruct(child); - return text; -} - -// Build an xml element error from error/reason parameters -static XMLElement* createXmlError(const Message& msg, const char* defaultText = 0) -{ - XMPPError::ErrorType eType = XMPPError::TypeModify; - XMPPError::Type err = XMPPError::UndefinedCondition; - String* reason = msg.getParam("reason"); - const char* error = msg.getValue("error",defaultText); - if (reason && *reason == "noauth") { - eType = XMPPError::TypeAuth; - err = XMPPError::NotAuthorized; - if (null(error)) - error = "Not authorized"; - } - return XMPPUtils::createError(eType,err,error); -} - - -/* - * CustomXmppIqMsg - */ -CustomXmppIqMsg::CustomXmppIqMsg(Message& msg, XMLElement& iq, XMLElement& child) - : Message("custom") -{ - addParam("module",__plugin.name()); - m_app = child.getAttribute("xmlns"); - m_oper = child.name(); - addParam("application",m_app); - addParam("operation",m_oper); - - // Check for stanza failure (the stream failed to send a required element) - bool failure = msg.getBoolValue("failure"); - if (failure) { - addParam("type","error"); - addParam("reason","noconn"); - addParam("error","Failed to send"); - copyParams(msg,"account,line,username,id"); - return; - } - - // Process received element - const char* type = msg.getValue("type"); - XMPPUtils::IqType iqType = XMPPUtils::iqType(type); - addParam("type",::lookup(iqType,s_customIqType,type)); - copyParams(msg,"account,line,username,from,to,id"); - bool needRsp = iqType == XMPPUtils::IqSet || iqType == XMPPUtils::IqGet; - addParam("need-response",String::boolText(needRsp)); - // Build params if not error - if (iqType != XMPPUtils::IqError) { - unsigned int n = 1; - XMLElement* ch = child.findFirstChild(); - for (; ch; ch = child.findNextChild(ch), n++) - ch->toList(*this,s_customPrefix + String(n)); - } - else { - String err, errText; - XMPPUtils::decodeError(&iq,err,errText); - if (errText) - addParam("error",errText); - else if (err) - addParam("error",err); - } -} - -void CustomXmppIqMsg::dispatched(bool accepted) -{ - if (!getBoolValue("need-response")) - return; - - XMPPUtils::IqType t = accepted ? XMPPUtils::IqResult : XMPPUtils::IqError; - XMLElement* iq = XMPPUtils::createIq(t,getValue("to"),getValue("from"),getValue("id")); - XMLElement* oper = new XMLElement(m_oper); - oper->setAttribute("xmlns",m_app); - // Check for result params - for (unsigned int n = 1; accepted; n++) { - String prefix; - prefix << "custom_out." << n; - if (TelEngine::null(getParam(prefix))) - break; - oper->addChild(new XMLElement(*this,prefix)); - } - iq->addChild(oper); - if (!accepted) - iq->addChild(createXmlError(*this,"Unhandled message")); - __plugin.xmppGenerate(*this,iq,true); -} - - -/* - * YJingleFeatures - */ -YJingleFeatures::YJingleFeatures() - : Module("jinglefeatures","misc"), - m_apps(0) -{ - Output("Loaded module Jingle Features"); - m_apps = new ObjList; -} - -YJingleFeatures::~YJingleFeatures() -{ - Output("Unloading module Jingle Features"); - TelEngine::destruct(m_apps); -} - -bool YJingleFeatures::received(Message& msg, int id) -{ - switch (id) { - case XmppIq: - return handleXmppIq(msg); - case Custom: - return handleCustom(msg); - case UserRoster: - return handleUserRoster(msg); - case UserInfo: - return handleUserInfo(msg); - case Halt: - unload(); - break; - } - return Module::received(msg,id); -} - -void YJingleFeatures::initialize() -{ - Output("Initializing module Jingle Features"); - - Configuration cfg(Engine::configFile("jinglefeatures")); - cfg.load(); - - NamedList dummy(""); - NamedList* general = cfg.getSection("general"); - if (!general) - general = &dummy; - String apps = general->getValue("custom_applications"); - TelEngine::destruct(m_apps); - m_apps = apps.split(',',false); - - NamedList* iq = cfg.getSection("iq"); - if (!iq) - iq = &dummy; - s_handleVCard = iq->getBoolValue("vcard",true); - s_handlePrivate = iq->getBoolValue("private",true); - s_handleAddrbook = iq->getBoolValue("addressbook",true); - - if (debugAt(DebugAll)) { - String s; - s << "vcard=" << String::boolText(s_handleVCard); - s << " private=" << String::boolText(s_handlePrivate); - s << " addressbook=" << String::boolText(s_handleAddrbook); - s << " custom_applications=" << apps; - Debug(this,DebugAll,"Initialized %s",s.c_str()); - } - - lock(); - - static bool s_first = true; - if (s_first) { - s_first = false; - setup(); - installRelay(XmppIq,"xmpp.iq",100); - installRelay(Custom,"custom",100); - } - - if (s_handleVCard || s_handlePrivate) - installRelay(UserInfo,"user.info",100); - else - uninstallRelay(UserInfo); - if (s_handleAddrbook) - installRelay(UserRoster,"user.roster",100); - else - uninstallRelay(UserRoster); - - unlock(); -} - -// xmpp.iq handler -bool YJingleFeatures::handleXmppIq(Message& msg) -{ - if (!isJingleMsg(msg)) - return false; - - // No XML: nothing to be done - XMLElement* xml = XMLElement::getXml(msg,false); - if (!xml) - return false; - - XMPPUtils::IqType t = XMPPUtils::iqType(msg.getValue("type")); - JabberID from = msg.getValue("from"); - JabberID to = msg.getValue("to"); - String id = msg.getValue("id"); - Debug(this,DebugAll,"Processing '%s' from=%s to=%s id=%s", - msg.c_str(),from.c_str(),to.c_str(),id.c_str()); - - XMLElement* child = xml->findFirstChild(); - - bool ok = false; - // Use a while to break to the end - while (child) { - String xmlns = child->getAttribute("xmlns"); - - // Query - if (child->type() == XMLElement::Query) { - XMPPNamespace::Type ns = XMPPNamespace::type(xmlns); - switch (ns) { - case XMPPNamespace::DynamicRoster: - ok = handleXmppIqDynamicRoster(msg,*child,t,from,to,id); - break; - case XMPPNamespace::IqPrivate: - ok = handleXmppIqPrivate(msg,*child,t,from,to,id); - break; - default: ; - } - break; - } - - // vCard - if (child->type() == XMLElement::VCard) { - if (XMPPUtils::hasXmlns(*child,XMPPNamespace::VCard)) - ok = handleXmppIqVCard(msg,*child,t,from,to,id); - break; - } - - // Custom - if (isApplication(&xmlns)) { - Engine::enqueue(new CustomXmppIqMsg(msg,*xml,*child)); - ok = true; - break; - } - - break; - } - - TelEngine::destruct(child); - return ok; -} - -// custom handler -bool YJingleFeatures::handleCustom(Message& msg) -{ - // Don't handle jingle(features) messages or messages with - // other target - if (!acceptMsg(msg,true) || isJingleMsg(msg)) - return false; - - // Check parameters - const char* oper = msg.getValue("operation"); - if (null(oper)) - return false; - String* xmlns = msg.getParam("application"); - if (!isApplication(xmlns)) - return false; - const char* type = msg.getValue("type"); - int iqType = ::lookup(type,s_customIqType,XMPPUtils::IqCount); - const char* error = 0; - if (iqType == XMPPUtils::IqError || iqType == XMPPUtils::IqCount) { - error = msg.getValue("error"); - if (iqType == XMPPUtils::IqCount && null(error)) { - Debug(this,DebugMild,"Custom message app=%s oper=%s with invalid type=%s", - xmlns->c_str(),oper,type); - return false; - } - } - - Debug(this,DebugAll,"Generating IQ from custom app=%s oper=%s type=%s", - xmlns->c_str(),oper,type); - XMLElement* iq = XMPPUtils::createIq((XMPPUtils::IqType)iqType,msg.getValue("from"), - msg.getValue("to"),0); - XMLElement* child = new XMLElement(oper); - child->setAttribute("xmlns",*xmlns); - // Build params - for (unsigned int n = 1; true; n++) { - String prefix; - prefix << s_customPrefix << n; - if (null(msg.getParam(prefix))) - break; - child->addChild(new XMLElement(msg,prefix)); - } - iq->addChild(child); - if (iqType == XMPPUtils::IqError) - iq->addChild(createXmlError(msg)); - - bool rsp = iqType == XMPPUtils::IqResult || iqType == XMPPUtils::IqError; - return xmppGenerate(msg,iq,rsp); -} - -// user.roster handler -bool YJingleFeatures::handleUserRoster(Message& msg) -{ - // Don't handle jingle(features) messages - if (!acceptMsg(msg,true) || isJingleMsg(msg)) - return false; - - NamedString* oper = msg.getParam("operation"); - if (!oper) - return false; - bool get = (*oper == "request"); - if (!get) { - if (*oper == "update") - get = false; - else - return false; - } - - bool dynamic = msg.getBoolValue("addressbook"); - if (dynamic && !s_handleAddrbook) - return false; - - Debug(this,DebugAll,"Processing '%s' operation=%s from=%s to=%s", - msg.c_str(),oper->c_str(),msg.getValue("from"),msg.getValue("to")); - - XMLElement* xml = XMPPUtils::createIq((get ? XMPPUtils::IqGet : XMPPUtils::IqSet), - msg.getValue("from"),msg.getValue("to"),msg.getValue("id")); - - XMPPNamespace::Type ns = XMPPNamespace::Roster; - if (dynamic) - ns = XMPPNamespace::DynamicRoster; - XMLElement* query = XMPPUtils::createElement(XMLElement::Query,ns); - // Fill items - unsigned int nParams = msg.length(); - String prefix = "contact."; - int n = msg.getIntValue("contact.count"); - for (int i = 1; i <= n; i++) { - String pref = prefix + String(i); - NamedString* jid = msg.getParam(pref); - if (!(jid && *jid)) - continue; - XMLElement* item = new XMLElement(XMLElement::Item); - item->setAttributeValid("jid",*jid); - - // Get data - if (get) { - query->addChild(item); - continue; - } - - // Set item attributes/children - // Dynamic: all params are children - // Roster: all params except for 'group' are attributes - pref << "."; - for (unsigned int j = 0; j < nParams; j++) { - NamedString* ns = msg.getParam(j); - if (!(ns && ns->name().startsWith(pref))) - continue; - String tmp = ns->name().substr(pref.length()); - if (!tmp) - continue; - if (dynamic || tmp == s_group) - item->addChild(new XMLElement(tmp,0,*ns)); - else - item->setAttributeValid(tmp,*ns); - } - query->addChild(item); - } - - xml->addChild(query); - return xmppGenerate(msg,xml); -} - -// user.info handler -bool YJingleFeatures::handleUserInfo(Message& msg) -{ - // Don't handle jingle(features) messages - if (!acceptMsg(msg,true) || isJingleMsg(msg)) - return false; - - NamedString* oper = msg.getParam("operation"); - if (!oper) - return false; - bool get = (*oper == "request"); - if (!get) { - if (*oper == "update") - get = false; - else - return false; - } - bool priv = msg.getBoolValue("private"); - - if (!s_handleVCard || (priv && !s_handlePrivate)) - return false; - Debug(this,DebugAll,"Processing '%s' operation=%s from=%s to=%s", - msg.c_str(),oper->c_str(),msg.getValue("from"),msg.getValue("to")); - - XMLElement* xml = 0; - if (!priv) { - xml = XMPPUtils::createVCard(get,msg.getValue("from"), - msg.getValue("to"),msg.getValue("id")); - XMLElement* vcard = !get ? xml->findFirstChild(XMLElement::VCard) : 0; - if (vcard) { - // Name - const char* first = msg.getValue("name.first"); - const char* middle = msg.getValue("name.middle"); - const char* last = msg.getValue("name.last"); - String firstN, lastN; - // Try to build elements if missing - if (!(first || last || middle)) { - String* tmp = msg.getParam("name"); - if (tmp) { - int pos = tmp->rfind(' '); - if (pos > 0) { - firstN = tmp->substr(0,pos); - lastN = tmp->substr(pos + 1); - } - else - lastN = *tmp; - } - first = firstN.c_str(); - last = lastN.c_str(); - } - XMLElement* n = new XMLElement("N"); - n->addChild(new XMLElement("GIVEN",0,first)); - n->addChild(new XMLElement("MIDDLE",0,middle)); - n->addChild(new XMLElement("FAMILY",0,last)); - vcard->addChild(n); - TelEngine::destruct(vcard); - } - } - else { - xml = XMPPUtils::createIq(get ? XMPPUtils::IqGet : XMPPUtils::IqSet, - msg.getValue("from"),msg.getValue("to"),msg.getValue("id")); - XMLElement* query = XMPPUtils::createElement(XMLElement::Query,XMPPNamespace::IqPrivate); - addChildren(msg,*query); - xml->addChild(query); - } - - return xmppGenerate(msg,xml); -} - -// Build and dispatch an "xmpp.generate" message. Copy error param on failure -// Consume the xml element -bool YJingleFeatures::xmppGenerate(Message& recv, XMLElement* xml, bool rsp) -{ - // Make sure we have an 'id' for debug purposes - if (xml && !rsp) { - const char* id = xml->getAttribute("id"); - if (!id) - xml->setAttribute("id",String((int)::random())); - } - Message* m = buildMsg("xmpp.generate",&recv); - m->addParam("protocol","xmpp"); - m->addParam(new NamedPointer("xml",xml,"")); - bool ok = Engine::dispatch(m); - if (!ok) - recv.copyParams(*m,"error"); - TelEngine::destruct(m); - return ok; -} - -// Unload the module: uninstall the relays -bool YJingleFeatures::unload() -{ - DDebug(this,DebugAll,"Cleanup"); - if (!lock(500000)) - return false; - uninstallRelays(); - unlock(); - return true; -} - -// Build an XML element's children from a list -bool YJingleFeatures::addChildren(NamedList& params, XMLElement& xml) -{ - String prefix = params.getValue("message-prefix"); - if (!prefix) - return false; - prefix << "."; - bool added = false; - for (unsigned int i = 1; i < 0xffffffff; i++) { - String childPrefix(prefix + String(i)); - if (!params.getValue(childPrefix)) - break; - xml.addChild(new XMLElement(params,childPrefix)); - added = true; - } - return added; -} - -// Add an XML element's children to a list -// The element's name will be used as message-prefix -bool YJingleFeatures::addChildren(XMLElement& xml, NamedList& params) -{ - String pref = xml.name(); - params.addParam("message-prefix",pref); - unsigned int n = 0; - pref << "."; - XMLElement* ch = xml.findFirstChild(); - bool added = ch != 0; - for (; ch; ch = xml.findNextChild(ch)) { - String tmpPref = pref; - tmpPref << ++n; - params.addParam(tmpPref,ch->name()); - tmpPref << "."; - const char* text = ch->getText(); - if (!null(text)) - params.addParam(tmpPref,text); - NamedList tmp(""); - ch->getAttributes(tmp); - unsigned int count = tmp.length(); - for (unsigned int i = 0; i < count; i++) { - NamedString* p = tmp.getParam(i); - if (p && p->name()) - params.addParam(tmpPref + p->name(),*p); - } - } - return added; -} - -// Check module and target parameters of a received message -bool YJingleFeatures::acceptMsg(Message& msg, bool checkTarget) -{ - NamedString* module = msg.getParam("module"); - if (module && *module == name()) - return false; - if (checkTarget) { - NamedString* target = msg.getParam("target"); - return !target || isJingleAlias(*target); - } - return true; -} - -// Handle DynamicRoster received with xmpp.iq -bool YJingleFeatures::handleXmppIqDynamicRoster(Message& msg, XMLElement& query, - XMPPUtils::IqType t, const JabberID& from, const JabberID& to, const String& id) -{ - if (!s_handleAddrbook || t != XMPPUtils::IqResult) - return false; - - Debug(this,DebugAll,"Processing '%s' [DynamicRoster] from=%s to=%s id=%s", - msg.c_str(),from.c_str(),to.c_str(),id.c_str()); - - Message* m = buildMsg("user.roster",&msg); - m->addParam("operation","notify"); - m->addParam("addressbook",String::boolText(true)); - m->copyParams(msg,"from,to,id"); - if (from.node()) - m->addParam("username",from.node()); - - unsigned int n = 0; - String prefix = "contact."; - XMLElement* item = query.findFirstChild(XMLElement::Item); - for (; item; item = query.findNextChild(item,XMLElement::Item)) { - const char* jid = item->getAttribute("jid"); - if (!(jid && *jid)) - continue; - n++; - String pref = prefix + String(n); - m->addParam(pref,jid); - pref << "."; - for (XMLElement* x = item->findFirstChild(); x; x = item->findNextChild(x)) - m->addParam(pref + x->name(),x->getText()); - } - m->addParam("contact.count",String(n)); - Engine::enqueue(m); - return true; -} - -// Handle client private iq data responses -bool YJingleFeatures::handleXmppIqPrivate(Message& msg, XMLElement& query, - XMPPUtils::IqType t, const JabberID& from, const JabberID& to, const String& id) -{ - if (!s_handlePrivate || t != XMPPUtils::IqResult) - return false; - - Debug(this,DebugAll,"Processing '%s' [Private] from=%s to=%s id=%s", - msg.c_str(),from.c_str(),to.c_str(),id.c_str()); - - - Message* m = buildMsg("user.info",&msg); - m->addParam("operation","notify"); - m->addParam("private",String::boolText(true)); - m->copyParams(msg,"from,to,id"); - if (from.node()) - m->addParam("username",from.node()); - XMLElement* ch = query.findFirstChild(); - if (ch) { - addChildren(*ch,*m); - TelEngine::destruct(ch); - } - Engine::enqueue(m); - return true; -} - -// Handle a valid vcard received with xmpp.iq -bool YJingleFeatures::handleXmppIqVCard(Message& msg, XMLElement& vcard, - XMPPUtils::IqType t, const JabberID& from, const JabberID& to, const String& id) -{ - if (!s_handleVCard || t != XMPPUtils::IqResult) - return false; - - Debug(this,DebugAll,"Processing '%s' [VCard] from=%s to=%s id=%s", - msg.c_str(),from.c_str(),to.c_str(),id.c_str()); - - Message* m = buildMsg("user.info",&msg); - m->addParam("operation","notify"); - m->addParam("vcard",String::boolText(true)); - m->copyParams(msg,"from,to,id"); - if (from.node()) - m->addParam("username",from.node()); - - XMLElement* n = vcard.findFirstChild("N"); - if (n) { - String name; - const char* given = getChildText(*n,"GIVEN"); - if (given) { - m->addParam("name.first",given); - name << given; - } - const char* middle = getChildText(*n,"MIDDLE"); - if (middle) { - m->addParam("name.middle",middle); - name.append(middle," "); - } - const char* family = getChildText(*n,"FAMILY"); - if (family) { - m->addParam("name.last",family); - name.append(family," "); - } - if (name) - m->addParam("name",name); - TelEngine::destruct(n); - } - // EMAIL - XMLElement* email = vcard.findFirstChild("EMAIL"); - if (email) { - const char* em = email->getText(); - if (!null(em)) - m->addParam("email",em); - TelEngine::destruct(email); - } - // Photo - XMLElement* photo = vcard.findFirstChild("PHOTO"); - if (photo) { - XMLElement* type = photo->findFirstChild("TYPE"); - XMLElement* binval = photo->findFirstChild("BINVAL"); - if (type && binval) { - const char* t = type->getText(); - const char* img = binval->getText(); - if (!(null(t) || null(img))) { - m->addParam("photo_format",t); - m->addParam("photo",img); - } - } - TelEngine::destruct(type); - TelEngine::destruct(binval); - TelEngine::destruct(photo); - } - - Engine::enqueue(m); - return true; -} - -}; // anonymous namespace - -/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/openssl.cpp b/modules/openssl.cpp index 5d06b42e..c15810c4 100644 --- a/modules/openssl.cpp +++ b/modules/openssl.cpp @@ -22,13 +22,14 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include +#include #include #include #include #include +#include #ifndef OPENSSL_NO_AES #include @@ -37,8 +38,6 @@ using namespace TelEngine; namespace { // anonymous -#define MODNAME "openssl" - #define MAKE_ERR(x) { #x, X509_V_ERR_##x } static TokenDict s_verifyCodes[] = { MAKE_ERR(UNABLE_TO_GET_ISSUER_CERT), @@ -75,11 +74,31 @@ static TokenDict s_verifyMode[] = { { 0, 0 } }; +class SslContext : public String +{ +public: + // Constructor. Build the context + SslContext(const char* name); + // Initialize certificate, key and domains. Check the key + // Return false on failure + bool init(const NamedList& params); + // Check if this context can be used for server sockets in a given domain + bool hasDomain(const String& domain); + // Add a comma separated list of domains to a buffer + void addDomains(String& buf); + // Release memory, free the context + virtual void destruct(); + inline operator SSL_CTX*() + { return m_context; } +protected: + SSL_CTX* m_context; + ObjList m_domains; +}; class SslSocket : public Socket, public Mutex { public: - SslSocket(SOCKET handle, bool server, int verify); + SslSocket(SOCKET handle, bool server, int verify, SslContext* context = 0); virtual ~SslSocket(); virtual bool terminate(); virtual bool valid(); @@ -132,20 +151,29 @@ public: }; #endif -class OpenSSL : public Plugin +class OpenSSL : public Module { public: OpenSSL(); ~OpenSSL(); virtual void initialize(); + // Find a context by name or domain + // This method is not thread safe. The caller must lock the plugin + // until the returned context is not used anymore + SslContext* findContext(const String& token, bool byDomain = false) const; protected: + virtual void statusParams(String& str); + virtual void statusDetail(String& str); + SslHandler* m_handler; + ObjList m_contexts; // Server contexts list + String m_statusCmd; // Module status command }; -INIT_PLUGIN(OpenSSL); static int s_index = -1; static SSL_CTX* s_context = 0; +static OpenSSL __plugin; // Attempt to add randomness from system time when called @@ -165,20 +193,142 @@ void infoCallback(const SSL* ssl, int where, int retVal) if (sock->ssl() == ssl) sock->onInfo(where,retVal); else - Debug(MODNAME,DebugFail,"Mismatched session %p [%p]",ssl,sock); + Debug(&__plugin,DebugFail,"Mismatched session %p [%p]",ssl,sock); } } +// Callback function called from OpenSSL for protocol messages +void msgCallback(int write, int version, int content_type, const void* buf, + size_t len, SSL* ssl, void* arg) +{ + Debug(&__plugin,DebugAll, + "%s SSL message: version=%d content_type=%d buf=%p len=%u ssl=%p", + write ? "Sent" : "Received",version,content_type,buf,(unsigned int)len,ssl); +} + + +SslContext::SslContext(const char* name) + : String(name), + m_context(0) +{ + m_context = ::SSL_CTX_new(::SSLv23_server_method()); + ::SSL_CTX_set_info_callback(m_context,infoCallback); +#ifdef DEBUG + ::SSL_CTX_set_msg_callback(m_context,msgCallback); +#endif +} + +// Initialize certificate, key and domains. Check the key +// Return false on failure +bool SslContext::init(const NamedList& params) +{ + String cert; + const char* c = params.getValue("certificate"); + if (c) { + cert << Engine::configPath(); + if (cert && !cert.endsWith(Engine::pathSeparator())) + cert << Engine::pathSeparator(); + cert << c; + } + String key; + const char* k = params.getValue("key"); + if (k) { + key << Engine::configPath(); + if (key && !key.endsWith(Engine::pathSeparator())) + key << Engine::pathSeparator(); + key << k; + } + else + key = cert; + // Load certificate and key. Check them + if (!::SSL_CTX_use_certificate_chain_file(m_context,cert)) { + unsigned long err = ::ERR_get_error(); + Debug(&__plugin,DebugWarn, + "Context '%s' failed to load certificate from '%s' '%s'", + c_str(),c ? cert.c_str() : "",::ERR_error_string(err,0)); + return false; + } + if (!::SSL_CTX_use_PrivateKey_file(m_context,key,SSL_FILETYPE_PEM)) { + unsigned long err = ::ERR_get_error(); + Debug(&__plugin,DebugWarn, + "Context '%s' failed to load key from '%s' '%s'", + c_str(),k ? key.c_str() : "",::ERR_error_string(err,0)); + return false; + } + if (!::SSL_CTX_check_private_key(m_context)) { + unsigned long err = ::ERR_get_error(); + Debug(&__plugin,DebugWarn, + "Context '%s' certificate='%s' or key='%s' are invalid '%s'", + c_str(),cert.c_str(),key.c_str(),::ERR_error_string(err,0)); + return false; + } + // Load domains + m_domains.clear(); + String* d = params.getParam("domains"); + if (d) { + ObjList* list = d->split(',',false); + for (ObjList* o = list->skipNull(); o; o = o->skipNext()) { + String* s = static_cast(o->get()); + s->trimBlanks(); + if (s->null()) + continue; + if (s->startsWith("*") && (s->length() < 3 || (*s)[1] != '.')) { + Debug(&__plugin,DebugNote,"Context '%s' ignoring invalid domain='%s'", + c_str(),s->c_str()); + continue; + } + m_domains.append(new String(s->toLower())); + } + TelEngine::destruct(list); + } + DDebug(&__plugin,DebugAll,"Context '%s' loaded certificate='%s' key='%s' domains=%s", + c_str(),cert.c_str(),key.c_str(),TelEngine::c_safe(d)); + return true; +} + +// Check if this context can be used for server sockets in a given domain +bool SslContext::hasDomain(const String& domain) +{ + for (ObjList* o = m_domains.skipNull(); o; o = o->skipNext()) { + String* s = static_cast(o->get()); + if (*s == domain || + ((*s)[0] == '*' && domain.endsWith(s->c_str() + 1))) + return true; + } + return false; +} + +// Add a comma separated list of domains to a buffer +void SslContext::addDomains(String& buf) +{ + bool notFirst = false; + for (ObjList* o = m_domains.skipNull(); o; o = o->skipNext()) { + if (notFirst) + buf << ","; + else + notFirst = true; + buf << (static_cast(o->get()))->c_str(); + } +} + +// Release memory, free the context +void SslContext::destruct() +{ + ::SSL_CTX_free(m_context); + String::destruct(); +} + // Create a SSL socket from a regular socket handle -SslSocket::SslSocket(SOCKET handle, bool server, int verify) +SslSocket::SslSocket(SOCKET handle, bool server, int verify, SslContext* context) : Socket(handle), Mutex(false,"SslSocket"), m_ssl(0) { - DDebug(DebugAll,"SslSocket::SslSocket(%d,%s,%s) [%p]", - handle,String::boolText(server),lookup(verify,s_verifyMode,"unknown"),this); + DDebug(&__plugin,DebugAll,"SslSocket::SslSocket(%d,%s,%s,%s) [%p]", + handle,String::boolText(server),lookup(verify,s_verifyMode,"unknown"), + context ? context->c_str() : "",this); if (Socket::valid()) { - m_ssl = ::SSL_new(s_context); + m_ssl = ::SSL_new(context ? *context : s_context); if (s_index >= 0) ::SSL_set_ex_data(m_ssl,s_index,this); ::SSL_set_verify(m_ssl,verify,0); @@ -193,7 +343,7 @@ SslSocket::SslSocket(SOCKET handle, bool server, int verify) // Destructor, clean up early SslSocket::~SslSocket() { - DDebug(DebugAll,"SslSocket::~SslSocket() handle=%d [%p]",handle(),this); + DDebug(&__plugin,DebugAll,"SslSocket::~SslSocket() handle=%d [%p]",handle(),this); clearFilters(); terminate(); } @@ -274,12 +424,12 @@ void SslSocket::onInfo(int where, int retVal) { #ifdef DEBUG if (where & SSL_CB_LOOP) - Debug(MODNAME,DebugAll,"State %s [%p]",SSL_state_string_long(m_ssl),this); + Debug(&__plugin,DebugAll,"State %s [%p]",SSL_state_string_long(m_ssl),this); #endif if ((where & SSL_CB_EXIT) && (retVal == 0)) - Debug(MODNAME,DebugMild,"Failed %s [%p]",SSL_state_string_long(m_ssl),this); + Debug(&__plugin,DebugMild,"Failed %s [%p]",SSL_state_string_long(m_ssl),this); if (where & SSL_CB_ALERT) - Debug(MODNAME,DebugMild,"Alert %s: %s [%p]", + Debug(&__plugin,DebugMild,"Alert %s: %s [%p]", SSL_alert_type_string_long(retVal), SSL_alert_desc_string_long(retVal),this); if (where & SSL_CB_HANDSHAKE_DONE) { @@ -287,7 +437,7 @@ void SslSocket::onInfo(int where, int retVal) if (verify != X509_V_OK) { // handshake succeeded but the certificate has problems const char* error = lookup(verify,s_verifyCodes); - Debug(MODNAME,DebugWarn,"Certificate verify error %ld%s%s [%p]", + Debug(&__plugin,DebugWarn,"Certificate verify error %ld%s%s [%p]", verify,error ? ": " : "",c_safe(error),this); } } @@ -300,23 +450,42 @@ bool SslHandler::received(Message& msg) addRand(msg.msgTime()); Socket** ppSock = static_cast(msg.userObject("Socket*")); if (!ppSock) { - Debug(MODNAME,DebugGoOn,"SslHandler: No pointer to Socket"); + Debug(&__plugin,DebugGoOn,"SslHandler: No pointer to Socket"); return false; } Socket* pSock = *ppSock; if (!pSock) { - Debug(MODNAME,DebugGoOn,"SslHandler: NULL Socket pointer"); + Debug(&__plugin,DebugGoOn,"SslHandler: NULL Socket pointer"); return false; } if (!pSock->valid()) { - Debug(MODNAME,DebugWarn,"SslHandler: Invalid Socket"); + Debug(&__plugin,DebugWarn,"SslHandler: Invalid Socket"); return false; } - SslSocket* sSock = new SslSocket(pSock->handle(), - msg.getBoolValue("server",false), - msg.getIntValue("verify",s_verifyMode,SSL_VERIFY_NONE)); + SslSocket* sSock = 0; + if (msg.getBoolValue("server",false)) { + Lock lock(&__plugin); + SslContext* c = 0; + String* token = msg.getParam("context"); + if (!TelEngine::null(token)) + c = __plugin.findContext(*token); + if (!c) { + token = msg.getParam("domain"); + if (!TelEngine::null(token)) + c = __plugin.findContext(String(*token).toLower(),true); + } + if (!c) { + Debug(&__plugin,DebugWarn,"SslHandler: Unable to find a server context"); + return false; + } + sSock = new SslSocket(pSock->handle(),true, + msg.getIntValue("verify",s_verifyMode,SSL_VERIFY_NONE),c); + } + else + sSock = new SslSocket(pSock->handle(),false, + msg.getIntValue("verify",s_verifyMode,SSL_VERIFY_NONE)); if (!sSock->valid()) { - Debug(MODNAME,DebugWarn,"SslHandler: Invalid SSL Socket"); + Debug(&__plugin,DebugWarn,"SslHandler: Invalid SSL Socket"); // detach and destroy new socket, preserve old one sSock->detach(); delete sSock; @@ -335,12 +504,12 @@ AesCtrCipher::AesCtrCipher() : m_key(0) { m_key = new AES_KEY; - DDebug(DebugAll,"AesCtrCipher::AesCtrCipher() key=%p [%p]",m_key,this); + DDebug(&__plugin,DebugAll,"AesCtrCipher::AesCtrCipher() key=%p [%p]",m_key,this); } AesCtrCipher::~AesCtrCipher() { - DDebug(DebugAll,"AesCtrCipher::~AesCtrCipher() key=%p [%p]",m_key,this); + DDebug(&__plugin,DebugAll,"AesCtrCipher::~AesCtrCipher() key=%p [%p]",m_key,this); delete m_key; } @@ -410,10 +579,11 @@ bool CipherHandler::received(Message& msg) OpenSSL::OpenSSL() - : Plugin("openssl",true), + : Module("openssl","misc",true), m_handler(0) { Output("Loaded module OpenSSL - based on " OPENSSL_VERSION_TEXT); + m_statusCmd << "status " << name(); } OpenSSL::~OpenSSL() @@ -424,20 +594,90 @@ OpenSSL::~OpenSSL() void OpenSSL::initialize() { - if (m_handler) - return; Output("Initializing module OpenSSL"); - ::SSL_load_error_strings(); - ::SSL_library_init(); - addRand(Time::now()); - s_index = ::SSL_get_ex_new_index(0,const_cast("TelEngine::SslSocket"),0,0,0); - s_context = ::SSL_CTX_new(::SSLv23_method()); - SSL_CTX_set_info_callback(s_context,infoCallback); // macro - no :: - m_handler = new SslHandler; - Engine::install(m_handler); + Configuration cfg(Engine::configFile("openssl")); + if (!m_handler) { + setup(); + ::SSL_load_error_strings(); + ::SSL_library_init(); + addRand(Time::now()); + s_index = ::SSL_get_ex_new_index(0,const_cast("TelEngine::SslSocket"),0,0,0); + s_context = ::SSL_CTX_new(::SSLv23_method()); + SSL_CTX_set_info_callback(s_context,infoCallback); // macro - no :: + m_handler = new SslHandler; + Engine::install(m_handler); #ifndef OPENSSL_NO_AES - Engine::install(new CipherHandler); + Engine::install(new CipherHandler); #endif + } + + lock(); + // Load server contexts + unsigned int n = cfg.sections(); + for (unsigned int i = 0; i < n; i++) { + NamedList* p = cfg.getSection(i); + if (!p || *p == "general" || !p->c_str()) + continue; + SslContext* context = findContext(*p); + if (!p->getBoolValue("enable",true)) { + if (context) { + DDebug(this,DebugAll,"Removing disabled context '%s'",context->c_str()); + m_contexts.remove(context); + } + continue; + } + if (!context) + context = new SslContext(*p); + if (context->init(*p)) { + if (!findContext(*p)) { + m_contexts.append(context); + DDebug(this,DebugAll,"Added context '%s'",context->c_str()); + } + } + else { + if (findContext(*p)) { + DDebug(this,DebugAll,"Removing invalid context '%s'",context->c_str()); + m_contexts.remove(context); + } + else { + DDebug(this,DebugAll,"Ignoring invalid context '%s'",context->c_str()); + TelEngine::destruct(context); + } + } + } + unlock(); +} + +// Find a context by name or domain +SslContext* OpenSSL::findContext(const String& token, bool byDomain) const +{ + if (!byDomain) { + ObjList* o = m_contexts.find(token); + return o ? static_cast(o->get()) : 0; + } + for (ObjList* o = m_contexts.skipNull(); o; o = o->skipNext()) { + SslContext* c = static_cast(o->get()); + if (c->hasDomain(token)) + return c; + } + return 0; +} + +void OpenSSL::statusParams(String& str) +{ + Lock lock(this); + str << "contexts=" << m_contexts.count(); +} + +void OpenSSL::statusDetail(String& str) +{ + Lock lock(this); + for (ObjList* o = m_contexts.skipNull(); o; o = o->skipNext()) { + SslContext* c = static_cast(o->get()); + str.append(c->c_str(),";"); + str << "="; + c->addDomains(str); + } } }; // anonymous namespace diff --git a/modules/server/presence.cpp b/modules/server/presence.cpp new file mode 100644 index 00000000..4d814c89 --- /dev/null +++ b/modules/server/presence.cpp @@ -0,0 +1,910 @@ +/** + * presence.cpp + * This file is part of the YATE Project http://YATE.null.ro + * + * Presence module + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2009 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + + +#include +#include + + +using namespace TelEngine; + + +namespace { + +#define MIN_COUNT 16 // number of lists for holding presences +#define EXPIRE_CHECK_MAX 10000 // interval in miliseconds for checking object for expiring +#define TIME_TO_KEEP 30000 // interval in miliseconds for keeping an object in memory + +class PresenceList; +class Presence; +class ResNotifyHandler; +class EngineStartHandler; +class PresenceModule; +class ExpirePresence; + +/* + * Class ResNotifyHandler + * Handles a resource.notify message + */ +class ResNotifyHandler : public MessageHandler +{ +public: + inline ResNotifyHandler(unsigned int priority = 10) + : MessageHandler("resource.notify", priority) + { } + virtual ~ResNotifyHandler() + { } + virtual bool received(Message& msg); +}; + +/* + * Class EngineStartHandler + * Handles a engine.start message + */ +class EngineStartHandler : public MessageHandler +{ +public: + inline EngineStartHandler() + : MessageHandler("engine.start",100) + {} + virtual bool received(Message& msg); +}; + +/* + * class Presence + * A presence object + */ +class Presence : public GenObject +{ +public: + Presence(const String& id, bool online = true, const char* instance = 0, + const char* data = 0, unsigned int expiresMsecs = 0); + ~Presence(); + inline void update(const char* data, unsigned int expireMs) { + m_data = data; + updateExpireTime(expireMs); + } + inline const String& getInstance() const + { return m_instance; } + inline const String& data() const + { return m_data; } + virtual const String& toString() const + { return m_id; } + inline bool hasExpired(u_int64_t time = Time::msecNow()) const + { return m_expires && m_expires < time; } + inline bool isOnline() const + { return m_online;} + inline void setOnline(bool online = true) + { m_online = online; } + inline void updateExpireTime(unsigned int msecs) + { m_expires = msecs ? Time::msecNow() + msecs : 0; } + inline bool isCaps(const String& capsid) const + { return m_caps && *m_caps == capsid; } + inline void setCaps(const String& capsid, const NamedList& list) { + TelEngine::destruct(m_caps); + m_caps = new NamedList(capsid); + m_caps->copyParams(list,"caps",'.'); + } + // Copy parameters to a list + void addCaps(NamedList& list, const String& prefix = String::empty()); +private: + String m_id; // presence id + String m_instance; // presence instance, for jabber it represents the resource + String m_data; // presence data, format unknown + u_int64_t m_expires;// time at which this object will expire + bool m_online; // online/offline flag + NamedList* m_caps; // Capabilities +}; + +/* + * class PresenceList + * A list of presences + */ +class PresenceList : public ObjList, public Mutex +{ +public: + // create a list + PresenceList(); + ~PresenceList(); + // find all presences with given id, disregarding the instance + ObjList* findPresence(const String& id); + // find a presence with the given instance + inline Presence* findPresence(const String& contact, const String& instance) { + ObjList* o = find(contact,instance); + return o ? static_cast(o->get()) : 0; + } + // Remove an item from list. Optionally delete it + // Return the item if found and not deleted + inline Presence* removePresence(const String& contact, const String& instance, + bool delObj = true) { + ObjList* o = find(contact,instance); + return o ? static_cast(o->remove(delObj)) : 0; + } + // delete expired objects + void expire(); + // Find an item by id and instance + ObjList* find(const String& contact, const String& instance); +}; + +/* + * Class PresenceModule + * Module for handling presences + */ +class PresenceModule : public Module +{ + friend class ExpirePresence; +public: + PresenceModule(); + virtual ~PresenceModule(); + //inherited methods + virtual void initialize(); + virtual bool received(Message& msg, int id); + // Get the list containing a given user + inline PresenceList* getList(const String& contact) + { return m_list + (contact.hash() % m_listCount); } + // Update capabilities for all instances with the given caps id + void updateCaps(const String& capsid, Message& msg); + // append presence + void addPresence(Presence* pres, bool onlyLocal = false); + // remove presence + void removePresence(Presence* pres); + // remove presence by id + void removePresenceById(const String& id); + // find presence by id + ObjList* findPresenceById(const String& id); + // find presence with an instance + Presence* findPresence(const String& contact, const String& instance); + // update a presence + void updatePresence(Presence* pres, const char* data); + // uninstall relays and message handlers + bool unload(); + + // Build a 'database' message used to update presence + Message* buildUpdateDb(const Presence& pres, bool newPres); + // Build a 'database' message used to delete presence + Message* buildDeleteDb(const Presence& pres); + + // database comunnication functions + bool insertDB(Presence* pres); + bool updateDB(Presence* pres); + bool removeDB(Presence* pres, bool allInstances = false, bool allPresences = false, String machine = ""); + bool queryDB(String id, String instance); + bool getInfoDB(String id, String instance, NamedList* result); + + // Build and dispatch a 'database' message. Replace query params + // Return a valid Message on success + Message* queryDb(const String& account, const String& query, + const NamedList& params); + // Dispatch a 'database' message. Return a valid Message pointer on success + // Consume the given pointer + Message* queryDb(Message* msg); + +private: + // array of lists that hold presences + PresenceList* m_list; + // resource.notify handler + ResNotifyHandler* m_notifyHandler; + // engine.start handler + EngineStartHandler* m_engineStartHandler; + // number of lists in which presences will be held + unsigned int m_listCount; + // thread for removing expired objects + ExpirePresence* m_expireThread; + // Query strings + // SQL statement for inserting to database + String m_insertDB; + // SQL statement for updating information in the database + String m_updateDB; + // SQL statement for remove a presence with a resource + String m_removeResDB; + // SQL statement for removing all instances of a contact + String m_removePresDB; + // SQL statement for removing all presences of this node + String m_removeAllDB; + // SQL statement for interrogating the database about a contact with a resource + String m_selectResDB; + // SQL statement for interrogating the database about a contact without a resource + String m_selectPresDB; + // database connection + String m_accountDB; +}; + +/* + * class ExpirePresence + * A thread that deletes old presences + */ +class ExpirePresence : public Thread +{ +public: + // create a thread whick will check for expired objects after the given "checkAfter" interval + ExpirePresence(unsigned int checkAfter = 5000); + ~ExpirePresence(); + void run(); + static unsigned int s_expireTime; +private: + unsigned int m_checkMs; +}; + +static String s_msgPrefix = "presence"; +static unsigned int s_presExpire; // Presence expire interval (relese memory only) +static unsigned int s_presExpireCheck; // Presence expire check interval +unsigned int ExpirePresence::s_expireTime = 0; +INIT_PLUGIN(PresenceModule); + + +UNLOAD_PLUGIN(unloadNow) +{ + if (unloadNow && !__plugin.unload()) + return false; + return true; +} + +/* + * ResNotifyHandler + */ +bool ResNotifyHandler::received(Message& msg) +{ + // TODO + // see message parameters and what to do with them + String* node = msg.getParam("nodename"); + if (!TelEngine::null(node) && *node != Engine::nodeName()) + __plugin.removeDB(0, true, true, *node); + + String* operation = msg.getParam("operation"); + if (TelEngine::null(operation)) + return false; + + if (*operation == "updatecaps") { + String* capsid = msg.getParam("caps.id"); + if (TelEngine::null(capsid)) + return false; + DDebug(&__plugin,DebugAll,"Processing %s oper=%s capsid=%s", + msg.c_str(),operation->c_str(),capsid->c_str()); + __plugin.updateCaps(*capsid,msg); + return false; + } + + String* contact = msg.getParam("contact"); + if (TelEngine::null(contact)) + return false; + DDebug(&__plugin,DebugAll,"Processing %s contact=%s oper=%s", + msg.c_str(),contact->c_str(),operation->c_str()); + String* instance = msg.getParam("instance"); + PresenceList* list = __plugin.getList(*contact); + if (*operation == "online" || *operation == "update") { + if (TelEngine::null(instance)) + return false; + Lock lock(list); + Presence* pres = list->findPresence(*contact,*instance); + bool newPres = (pres == 0); + if (newPres) { + pres = new Presence(*contact,true,*instance); + list->append(pres); + } + pres->update(msg.getValue("data"),s_presExpire); + String* capsid = msg.getParam("caps.id"); + if (!TelEngine::null(capsid)) + pres->setCaps(*capsid,msg); + // Update database only if we expire the data from memory + if (s_presExpire) { + Message* m = __plugin.buildUpdateDb(*pres,newPres); + lock.drop(); + TelEngine::destruct(__plugin.queryDb(m)); + } + } + else if (*operation == "remove" || *operation == "offline") { + if (TelEngine::null(instance)) { + // TODO: all contact's instances are offline + return false; + } + list->lock(); + Presence* pres = list->removePresence(*contact,*instance,false); + list->unlock(); + // Remove from database only if we expire the data from memory + if (pres && s_presExpire) + TelEngine::destruct(__plugin.queryDb(__plugin.buildDeleteDb(*pres))); + TelEngine::destruct(pres); + } + else if (*operation == "query") { + Lock lock(list); + if (!TelEngine::null(instance)) { + Presence* pres = list->findPresence(*contact,*instance); + if (pres) { + msg.addParam("data",pres->data()); + msg.addParam("nodename",Engine::nodeName()); + pres->addCaps(msg); + return true; + } + } + else { + ObjList* l = list->findPresence(*contact); + if (!l) + return false; + msg.addParam("message-prefix",s_msgPrefix); + unsigned int n = 0; + String prefix = s_msgPrefix + "."; + for (ObjList* o = l->skipNull(); o; o = o->skipNext()) { + Presence* pres = static_cast(o->get()); + String param; + param << prefix << ++n << "."; + msg.addParam(param + "instance",pres->getInstance()); + msg.addParam(param + "data",pres->data()); + msg.addParam(param + "nodename",Engine::nodeName()); + pres->addCaps(msg,param); + } + msg.addParam(prefix + "count",String(n)); + TelEngine::destruct(l); + return n != 0; + } + } + return false; +} + + +/* + * EngineStartHandler + */ +bool EngineStartHandler::received(Message& msg) +{ + __plugin.removeDB(0,true,true); + return false; +} + +/* + * Presence + */ +Presence::Presence(const String& id, bool online, const char* instance, + const char* data, unsigned int expireMs) + : m_id(id), m_instance(instance), m_data(data), m_online(online), + m_caps(0) +{ + updateExpireTime(expireMs); + DDebug(&__plugin,DebugAll,"Presence contact='%s' instance='%s' online=%s [%p]", + id.c_str(),instance,String::boolText(m_online),this); +} + +Presence::~Presence() +{ + TelEngine::destruct(m_caps); + DDebug(&__plugin,DebugAll,"Presence contact='%s' instance='%s' destroyed [%p]", + m_id.c_str(),m_instance.c_str(),this); +} + +// Copy parameters to a list +void Presence::addCaps(NamedList& list, const String& prefix) +{ + if (!m_caps) + return; + if (!prefix) { + list.copyParams(*m_caps); + return; + } + unsigned int n = m_caps->count(); + for (unsigned int i = 0; i < n; i++) { + NamedString* ns = m_caps->getParam(i); + if (ns) + list.addParam(prefix + ns->name(),*ns); + } +} + + +/* + * PresenceList + */ +PresenceList::PresenceList() + : Mutex("PresenceList") +{ + XDebug(&__plugin,DebugAll,"PresenceList() [%p]",this); +} + +PresenceList::~PresenceList() +{ + XDebug(&__plugin,DebugAll, "PresenceList destroyed [%p]",this); +} + +ObjList* PresenceList::findPresence(const String& id) +{ + if (TelEngine::null(id)) + return 0; + DDebug(&__plugin,DebugAll,"PresenceList::findPresence('%s') [%p]",id.c_str(),this); + ObjList* res = 0; + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + if (id == o->get()->toString()) { + if (!res) + res = new ObjList(); + res->append(o->get())->setDelete(false); + } + } + return res; +} + +void PresenceList::expire() +{ + u_int64_t time = Time::msecNow(); + Lock lock(this); + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Presence* pres = static_cast(o->get()); + if (pres->hasExpired(time)) { + Debug(&__plugin,DebugAll,"Presence (%p) contact=%s instance=%s expired", + pres,pres->toString().c_str(),pres->getInstance().c_str()); + __plugin.removePresence(pres); + } + } +} + +// Find an item by id and instance +ObjList* PresenceList::find(const String& contact, const String& instance) +{ + if (contact.null() || instance.null()) + return 0; + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Presence* pres = static_cast(o->get()); + if (contact == pres->toString() && instance == pres->getInstance()) + return o; + } + return 0; +} + + +/* + * ExpirePresence + */ +ExpirePresence::ExpirePresence(unsigned int checkAfter) + : Thread("ExpirePresence"), m_checkMs(checkAfter) +{ + __plugin.lock(); + __plugin.m_expireThread = this; + __plugin.unlock(); +} + +ExpirePresence::~ExpirePresence() +{ + Debug(&__plugin,DebugAll,"ExpirePresence thread terminated [%p]",this); + __plugin.lock(); + __plugin.m_expireThread = 0; + __plugin.unlock(); +} + +void ExpirePresence::run() +{ + Debug(&__plugin,DebugAll,"%s started [%p]",currentName(),this); + while (true) { + if (Thread::check(false) || Engine::exiting()) + break; + if (s_expireTime < m_checkMs) + Thread::idle(); + else { + s_expireTime = 0; + for (unsigned int i = 0; i < __plugin.m_listCount; i++) + __plugin.m_list[i].expire(); + } + } +} + + +/* + * PresenceModule + */ +PresenceModule::PresenceModule() + : Module("presence", "misc"), + m_list(0), m_notifyHandler(0), m_engineStartHandler(0), + m_listCount(0), m_expireThread(0) +{ + Output("Loaded module Presence"); +} + +PresenceModule::~PresenceModule() +{ + Output("Unloaded module Presence"); + TelEngine::destruct(m_notifyHandler); + TelEngine::destruct(m_engineStartHandler); + if (m_list) + delete[] m_list; +} + +void PresenceModule::initialize() +{ + Output("Initializing module Presence"); + + Configuration cfg(Engine::configFile("presence")); + cfg.load(); + + if (!m_list) { + setup(); + installRelay(Halt); + + m_notifyHandler = new ResNotifyHandler(); + Engine::install(m_notifyHandler); + m_engineStartHandler = new EngineStartHandler(); + Engine::install(m_engineStartHandler); + + m_listCount = cfg.getIntValue("general", "listcount", MIN_COUNT); + if (m_listCount < MIN_COUNT) + m_listCount = MIN_COUNT; + m_list = new PresenceList[m_listCount]; + + // expire thread? + // TODO: Make sure these values are correct + s_presExpireCheck = cfg.getIntValue("general", "expirecheck"); + if (s_presExpireCheck < 0) + s_presExpireCheck = 0; + if (s_presExpireCheck) { + if (s_presExpireCheck > EXPIRE_CHECK_MAX) + s_presExpireCheck = EXPIRE_CHECK_MAX; + s_presExpire = cfg.getIntValue("general", "expiretime", TIME_TO_KEEP); + (new ExpirePresence(s_presExpireCheck))->startup(); + } + + // queries init + m_insertDB = cfg.getValue("database", "insert_presence", ""); + m_updateDB = cfg.getValue("database", "update_presence", ""); + m_removeResDB = cfg.getValue("database", "remove_instance", ""); + m_removePresDB = cfg.getValue("database", "remove_presence", ""); + m_removeAllDB = cfg.getValue("database", "remove_all", ""); + m_selectResDB = cfg.getValue("database", "select_instance", ""); + m_selectPresDB = cfg.getValue("database", "select_presence", ""); + + // database connection init + m_accountDB = cfg.getValue("database", "account"); + } +} + +bool PresenceModule::unload() +{ + DDebug(this,DebugAll,"unload()"); + if (!lock(500000)) + return false; + uninstallRelays(); + Engine::uninstall(m_notifyHandler); + Engine::uninstall(m_engineStartHandler); + if (m_expireThread) + m_expireThread->cancel(); + unlock(); + // Wait for expire thread termination + while (m_expireThread) + Thread::yield(); + return true; +} + +bool PresenceModule::received(Message& msg, int id) +{ + if (id == Timer) + ExpirePresence::s_expireTime += 1000; + else if (id == Halt) { + unload(); + DDebug(this,DebugAll,"Halted"); + } + return Module::received(msg,id); +} + +// Update capabilities for all instances with the given caps id +void PresenceModule::updateCaps(const String& capsid, Message& msg) +{ + for (unsigned int i = 0; i < m_listCount; i++) { + Lock lock(m_list[i]); + for (ObjList* o = m_list[i].skipNull(); o; o = o->skipNext()) { + Presence* p = static_cast(o->get()); + if (p->isCaps(capsid)) + p->setCaps(capsid,msg); + } + } +} + +void PresenceModule::addPresence(Presence* pres, bool onlyLocal) +{ + if (!pres) + return; + unsigned int index = pres->toString().hash() % m_listCount; + DDebug(this,DebugAll,"Adding presence (%p) contact='%s' instance='%s'", + pres,pres->toString().c_str(),pres->getInstance().c_str()); + Lock lock(m_list[index]); + m_list[index].append(pres); + pres->updateExpireTime(s_presExpire); + if (!onlyLocal) + insertDB(pres); +} + +void PresenceModule::removePresence(Presence* pres) +{ + if (!pres) + return; + unsigned int index = pres->toString().hash() % m_listCount; + DDebug(this,DebugAll,"Removing presence (%p) contact=%s instance=%s", + pres,pres->toString().c_str(),pres->getInstance().c_str()); + Lock lock(m_list[index]); + if (!pres->isOnline()) + removeDB(pres); + m_list[index].remove(pres); +} + +void PresenceModule::removePresenceById(const String& id) +{ + if (TelEngine::null(id)) + return; + unsigned int index = id.hash() % m_listCount; + Lock lock(m_list[index]); + for (ObjList* o = m_list[index].skipNull(); o; o = o->skipNext()) { + Presence* pres = static_cast(o->get()); + if (pres->toString() == id) { + m_list[index].remove(pres); + removeDB(pres, true); + } + } + // list->remove(id); +} + +void PresenceModule::updatePresence(Presence* pres, const char* data) +{ + if (!pres) + return; + DDebug(this,DebugAll,"updatePresence() contact='%s' instance='%s' data='%s'", + pres->toString().c_str(),pres->getInstance().c_str(),data); + pres->update(data,s_presExpire); + updateDB(pres); +} + +ObjList* PresenceModule::findPresenceById(const String& id) +{ + if (TelEngine::null(id)) + return 0; + unsigned int index = id.hash() % m_listCount; + Lock lock(m_list[index]); + ObjList* ol = m_list[index].findPresence(id); + NamedList info(""); + if (!getInfoDB(id, "", &info)) + return ol; + int count = info.getIntValue("count"); + for (int i = 1; i <= count; i++) { + String prefix = String(i) + "."; + String* instance = info.getParam(prefix + "instance"); + const char* data = info.getValue(prefix + "data"); + if (instance && !findPresence(id, *instance)) { + Presence* pres = new Presence(id, true, *instance, data); + ol->append(pres)->setDelete(false); + } + } + return ol; +} + +Presence* PresenceModule::findPresence(const String& contact, const String& instance) +{ + if (TelEngine::null(contact)) + return 0; + DDebug(this,DebugAll,"findPresence('%s','%s')",contact.c_str(),instance.c_str()); + unsigned int index = contact.hash() % m_listCount; + Presence* pres = m_list[index].findPresence(contact, instance); + if (pres) + return pres; + NamedList info(""); + if (!getInfoDB(contact, instance, &info)) + return 0; + int count = info.getIntValue("count"); + for (int i = 1; i <= count; i++) { + String data = info.getValue("data"); + Presence* pres = new Presence(contact, true, instance, data); + addPresence(pres, true); + return pres; + } + return 0; +} + +// Build a 'database' message used to update presence +Message* PresenceModule::buildUpdateDb(const Presence& pres, bool newPres) +{ + String& query = newPres ? m_insertDB : m_updateDB; + if (!(m_accountDB && query)) + return 0; + Message* msg = new Message("database"); + msg->addParam("account",m_accountDB); + NamedList p(""); + p.addParam("contact",pres.toString()); + p.addParam("instance",pres.getInstance()); + p.addParam("nodename",Engine::nodeName()); + p.addParam("data",pres.data()); + String tmp = query; + p.replaceParams(tmp,true); + msg->addParam("query",tmp); + return msg; +} + +// Build a 'database' message used to delete presence +Message* PresenceModule::buildDeleteDb(const Presence& pres) +{ + if (!(m_accountDB && m_removeResDB)) + return 0; + Message* msg = new Message("database"); + msg->addParam("account",m_accountDB); + NamedList p(""); + p.addParam("contact",pres.toString()); + p.addParam("instance",pres.getInstance()); + String tmp = m_removeResDB; + p.replaceParams(tmp,true); + msg->addParam("query",tmp); + return msg; +} + +bool PresenceModule::insertDB(Presence* pres) +{ + if (!pres || TelEngine::null(m_insertDB)) + return false; + NamedList p(""); + p.addParam("contact", pres->toString()); + p.addParam("instance", pres->getInstance()); + p.addParam("nodename", Engine::nodeName()); + p.addParam("data", pres->data()); + Message* msg = queryDb(m_accountDB, m_insertDB, p); + if (msg) { + TelEngine::destruct(msg); + return true; + } + return false; +} + +bool PresenceModule::updateDB(Presence* pres) +{ + if (!pres || TelEngine::null(m_updateDB)) + return false; + NamedList p(""); + p.addParam("contact", pres->toString()); + p.addParam("instance", pres->getInstance()); + p.addParam("nodename", Engine::nodeName()); + p.addParam("data", pres->data()); + Message* msg = queryDb(m_accountDB, m_updateDB, p); + if (msg) { + TelEngine::destruct(msg); + return true; + } + return false; +} + +bool PresenceModule::removeDB(Presence* pres, bool allInstances, bool allPresences, String machine) +{ + NamedList queryList(""); + String query; + if (TelEngine::null(machine)) + queryList.addParam("nodename", Engine::nodeName()); + else + queryList.addParam("nodename", machine); + if (allPresences) { + query = m_removeAllDB; + } + else { + if (!pres) + return false; + if (!allInstances) { + query = m_removeResDB; + queryList.addParam("instance", pres->getInstance()); + } + else + query = m_removePresDB; + queryList.addParam("contact", pres->toString()); + } + Message* msg = queryDb(m_accountDB, query, queryList); + if (msg) { + int n = msg->getIntValue("affected"); + if (n > 0) + Debug(this, DebugInfo, "Removed %d items from database", n); + TelEngine::destruct(msg); + return true; + } + return true; +} + +// check only if a contact with/without instance is present, return true or false +bool PresenceModule::queryDB(String id, String instance) +{ + if (TelEngine::null(id)) + return false; + NamedList queryList(""); + String query; + queryList.addParam("contact", id); + if (TelEngine::null(instance)) + query = m_selectPresDB; + else { + queryList.addParam("instance", instance); + query = m_selectResDB; + } + Message* msg = queryDb(m_accountDB, query, queryList); + if (msg) { + bool ok = msg->getIntValue("rows") > 0; + TelEngine::destruct(msg); + return ok; + } + return false; +} + +// Interrogate database about a contact and retrieve data about it +bool PresenceModule::getInfoDB(String id, String instance, NamedList* result) +{ + if (TelEngine::null(id)) + return false; + NamedList queryList(""); + String query; + queryList.addParam("contact", id); + if (TelEngine::null(instance)) + query = m_selectPresDB; + else { + queryList.addParam("instance", instance); + query = m_selectResDB; + } + Message* msg = queryDb(m_accountDB, query, queryList); + Array* res = msg ? static_cast(msg->userObject("Array")) : 0; + int n = res ? msg->getIntValue("rows") : 0; + if (!msg || n < 1) { + TelEngine::destruct(msg); + return false; + } + result->setParam("count", String(res->getRows() - 1)); + for (int i = 0; i < res->getColumns(); i++) { + String* colName = YOBJECT(String, res->get(i, 0)); + if (!(colName && *colName)) + continue; + for (int j = 1; j < res->getRows(); j++) { + String* val = YOBJECT(String, res->get(i, j)); + String paramName = String(j); + paramName << "." << *colName; + if (!val) + continue; + result->setParam(paramName, *val); + } + } + TelEngine::destruct(msg); + return true; +} + +// Build and dispatch a 'database' message. Replace query params +Message* PresenceModule::queryDb(const String& account, const String& query, + const NamedList& params) +{ + Message* msg = new Message("database"); + msg->addParam("account", account); + String tmp = query; + params.replaceParams(tmp,true); + msg->addParam("query", tmp); + msg->addParam("results", String::boolText(true)); + if (!Engine::dispatch(msg) || msg->getParam("error")) { + DDebug(this,DebugNote,"Database query '%s' failed error='%s'", + tmp.c_str(),msg->getValue("error")); + TelEngine::destruct(msg); + } + return msg; +} + +// Dispatch a 'database' message. Return a valid Message pointer on success +// Consume the given pointer +Message* PresenceModule::queryDb(Message* msg) +{ + if (!msg) + return 0; + if (!Engine::dispatch(msg) || msg->getParam("error")) { + DDebug(this,DebugNote,"Database query '%s' failed error='%s'", + msg->getValue("query"),msg->getValue("error")); + TelEngine::destruct(msg); + } + return msg; +} + +} + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/server/subscription.cpp b/modules/server/subscription.cpp new file mode 100644 index 00000000..172e1f8f --- /dev/null +++ b/modules/server/subscription.cpp @@ -0,0 +1,2595 @@ +/** + * subscription.cpp + * This file is part of the YATE Project http://YATE.null.ro + * + * Subscription handler and presence notifier + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2006 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// TODO: +// - Implement commands +// status (user) [instances|contacts] +// drop subscription [to|from] (user) (contact) +// - Handle automatic (un)subscribe response for known users + + +#include + +using namespace TelEngine; +namespace { // anonymous + +class SubscriptionState; // This class holds subscription states +class Instance; // A known instance of an user/contact +class InstanceList; // A list of instances +class Contact; // An user's contact +class User; // An user along with its contacts +class PresenceUser; // An presence user along with its contacts +class EventUser; // An event user along with its contacts +class ExpireThread; // An worker who expires event subscriptions +class UserList; // A list of users +class GenericUser; // A generic user along with its contacts +class GenericContact; // A generic user's contact +class GenericUserList; // A list of generic users +class SubMessageHandler; // Message handler(s) installed by the module +class SubscriptionModule; // The module + +/* + * This class holds subscription states + */ +class SubscriptionState +{ +public: + enum Sub { + None = 0x00, + To = 0x01, + From = 0x02, + PendingIn = 0x10, + PendingOut = 0x20, + }; + inline SubscriptionState() + : m_value(None) + {} + inline SubscriptionState(int flags) + : m_value(flags) + {} + inline SubscriptionState(const String& flags) + : m_value(0) + { replace(flags); } + inline bool to() const + { return test(To); } + inline bool from() const + { return test(From); } + inline bool pendingOut() const + { return test(PendingOut); } + inline bool pendingIn() const + { return test(PendingIn); } + inline void set(int flag) + { m_value |= flag; } + inline void reset(int flag) + { m_value &= ~flag; } + inline void replace(int value) + { m_value = value; } + inline bool test(int mask) const + { return (m_value & mask) != 0; } + inline operator int() const + { return m_value; } + // Replace all flags from a list + void replace(const String& flags); + // Build a list from flags + void toString(String& buf) const; + // Build a list parameters from flags + inline void toParam(NamedList& list, const char* param = "subscription") const { + String buf; + toString(buf); + list.addParam(param,buf); + } + static const TokenDict s_names[]; +private: + int m_value; // The value +}; + +/* + * A known instance of an user/contact + */ +class Instance : public String +{ +public: + inline Instance(const char* name, int prio) + : String(name), m_priority(prio), m_caps(0) + {} + // Add prefixed parameter(s) from this instance + void addListParam(NamedList& list, unsigned int index); + inline bool isCaps(const String& capsid) const + { return m_caps && *m_caps == capsid; } + inline void setCaps(const String& capsid, const NamedList& list) { + TelEngine::destruct(m_caps); + m_caps = new NamedList(capsid); + m_caps->copyParams(list,"caps",'.'); + } + // Copy parameters to a list + void addCaps(NamedList& list, const String& prefix = String::empty()); + + int m_priority; + NamedList* m_caps; +}; + +/* + * A known instance of an user/contact + */ +class InstanceList : public ObjList +{ +public: + // Find an instance + inline Instance* findInstance(const String& name) { + ObjList* o = find(name); + return o ? static_cast(o->get()) : 0; + } + // Insert an instance in the list. Returns it + inline Instance* add(const String& name, int prio) + { return add(new Instance(name,prio)); } + // Insert an instance in the list. Returns it + Instance* add(Instance* inst); + // Insert or set an existing instance. Returns it + Instance* set(const String& name, int prio, bool* newInst = 0); + // Update capabilities for all instances with the given caps id + void updateCaps(const String& capsid, NamedList& list); + // Remove an instance. Returns it found and not deleted + inline Instance* removeInstance(const String& name, bool delObj = true) { + ObjList* o = find(name); + return o ? static_cast(o->remove(delObj)) : 0; + } + // Add prefixed parameter(s) for all instances + // Return the number of instances added + unsigned int addListParam(NamedList& list, String* skip = 0); + // Notify all instances in the list to/from another one + void notifyInstance(bool online, bool out, const String& from, const String& to, + const String& inst, const char* data) const; + // Notify all instances in the list with the same from/to. + // Notifications are made from/to the given instance to/from all other instances + void notifySkip(bool online, bool out, const String& notifier, + const String& inst, const char* data) const; + // Retrieve data and notify each instance in the list to a given one + void notifyUpdate(bool online, const String& from, const String& to, + const String& inst) const; + // Retrieve data and notify each instance in the list to to another list + void notifyUpdate(bool online, const String& from, const String& to, + const InstanceList& dest) const; +}; + +/* + * An user's contact + */ +class Contact : public String +{ +public: + inline Contact(const char* name, int sub) + : String(name), m_subscription(sub) + {} + inline Contact(const char* name, const String& sub = String::empty()) + : String(name), m_subscription(sub) + {} + // Build a 'database' message used to update changes + Message* buildUpdateDb(const String& user, bool add = false); + // Set the contact from an array row + void set(Array& a, int row); + // Build a contact from an array row + static Contact* build(Array& a, int row); + + InstanceList m_instances; + SubscriptionState m_subscription; +}; + +/* + * An user's contact + */ +class EventContact : public NamedList +{ +public: + inline EventContact(const String& id, const NamedList& params) + : NamedList(params), m_sequence(0) { + assign(id); + m_time = params.getIntValue("expires") * 1000 + Time::msecNow(); + } + inline virtual ~EventContact() + {} + inline bool hasExpired(u_int64_t time) + { return time > m_time; } + virtual const String& toString() const + { return *this; } + inline unsigned int getSeq() + { return m_sequence++; } + inline int getTimeLeft() + { return m_time - Time::secNow(); } +private: + u_int64_t m_time; + unsigned int m_sequence; +}; + +/* + * An user along with its contacts + */ +class User : public RefObject, public Mutex +{ +public: + User(const char* name); + virtual ~User(); + inline const String& user() const + { return m_user; } + virtual const String& toString() const + { return m_user; } + ObjList m_list; // The list of contacts +protected: + virtual void destroyed(); +private: + String m_user; // The user name +}; + +/* + * An user along with its contacts + */ +class PresenceUser : public User +{ +public: + PresenceUser(const char* name); + virtual ~PresenceUser(); + inline InstanceList& instances() + { return m_instances; } + // Notify all user's instances + void notify(const Message& msg); + // Append a new contact + void appendContact(Contact* c); + inline Contact* appendContact(const char* name, int sub) { + Contact* c = new Contact(name,sub); + appendContact(c); + return c; + } + // Find a contact + inline Contact* findContact(const String& name) { + ObjList* o = m_list.find(name); + return o ? static_cast(o->get()) : 0; + } + // Remove a contact. Return it if found and not deleted + Contact* removeContact(const String& name, bool delObj = true); + // +private: + InstanceList m_instances; // The list of instances +}; + +/* + * An user along with its contacts + */ +class EventUser : public User +{ +public: + EventUser(const char* name); + virtual ~EventUser(); + // Notify all user's + void notify(const Message& msg, bool haveDialog = true); + void notifyMwi(const Message& msg); + // Append a new contact + void appendContact(EventContact* c); + // Find a contact + inline EventContact* findContact(const String& name) { + ObjList* o = m_list.find(name); + return o ? static_cast(o->get()) : 0; + } + void expire(u_int64_t time); + // Remove a contact. Return it if found and not deleted + EventContact* removeContact(const String& name, bool delObj = true); + NamedList* getParams(const NamedList& msg,bool init); + // +}; + +class ExpireThread : public Thread +{ +public: + ExpireThread(Priority prio = Thread::Normal); + virtual ~ExpireThread(); + virtual void run(); +}; + +/* + * A list of users + */ +class UserList : public GenObject, public Mutex +{ +public: + UserList(); + inline ObjList& users() + { return m_users; } + // Find an user. Load it from database if not found and load is true + // Returns referrenced pointer if found + PresenceUser* getUser(const String& user, bool load = true); + // Remove an user from list + void removeUser(const String& user); +protected: + // Load an user from database. Build an PresenceUser object and returns it if found + PresenceUser* askDatabase(const String& name); +private: + ObjList m_users; // Users list +}; + +/* + * A generic user along with its contacts + */ +class GenericUser : public RefObject, public Mutex +{ +public: + GenericUser(const char* regexp); + ~GenericUser(); + inline bool matches(const char* str) const + { return m_user.matches(str); } + inline bool compile() + { return m_user.compile(); } + virtual const String& toString() const + { return m_user; } + // Find a contact matching the given string + GenericContact* find(const String& contact); + ObjList m_list; // The list of contacts +protected: + virtual void destroyed(); +private: + Regexp m_user; // The user regexp +}; + +/* + * A generic user's contact + */ +class GenericContact : public Regexp +{ +public: + inline GenericContact(const char* regexp) + : Regexp(regexp) + {} +}; + +/* + * A list of generic users + */ +class GenericUserList : public ObjList, public Mutex +{ +public: + GenericUserList(); + // (Re)Load from database + void load(); + // Find an user matching the given string + // Returns referenced pointer + GenericUser* findUser(const String& user); +}; + +/* + * Message handler(s) installed by the module + */ +class SubMessageHandler : public MessageHandler +{ +public: + enum { + ResSubscribe, + ResNotify, + UserRoster, + UserUpdate, + EngineStart, + CallCdr, + Mwi, + }; + SubMessageHandler(int handler, int prio = 80); +protected: + virtual bool received(Message& msg); +private: + int m_handler; +}; + +/* + * The module + */ +class SubscriptionModule : public Module +{ +public: + SubscriptionModule(); + virtual ~SubscriptionModule(); + virtual void initialize(); + // Check if a message was sent by us + inline bool isModule(const Message& msg) const { + 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; + } + // Enqueue a resource.notify for a given instance + void notify(bool online, const String& from, const String& to, + const String& fromInst = String::empty(), const String& toInst = String::empty(), + const char* data = 0); + // Notify (un)subscribed + void subscribed(bool sub, const String& from, const String& to); + // Enqueue a resource.subscribe + void subscribe(bool sub, const String& from, const String& to, + const String* instance = 0); + // Enqueue a resource.notify with operation=probe + void probe(const char* from, const char* to); + // Dispatch a user.roster message with operation 'update' + // Load contact data from database + // Return the database result if requested + Array* notifyRosterUpdate(const char* username, const char* contact, + bool retData = false, bool sync = true); + // Handle 'resource.subscribe' for messages with event + bool handleResSubscribe(const String& event, const String& subscriber, + const String& notifier, const String& oper, Message& msg); + void handleCallCdr(const Message& msg, const String& notif); + void handleMwi(const Message& msg); + // Handle 'resource.subscribe' messages with (un)subscribe operation + bool handleResSubscribe(bool sub, const String& subscriber, const String& notifier, + Message& msg); + bool askDB(const String& subscriber, const String& notifier, const String& oper); + EventUser* getEventUser(bool create, const String& notifier, const String& oper); + // Handle 'resource.subscribe' messages with query operation + bool handleResSubscribeQuery(const String& subscriber, const String& notifier, + Message& msg); + // Handle online/offline resource.notify from contact + bool handleResNotify(bool online, Message& msg); + // Handle resource.notify with operation (un)subscribed + bool handleResNotifySub(bool sub, const String& from, const String& to, + Message& msg); + // Handle resource.notify with operation probe + bool handleResNotifyProbe(const String& from, const String& to, Message& msg); + // Update capabilities for all instances with the given caps id + void updateCaps(const String& capsid, NamedList& list); + // Handle 'user.roster' messages with operation 'query' + bool handleUserRosterQuery(const String& user, const String* contact, Message& msg); + // Handle 'user.roster' messages with operation 'update' + bool handleUserRosterUpdate(const String& user, const String& contact, Message& msg); + // Handle 'user.roster' messages with operation 'delete' + bool handleUserRosterDelete(const String& user, const String& contact, Message& msg); + // Handle 'user.update' messages with operation 'delete' + void handleUserUpdateDelete(const String& user, Message& msg); + // Handle 'msg.route' messages + bool imRoute(Message& msg); + void expireSubscriptions(); + // Build a database message from account and query. + // Replace query params. Return Message pointer on success + Message* buildDb(const String& account, const String& query, + const NamedList& params); + // Dispatch a database message + // Return Message pointer on success. Release msg on failure + Message* queryDb(Message*& msg); + + String m_account; + String m_userLoadQuery; + String m_userEventQuery; + String m_userDeleteQuery; + String m_contactLoadQuery; + String m_contactSubSetQuery; + String m_contactSetQuery; + String m_contactDeleteQuery; + String m_genericUserLoadQuery; + UserList m_users; + NamedList m_events; + ExpireThread* m_expire; + GenericUserList m_genericUsers; + +protected: + virtual bool received(Message& msg, int id); + // Execute commands + virtual bool commandExecute(String& retVal, const String& line); + // Handle command complete requests + virtual bool commandComplete(Message& msg, const String& partLine, + const String& partWord); + // Notify 'from' instances to 'to' + void notifyInstances(bool online, PresenceUser& from, PresenceUser& to); +private: + ObjList m_handlers; // Message handlers list +}; + + +INIT_PLUGIN(SubscriptionModule); // The module +static bool s_singleOffline = true; // Enqueue a single 'offline' resource.notify + // message when multiple instances are available +bool s_check = true; + +// Subscription flag names +const TokenDict SubscriptionState::s_names[] = { + {"none", None}, + {"to", To}, + {"from", From}, + {"pending_in", PendingIn}, + {"pending_out", PendingOut}, + {0,0}, +}; + +// Message handlers installed by the module +static const TokenDict s_msgHandler[] = { + {"resource.subscribe", SubMessageHandler::ResSubscribe}, + {"resource.notify", SubMessageHandler::ResNotify}, + {"user.roster", SubMessageHandler::UserRoster}, + {"user.update", SubMessageHandler::UserUpdate}, + {"engine.start", SubMessageHandler::EngineStart}, + {"call.cdr", SubMessageHandler::CallCdr}, + {"mwi", SubMessageHandler::Mwi}, + {0,0} +}; + +static const char* s_cmds[] = { + "status", // Subscription status + "unsubscribe", // Unsubscribe user from contact's presence + 0 +}; + +// Decode a list of comma separated flags +static int decodeFlags(const String& str, const TokenDict* flags) +{ + int st = 0; + ObjList* list = str.split(',',false); + for (ObjList* ob = list->skipNull(); ob; ob = ob->skipNext()) + st |= lookup(static_cast(ob->get())->c_str(),flags); + TelEngine::destruct(list); + return st; +} + +// Encode a value to comma separated list of flags +static void encodeFlags(String& buf, int value, const TokenDict* flags) +{ + if (!flags) + return; + for (; flags->token; flags++) + if (0 != (value & flags->value)) + buf.append(flags->token,","); +} + + +/* + * SubscriptionState + */ +// Replace all flags from a list +void SubscriptionState::replace(const String& flags) +{ + m_value = decodeFlags(flags,s_names); +} + +// Build a list from flags +void SubscriptionState::toString(String& buf) const +{ + encodeFlags(buf,m_value,s_names); +} + + +/* + * Instance + */ +// Add prefixed parameter(s) from this instance +void Instance::addListParam(NamedList& list, unsigned int index) +{ + String prefix("instance."); + prefix << index; + list.addParam(prefix,c_str()); + addCaps(list,prefix + "."); +} + +// Copy parameters to a list +void Instance::addCaps(NamedList& list, const String& prefix) +{ + if (!m_caps) + return; + if (!prefix) { + list.copyParams(*m_caps); + return; + } + unsigned int n = m_caps->count(); + for (unsigned int i = 0; i < n; i++) { + NamedString* ns = m_caps->getParam(i); + if (ns) + list.addParam(prefix + ns->name(),*ns); + } +} + + +/* + * InstanceList + */ +// Insert an instance in the list +Instance* InstanceList::add(Instance* inst) +{ + if (!inst) + return 0; + ObjList* o = skipNull(); + for (; o; o = o->skipNext()) { + Instance* tmp = static_cast(o->get()); + if (inst->m_priority > tmp->m_priority) + break; + } + if (o) + o->insert(inst); + else + append(inst); + XDebug(&__plugin,DebugAll,"InstanceList set '%s' prio=%u [%p]", + inst->c_str(),inst->m_priority,this); + return inst; +} + +// Insert or set an existing instance +Instance* InstanceList::set(const String& name, int prio, bool* newInst) +{ + Instance* inst = 0; + ObjList* o = find(name); + if (newInst) + *newInst = (o == 0); + if (o) { + inst = static_cast(o->get()); + // Re-insert if priority changed + if (inst->m_priority != prio) { + o->remove(false); + inst->m_priority = prio; + add(inst); + } + } + else + inst = add(name,prio); + return inst; +} + +// Update capabilities for all instances with the given caps id +void InstanceList::updateCaps(const String& capsid, NamedList& list) +{ + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Instance* i = static_cast(o->get()); + if (i->isCaps(capsid)) + i->setCaps(capsid,list); + } +} + +// Add prefixed parameter(s) for all instances +// Return the number of instances added +unsigned int InstanceList::addListParam(NamedList& list, String* skip) +{ + unsigned int n = 0; + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Instance* tmp = static_cast(o->get()); + if (!skip || *skip != *tmp) + tmp->addListParam(list,++n); + } + return n; +} + +// Notify all instances in the list +void InstanceList::notifyInstance(bool online, bool out, const String& from, + const String& to, const String& inst, const char* data) const +{ + DDebug(&__plugin,DebugAll, + "InstanceList::notifyInstance(%s,%s,%s,%s,%s,%p) count=%u [%p]", + online ? "online" : "offline",out ? "from" : "to", + from.c_str(),to.c_str(),inst.c_str(),data,count(),this); + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Instance* tmp = static_cast(o->get()); + if (out) + __plugin.notify(online,from,to,*tmp,inst,data); + else + __plugin.notify(online,from,to,inst,*tmp,data); + } +} + +// Notify all instances in the list with the same from/to. +// Notifications are made from/to the given instance to/from all other instances +void InstanceList::notifySkip(bool online, bool out, const String& notifier, + const String& inst, const char* data) const +{ + DDebug(&__plugin,DebugAll,"InstanceList::notifySkip(%s,%s,%s,%s,%p) [%p]", + online ? "online" : "offline",out ? "from" : "to", + notifier.c_str(),inst.c_str(),data,this); + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Instance* tmp = static_cast(o->get()); + if (*tmp != inst) { + if (out) + __plugin.notify(online,notifier,notifier,*tmp,inst,data); + else + __plugin.notify(online,notifier,notifier,inst,*tmp,data); + } + } +} + +// Retrieve data and notify each instance in the list to a given one +void InstanceList::notifyUpdate(bool online, const String& from, const String& to, + const String& inst) const +{ + DDebug(&__plugin,DebugAll,"InstanceList::notifyUpdate(%s,%s,%s,%s) [%p]", + online ? "online" : "offline",from.c_str(),to.c_str(),inst.c_str(),this); + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Instance* tmp = static_cast(o->get()); + Message* m = 0; + const char* data = 0; + if (online) { + m = __plugin.message("resource.notify"); + m->addParam("operation","query"); + m->addParam("contact",from); + m->addParam("instance",*tmp); + if (Engine::dispatch(m)) + data = m->getValue("data"); + } + __plugin.notify(online,from,to,*tmp,inst,data); + TelEngine::destruct(m); + } +} + +// Retrieve data and notify each instance in the list to to another list +void InstanceList::notifyUpdate(bool online, const String& from, const String& to, + const InstanceList& dest) const +{ + DDebug(&__plugin,DebugAll,"InstanceList::notifyUpdate(%s,%s,%s) [%p]", + online ? "online" : "offline",from.c_str(),to.c_str(),this); + if (!dest.skipNull()) + return; + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + Instance* tmp = static_cast(o->get()); + Message* m = 0; + const char* data = 0; + if (online) { + m = __plugin.message("resource.notify"); + m->addParam("operation","query"); + m->addParam("contact",from); + m->addParam("instance",*tmp); + if (Engine::dispatch(m)) + data = m->getValue("data"); + } + dest.notifyInstance(online,false,from,to,*tmp,data); + TelEngine::destruct(m); + } +} + + +/* + * Contact + */ +// Build a 'database' message used to update changes +Message* Contact::buildUpdateDb(const String& user, bool add) +{ + NamedList p(""); + p.addParam("username",user); + p.addParam("contact",c_str()); + m_subscription.toParam(p); + DDebug(&__plugin,DebugAll,"Contact::buildUpdateDb() user=%s %s contact=%s sub=%s", + user.c_str(),add ? "adding" : "updating",c_str(),p.getValue("subscription")); + return __plugin.buildDb(__plugin.m_account,__plugin.m_contactSubSetQuery,p); +} + +// Set the contact from an array row +void Contact::set(Array& a, int row) +{ + int cols = a.getColumns(); + for (int col = 1; col < cols; col++) { + String* s = YOBJECT(String,a.get(col,0)); + if (!s) + continue; + if (*s == "subscription") { + String* sub = YOBJECT(String,a.get(col,row)); + if (sub) + m_subscription.replace(*sub); + } + } +} + +// Build a contact from an array row +Contact* Contact::build(Array& a, int row) +{ + Contact* c = 0; + int cols = a.getColumns(); + for (int col = 1; col < cols; col++) { + String* s = YOBJECT(String,a.get(col,0)); + if (!s) + continue; + if (*s == "contact") { + String* n = YOBJECT(String,a.get(col,row)); + if (!TelEngine::null(n)) + c = new Contact(*n,String::empty()); + break; + } + } + if (c) + c->set(a,row); + return c; +} + +User::User(const char* name) + : Mutex(true,__plugin.name() + ":User"), m_user(name) +{ + +} + +User::~User() +{ + m_list.clear(); + m_user.clear(); +} + +void User::destroyed() +{ + m_list.clear(); + RefObject::destroyed(); +} + +/* + * PresenceUser + */ +PresenceUser::PresenceUser(const char* name) + : User(name) +{ + DDebug(&__plugin,DebugAll,"PresenceUser::PresenceUser(%s) [%p]",name,this); +} + +PresenceUser::~PresenceUser() +{ + DDebug(&__plugin,DebugAll,"PresenceUser::~PresenceUser(%s) [%p]",user().c_str(),this); + m_list.clear(); +} + +void PresenceUser::notify(const Message& msg) +{ + Lock lock(this); + ObjList* o = m_list.skipNull(); + for (; o; o = o->skipNext()) { + Contact* c = static_cast(o->get()); + if (!c->m_subscription.from()) + continue; + if (!c->m_instances.skipNull()) { + DDebug(&__plugin,DebugAll,"PresenceUser(%s) no instances for contact %s [%p]", + user().c_str(),c->c_str(),this); + continue; + } + DDebug(&__plugin,DebugAll,"PresenceUser(%s) notifying contact %s [%p]", + user().c_str(),c->c_str(),this); + String* oper = msg.getParam("operation"); + bool online = !oper || *oper != "finalize"; + c->m_instances.notifyInstance(online,false,user(),*c,msg.getValue("callid"),0); + } + +} + +// Add a contact +void PresenceUser::appendContact(Contact* c) +{ + if (!c) + return; + Lock lock(this); + m_list.append(c); +#ifdef DEBUG + String sub; + c->m_subscription.toString(sub); + DDebug(&__plugin,DebugAll,"PresenceUser(%s) added contact (%p,%s) subscription=%s [%p]", + user().c_str(),c,c->c_str(),sub.c_str(),this); +#endif +} + +// Remove a contact. Return it if found and not deleted +Contact* PresenceUser::removeContact(const String& name, bool delObj) +{ + ObjList* o = m_list.find(name); + if (!o) + return 0; + Contact* c = static_cast(o->get()); +#ifdef DEBUG + String sub; + c->m_subscription.toString(sub); + DDebug(&__plugin,DebugAll,"PresenceUser(%s) removed contact (%p,%s) subscription=%s [%p]", + user().c_str(),c,c->c_str(),sub.c_str(),this); +#endif + o->remove(delObj); + return delObj ? 0 : c; +} + +/* + * EventUser + */ +EventUser::EventUser(const char* name) + : User(name) +{ + DDebug(&__plugin,DebugAll,"EventUser::EventUser(%s) [%p]",name,this); +} + +EventUser::~EventUser() +{ + DDebug(&__plugin,DebugAll,"PresenceUser::~PresenceUser(%s) [%p]",user().c_str(),this); + m_list.clear(); +} + +// Add a contact +void EventUser::appendContact(EventContact* c) +{ + if (!c) + return; + Lock lock(this); + ObjList* o = m_list.find(c->toString()); + if (o) + o->set(c); + else + m_list.append(c); +#ifdef DEBUG + String sub; + DDebug(&__plugin,DebugAll,"EventUser(%s) added contact (%p,%s) [%p]", + user().c_str(),c,c->c_str(),this); +#endif +} + +// Remove a contact. Return it if found and not deleted +EventContact* EventUser::removeContact(const String& name, bool delObj) +{ + ObjList* o = m_list.find(name); + if (!o) + return 0; + EventContact* c = static_cast(o->get()); +#ifdef DEBUG + DDebug(&__plugin,DebugAll,"EventUser(%s) removed contact (%p,%s) [%p]", + user().c_str(),c,c->c_str(),this); +#endif + o->remove(delObj); + return delObj ? 0 : c; +} + +void EventUser::notify(const Message& msg, bool haveDialog) +{ + for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) { + EventContact* c = static_cast(o->get()); + if (!c) + continue; + String notif = msg.getValue("caller"); + if(notif == *c) + continue; + Message* m = new Message("resource.notify"); + m->copyParams(*c); + m->setParam("notifyseq",String(c->getSeq())); + m->setParam("subscriptionstate","active"); + m->setParam("expires",String(c->getTimeLeft())); + if (m->getParam("notifier-uri")) + m->setParam("notifier-uri",msg.getValue("local-uri")); + String oper = msg.getValue("operation"); + bool init = (oper == "initialize"); + if (haveDialog) { + m->setParam("state","full"); + NamedList* nl = getParams(msg,init); + if (!nl) { + Engine::enqueue(m); + continue; + } + String dir = msg.getValue("direction"); + String caller,called; + if (dir == "incoming") { + called = msg.getParam("called"); + caller = msg.getParam("caller"); + } + else if (dir == "outgoing") { + called = msg.getParam("caller"); + caller = msg.getParam("called"); + } + nl->addParam("dialog.caller",caller); + nl->addParam("dialog.called",called); + NamedPointer* p = new NamedPointer("cdr",nl); + m->addParam(p); + } + else { + m->setParam("state","full"); + for (unsigned int i = 0;i < msg.count();i++) { + NamedString* ns = msg.getParam(i); + NamedPointer* p = ns ? static_cast(ns->getObject("NamedPointer")) : 0; + if (!p) + continue; + NamedList* list = static_cast(p->userData()); + if (!list) + continue; + NamedList* nl = getParams(*list,init); + NamedPointer* np = new NamedPointer("cdr",nl); + m->addParam(np); + } + } + Engine::enqueue(m); + } +} + +void EventUser::notifyMwi(const Message& msg) +{ + for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) { + EventContact* c = static_cast(o->get()); + if (!c) + continue; + Message* m = new Message("resource.notify"); + m->copyParams(msg); + m->copyParams(*c); + Engine::enqueue(m); + } +} + +NamedList* EventUser::getParams(const NamedList& msg,bool init) +{ + NamedList* nl = new NamedList(""); + nl->setParam("dialog.id",msg.getValue("billid")); + String state = msg.getValue("status"); + if (state == "incoming" || state == "outgoing") + state = "initiating"; + String oper = msg.getValue("operation"); + if (oper == "finalize") + state = "hangup"; + nl->setParam("dialog.state",state); + if (init) + return nl; + nl->setParam("dialog.callid",msg.getValue("chan")); + nl->setParam("dialog.remoteuri",msg.getValue("remote-uri")); + nl->setParam("dialog.localuri",msg.getValue("local-uri")); + nl->setParam("duration",msg.getValue("duration")); + nl->setParam("dialog.direction",msg.getValue("direction")); + return nl; +} + +void EventUser::expire(u_int64_t time) +{ + for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) { + EventContact* c = static_cast(o->get()); + if (!c) + continue; + if (!c->hasExpired(time)) + continue; + Debug(DebugNote,"Subscribtion terminated for Contact %s",c->c_str()); + Message* m = new Message ("resource.notify"); + m->addParam("subscriptionstate","terminated"); + m->addParam("terminatereason","timeout"); + m->copyParams(*c); + m_list.remove(c); + Engine::enqueue(m); + } +} + +/* + * ExpireThread + */ +ExpireThread::ExpireThread(Priority prio) + : Thread("ExpireThread", prio) +{ + XDebug(&__plugin,DebugAll,"ExpireThread created [%p]",this); + Lock lock(__plugin); + __plugin.m_expire = this; +} + +ExpireThread::~ExpireThread() +{ + XDebug(&__plugin,DebugAll,"ExpireThread destroyed [%p]",this); + Lock lock(__plugin); + if (__plugin.m_expire) { + __plugin.m_expire = 0; + lock.drop(); + Debug(&__plugin,DebugWarn,"ExpireThread abnormally terminated [%p]",this); + } +} + +void ExpireThread::run() +{ + DDebug(&__plugin,DebugAll,"%s start running [%p]",currentName(),this); + while (!Engine::exiting()) { + if (s_check) { + __plugin.expireSubscriptions(); + s_check = false; + } + Thread::idle(false); + if (Thread::check(false)) + break; + } + Lock lock(__plugin); + __plugin.m_expire = 0; +} + + + +/* + * UserList + */ +UserList::UserList() + : Mutex(true,__plugin.name() + ":UserList") +{ +} + +// Find an user. Load it from database if not found +// Returns referrenced pointer if found +PresenceUser* UserList::getUser(const String& user, bool load) +{ + XDebug(&__plugin,DebugAll,"UserList::getUser(%s)",user.c_str()); + Lock lock(this); + ObjList* o = m_users.find(user); + if (o) { + PresenceUser* u = static_cast(o->get()); + return u->ref() ? u : 0; + } + lock.drop(); + if (!load) + return 0; + PresenceUser* u = askDatabase(user); + if (!u) + return 0; + // Check if the user was already added while unlocked + Lock lock2(this); + ObjList* tmp = m_users.find(user); + if (!tmp) + m_users.append(u); + else { + TelEngine::destruct(u); + u = static_cast(tmp->get()); + } + return u->ref() ? u : 0; +} + +// Remove an user from list +void UserList::removeUser(const String& user) +{ + Lock lock(this); + ObjList* o = m_users.find(user); + if (!o) + return; +#ifdef DEBUG + PresenceUser* u = static_cast(o->get()); + Debug(&__plugin,DebugAll,"UserList::removeUser() %p '%s'",u,user.c_str()); +#endif + o->remove(); +} + +// Load an user from database. Build an PresenceUser and returns it if found +PresenceUser* UserList::askDatabase(const String& name) +{ + NamedList p(""); + p.addParam("username",name); + Message* m = __plugin.buildDb(__plugin.m_account,__plugin.m_userLoadQuery,p); + m = __plugin.queryDb(m); + if (!m) + return 0; + PresenceUser* u = 0; + Array* a = 0; + if (m->getIntValue("rows") >= 1) + a = static_cast(m->userObject("Array")); + if (a) { + u = new PresenceUser(name); + int rows = a->getRows(); + for (int i = 1; i < rows; i++) { + Contact* c = Contact::build(*a,i); + if (c) + u->appendContact(c); + } + } + TelEngine::destruct(m); + return u; +} + + +/* + * GenericUser + */ +GenericUser::GenericUser(const char* regexp) + : Mutex(true,__plugin.name() + ":GenericUser"), + m_user(regexp) +{ + DDebug(&__plugin,DebugAll,"GenericUser(%s) [%p]",regexp,this); +} + +GenericUser::~GenericUser() +{ + DDebug(&__plugin,DebugAll,"GenericUser(%s) destroyed [%p]",m_user.c_str(),this); + m_list.clear(); +} + +// Find a contact matching the given string +GenericContact* GenericUser::find(const String& contact) +{ + for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) { + GenericContact* c = static_cast(o->get()); + if (c->matches(contact.c_str())) + return c; + } + return 0; +} + +void GenericUser::destroyed() +{ + m_list.clear(); + RefObject::destroyed(); +} + + +/* + * GenericUserList + */ +GenericUserList::GenericUserList() + : Mutex(true,__plugin.name() + ":GenericUserList") +{ +} + +// (Re)Load from database +void GenericUserList::load() +{ + DDebug(&__plugin,DebugAll,"Loading generic users"); + Message* m = __plugin.buildDb(__plugin.m_account,__plugin.m_genericUserLoadQuery,NamedList::empty()); + m = __plugin.queryDb(m); + Lock lock(this); + clear(); + if (!m) + return; + Array* a = static_cast(m->userObject("Array")); + if (!a) { + TelEngine::destruct(m); + return; + } + int rows = a->getRows(); + int cols = a->getColumns(); + for (int i = 1; i < rows; i++) { + String* user = 0; + String* contact = 0; + // Get username + for (int j = 0; j < cols; j++) { + String* tmp = YOBJECT(String,a->get(j,0)); + if (!tmp) + continue; + if (*tmp == "username") + user = YOBJECT(String,a->get(j,i)); + else if (*tmp == "contact") + contact = YOBJECT(String,a->get(j,i)); + } + if (!(user && contact)) + continue; + GenericContact* c = new GenericContact(*contact); + if (!c->compile()) { + Debug(&__plugin,DebugNote,"Invalid generic contact regexp '%s' for user=%s", + contact->c_str(),user->c_str()); + TelEngine::destruct(c); + continue; + } + GenericUser* u = 0; + ObjList* o = find(*user); + if (o) + u = static_cast(o->get()); + else { + u = new GenericUser(*user); + if (u->compile()) + append(u); + else { + Debug(&__plugin,DebugNote,"Invalid generic user regexp '%s'",user->c_str()); + TelEngine::destruct(c); + TelEngine::destruct(u); + } + } + if (u) { + u->lock(); + u->m_list.append(c); + u->unlock(); + DDebug(&__plugin,DebugAll,"Added generic user='%s' contact='%s'", + user->c_str(),contact->c_str()); + } + } + TelEngine::destruct(m); +} + +// Find an user matching the given string +// Returns referenced pointer +GenericUser* GenericUserList::findUser(const String& user) +{ + Lock lock(this); + for (ObjList* o = skipNull(); o; o = o->skipNext()) { + GenericUser* u = static_cast(o->get()); + if (u->matches(user)) + return u->ref() ? u : 0; + } + return 0; +} + + +/* + * SubMessageHandler + */ +SubMessageHandler::SubMessageHandler(int handler, int prio) + : MessageHandler(lookup(handler,s_msgHandler),prio), + m_handler(handler) +{ +} + +bool SubMessageHandler::received(Message& msg) +{ + if (m_handler == ResNotify) { + if (__plugin.isModule(msg) || msg.getParam("event")) + return false; + 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") + return __plugin.handleResNotify(online,msg); + if (*oper == "updatecaps") { + String* capsid = msg.getParam("caps.id"); + if (!TelEngine::null(capsid)) + __plugin.updateCaps(*capsid,msg); + 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") + return __plugin.handleResNotifySub(sub,*src,*dest,msg); + // probe + if (*oper == "probe") + return __plugin.handleResNotifyProbe(*src,*dest,msg); + return false; + } + if (m_handler == ResSubscribe) { + if (__plugin.isModule(msg)) + return false; + String* oper = msg.getParam("operation"); + String* notifier = msg.getParam("notifier"); + String* subscriber = msg.getParam("subscriber"); + if (TelEngine::null(oper) || TelEngine::null(subscriber) || TelEngine::null(notifier)) + return false; + String* event = msg.getParam("event"); + if (event) { + if (!__plugin.m_userEventQuery) + return false; + return __plugin.handleResSubscribe(*event,*subscriber,*notifier,*oper,msg); + } + bool sub = (*oper == "subscribe"); + if (sub || *oper == "unsubscribe") + return __plugin.handleResSubscribe(sub,*subscriber,*notifier,msg); + if (*oper == "query") + return __plugin.handleResSubscribeQuery(*subscriber,*notifier,msg); + return false; + } + if (m_handler == UserRoster) { + if (__plugin.isModule(msg)) + return false; + XDebug(&__plugin,DebugAll,"%s oper='%s' user='%s' contact='%s'", + msg.c_str(),msg.getValue("operation"),msg.getValue("username"), + msg.getValue("contact")); + String* oper = msg.getParam("operation"); + if (TelEngine::null(oper)) + return false; + String* user = msg.getParam("username"); + String* contact = msg.getParam("contact"); + if (TelEngine::null(user)) + return false; + if (*oper == "query") + return __plugin.handleUserRosterQuery(*user,contact,msg); + if (TelEngine::null(contact)) + return false; + if (*oper == "update") + return __plugin.handleUserRosterUpdate(*user,*contact,msg); + if (*oper == "delete") + return __plugin.handleUserRosterDelete(*user,*contact,msg); + return false; + } + if (m_handler == UserUpdate) { + String* notif = msg.getParam("notify"); + if (TelEngine::null(notif) || *notif != "delete") + return false; + String* user = msg.getParam("user"); + if (TelEngine::null(user)) + return false; + __plugin.handleUserUpdateDelete(*user,msg); + return false; + } + if (m_handler == EngineStart) { + __plugin.m_genericUsers.load(); + return false; + } + if (m_handler == CallCdr) { + String* notif = msg.getParam("external"); + if (TelEngine::null(notif)) + return false; + __plugin.handleCallCdr(msg,*notif); + return false; + } + if (m_handler == Mwi) { + String* oper = msg.getParam("operation"); + if (*oper != "notify") + return false; + __plugin.handleMwi(msg); + return true; + } + + Debug(&__plugin,DebugStub,"SubMessageHandler(%s) not handled!",msg.c_str()); + return false; +} + + +/* + * SubscriptionModule Module + */ +SubscriptionModule::SubscriptionModule() + : Module("subscription","misc",true), m_events("") +{ + Output("Loaded module Subscriptions"); +} + +SubscriptionModule::~SubscriptionModule() +{ + Output("Unloading module Subscriptions"); +} + +void SubscriptionModule::initialize() +{ + Output("Initializing module Subscriptions"); + + if (m_handlers.skipNull()) { + // Reload generic users (wait engine.start for the first load) + m_genericUsers.load(); + } + else { + Configuration cfg(Engine::configFile("subscription")); + m_account = cfg.getValue("general","account"); + m_userLoadQuery = cfg.getValue("general","user_roster_load"); + m_userEventQuery = cfg.getValue("general","user_event_auth"); + m_userDeleteQuery = cfg.getValue("general","user_roster_delete"); + m_contactLoadQuery = cfg.getValue("general","contact_load"); + m_contactSubSetQuery = cfg.getValue("general","contact_subscription_set"); + m_contactSetQuery = cfg.getValue("general","contact_set"); + m_contactDeleteQuery = cfg.getValue("general","contact_delete"); + m_genericUserLoadQuery = cfg.getValue("general","generic_roster_load"); + + if (m_userEventQuery) + (new ExpireThread())->startup(); + + // Install relays + setup(); + installRelay(Halt); + installRelay(ImRoute); + // Install handlers + for (const TokenDict* d = s_msgHandler; d->token; d++) { + if (d->value == SubMessageHandler::CallCdr && !m_userEventQuery) + continue; + SubMessageHandler* h = new SubMessageHandler(d->value); + Engine::install(h); + m_handlers.append(h); + } + } +} + +// Enqueue a resource.notify for a given instance +// data: optional data used to override instance's data +void SubscriptionModule::notify(bool online, const String& from, const String& to, + const String& fromInst, const String& toInst, const char* data) +{ + const char* what = online ? "online" : "offline"; + Debug(this,DebugAll,"notify=%s notifier=%s (%s) subscriber=%s (%s)", + what,from.c_str(),fromInst.c_str(),to.c_str(),toInst.c_str()); + Message* m = message("resource.notify"); + m->addParam("operation",what); + m->addParam("from",from); + m->addParam("to",to); + if (fromInst) + m->addParam("from_instance",fromInst); + if (toInst) + m->addParam("to_instance",toInst); + if (!TelEngine::null(data)) + m->addParam("data",data); + Engine::enqueue(m); +} + +// Notify (un)subscribed +void SubscriptionModule::subscribed(bool sub, const String& from, const String& to) +{ + Debug(this,DebugAll,"subscribed(%s) from=%s to=%s", + String::boolText(sub),from.c_str(),to.c_str()); + Message* m = message("resource.notify"); + m->addParam("operation",sub ? "subscribed" : "unsubscribed"); + m->addParam("from",from); + m->addParam("to",to); + Engine::enqueue(m); +} + +// Enqueue a resource.subscribe +void SubscriptionModule::subscribe(bool sub, const String& from, const String& to, + const String* instance) +{ + const char* what = sub ? "subscribe" : "unsubscribe"; + Debug(this,DebugAll,"Requesting %s subscriber=%s notifier=%s", + what,from.c_str(),to.c_str()); + Message* m = message("resource.subscribe"); + m->addParam("operation",what); + m->addParam("subscriber",from); + m->addParam("notifier",to); + if (!TelEngine::null(instance)) + m->addParam("instance",*instance); + Engine::enqueue(m); +} + +// Enqueue a resource.notify with operation=probe +void SubscriptionModule::probe(const char* from, const char* to) +{ + Message* m = message("resource.notify"); + m->addParam("operation","probe"); + m->addParam("from",from); + m->addParam("to",to); + Engine::enqueue(m); +} + +// Dispatch a user.roster message with operation 'update' +// Load contact data from database +// Return the database result if requested +Array* SubscriptionModule::notifyRosterUpdate(const char* username, const char* contact, + bool retData, bool sync) +{ + NamedList p(""); + p.addParam("username",username); + p.addParam("contact",contact); + Message* m = buildDb(m_account,m_contactLoadQuery,p); + m = queryDb(m); + Array* data = 0; + if (m && m->getIntValue("rows") >= 1) { + data = static_cast(m->userObject("Array")); + if (data && data->ref()) + m->userData(0); + else + data = 0; + } + TelEngine::destruct(m); + if (!data) + return 0; + + Message* mu = message("user.roster"); + mu->addParam("notify","update"); + mu->addParam("username",username); + mu->addParam("contact.count","1"); + String prefix("contact.1"); + mu->addParam(prefix,contact); + prefix << "."; + // Add contact data + int cols = data->getColumns(); + for (int col = 1; col < cols; col++) { + String* name = YOBJECT(String,data->get(col,0)); + if (TelEngine::null(name) || *name == "username" || *name == "contact") + continue; + String* value = YOBJECT(String,data->get(col,1)); + if (!value) + continue; + mu->addParam(prefix + *name,*value); + } + if (sync) { + Engine::dispatch(mu); + TelEngine::destruct(mu); + } + else + Engine::enqueue(mu); + + if (!retData) + TelEngine::destruct(data); + return data; +} + +// Handle 'resource.subscribe' for messages with event +bool SubscriptionModule::handleResSubscribe(const String& event, const String& subscriber, + const String& notifier, const String& oper,Message& msg) +{ + EventUser* user = 0; + if (oper != "subscribe") { + user = getEventUser(false,notifier,event); + if (!user) + return false; + user->removeContact(subscriber,true); + //TODO should the user be removed????? // Make notification of subscription terminated + return true; + } + if (!askDB(notifier,subscriber,event)) + return false; + user = getEventUser(true,notifier,event); + if (!user) + return false; + user->appendContact(new EventContact(subscriber,msg)); + Message* m = 0; + if (event == "dilaog") { + m = new Message("cdr.query"); + m->addParam("external",notifier); + } + else { + m = new Message("mwi.query"); + m->addParam("subscriber",subscriber); + m->addParam("notifier",notifier); + m->addParam("message-summary.voicenew","0"); + m->addParam("message-summary.voiceold","0"); + } + if (Engine::dispatch(m)) + event == "dilaog" ? user->notify(*m,false) : user->notifyMwi(*m); + else + event == "dilaog" ? user->notify(msg,false) : user->notifyMwi(msg); + TelEngine::destruct(m); + return true; +} + +EventUser* SubscriptionModule::getEventUser(bool create, const String& notifier, + const String& event) +{ + NamedString* p = m_events.getParam(event); + NamedPointer* po = static_cast(p); + if (!po) { + if (!create) + return 0; + po = new NamedPointer(event,new NamedList(event)); + XDebug(this,DebugAll,"Creating List for Event %s",event.c_str()); + m_events.setParam(po); + } + NamedList* eventList = static_cast(po->userData()); + // Find Notifier list + NamedString* ns = eventList->getParam(notifier); + NamedPointer* np = static_cast(ns); + if (!np) { + if (!create) + return 0; + np = new NamedPointer(notifier,new EventUser(notifier)); + XDebug(this,DebugAll,"Creating user %s for Event %s",notifier.c_str(),event.c_str()); + eventList->setParam(np); + } + + return static_cast(np->userData()); +} + +bool SubscriptionModule::askDB(const String& subscriber, const String& notifier, + const String& oper) +{ + if (subscriber) + return true; + NamedList nl(""); + nl.setParam("subscriber",subscriber); + nl.setParam("notifier",notifier); + nl.setParam("operation",oper); + Message* m = buildDb(m_account,m_userEventQuery,nl); + if (!m) + return false; + m = queryDb(m); + bool ok = m != 0; + TelEngine::destruct(m); + return ok; +} + +void SubscriptionModule::handleCallCdr(const Message& msg, const String& notif) +{ + DDebug(this,DebugAll,"handleCallCdr() notifier=%s",notif.c_str()); + // TODO: lock!!!!!!!!!!! + EventUser* user = getEventUser(false,notif,"dialog"); + if (user) + user->notify(msg); + PresenceUser* pu = 0; + m_users.lock(); + for (ObjList* o = m_users.users().skipNull(); o; o = o->skipNext()) { + pu = static_cast(o->get()); + if (pu->user().substr(0,pu->user().find("@")) == notif) { + pu->ref(); + break; + } + pu = 0; + } + m_users.unlock(); + if (!pu) + return; + pu->notify(msg); + TelEngine::destruct(pu); +} + +void SubscriptionModule::handleMwi(const Message& msg) +{ + EventUser* user = getEventUser(false,msg.getValue("notifier"),"message-summary"); + if (user) + user->notifyMwi(msg); +} +// Handle 'resource.subscribe' messages with (un)subscribe operation +bool SubscriptionModule::handleResSubscribe(bool sub, const String& subscriber, + const String& notifier, Message& msg) +{ + DDebug(this,DebugAll,"handleResSubscribe(%s) subscriber=%s notifier=%s", + String::boolText(sub),subscriber.c_str(),notifier.c_str()); + // Check if the subscriber and/or notifier are in the list (our server) + PresenceUser* from = m_users.getUser(subscriber); + PresenceUser* to = m_users.getUser(notifier); + bool rsp = false; + + // Process the subscriber's state. Use a while() to break + while (from) { + Lock lock(from); + Contact* c = from->findContact(notifier); + Message* m = 0; + bool newContact = (c == 0); + if (c) { + if (sub) { + // Subscription request + // Not subscribed: remember pending out request + // Subscribed: reset pending out flag if set + if (c->m_subscription.to() == c->m_subscription.pendingOut()) { + if (!c->m_subscription.to()) + c->m_subscription.set(SubscriptionState::PendingOut); + else + c->m_subscription.reset(SubscriptionState::PendingOut); + m = c->buildUpdateDb(subscriber); + } + } + else { + // Subscription termination request + bool changed = c->m_subscription.to() || c->m_subscription.pendingOut(); + // Make sure the 'To' and 'PendingOut' are not set + c->m_subscription.reset(SubscriptionState::To | SubscriptionState::PendingOut); + if (changed) + m = c->buildUpdateDb(subscriber); + } + } + else { + if (sub) { + // Add 'notifier' to the contact list if subscription is requested + // TODO: Check credentials + c = new Contact(notifier,SubscriptionState::PendingOut); + m = c->buildUpdateDb(subscriber,true); + } + if (!c) + break; + } + lock.drop(); + if (m) + m = queryDb(m); + if (m) { + bool ok = true; + if (newContact) { + // Append the new contact. Check if not already added while not locked + Lock lck(from); + ok = !from->findContact(notifier); + if (ok) + from->appendContact(c); + else + TelEngine::destruct(c); + } + // Notify changes + if (ok) + notifyRosterUpdate(subscriber,notifier); + TelEngine::destruct(m); + } + break; + } + // Process the notifier's state. Use a while() to break + while (to) { + Lock lock(to); + Contact* c = to->findContact(subscriber); + if (!c) + break; + Message* m = 0; + bool unsubscribed = !sub && c->m_subscription.from(); + rsp = !sub || c->m_subscription.from(); + if (sub) { + // Subscription request + // Not subscribed: remember pending in request + // Subscribed: reset pending in flag if set + if (c->m_subscription.from() == c->m_subscription.pendingIn()) { + if (!c->m_subscription.from()) + c->m_subscription.set(SubscriptionState::PendingIn); + else + c->m_subscription.reset(SubscriptionState::PendingIn); + m = c->buildUpdateDb(notifier); + } + } + else { + if (c->m_subscription.from() || c->m_subscription.pendingIn()) { + c->m_subscription.reset(SubscriptionState::From | SubscriptionState::PendingIn); + m = c->buildUpdateDb(notifier); + } + } + lock.drop(); + if (m) + TelEngine::destruct(queryDb(m)); + // Notify subscription change and 'offline' + if (unsubscribed) { + notify(false,notifier,subscriber); + notifyRosterUpdate(notifier,subscriber); + } + // Respond on behalf of the notifier + if (rsp) { + // Internally handle the message before sending it if the destination was found + // (update destination data) + if (from) { + Message tmp("resource.notify"); + handleResNotifySub(sub,notifier,subscriber,tmp); + } + subscribed(sub,notifier,subscriber); + } + break; + } + TelEngine::destruct(from); + TelEngine::destruct(to); + return rsp; +} + +// Handle 'resource.subscribe' messages with query operation +bool SubscriptionModule::handleResSubscribeQuery(const String& subscriber, + const String& notifier, Message& msg) +{ + DDebug(this,DebugAll,"handleResSubscribeQuery() subscriber=%s notifier=%s", + subscriber.c_str(),notifier.c_str()); + if (subscriber == notifier) + return true; + bool ok = false; + // Check generic users + GenericUser* gu = m_genericUsers.findUser(notifier); + if (gu) { + gu->lock(); + GenericContact* c = gu->find(subscriber); + ok = c != 0; + gu->unlock(); + TelEngine::destruct(gu); + if (ok) + return true; + } + PresenceUser* u = m_users.getUser(notifier); + if (u) { + u->lock(); + Contact* c = u->findContact(subscriber); + ok = c && c->m_subscription.from(); + u->unlock(); + TelEngine::destruct(u); + } + DDebug(this,DebugInfo,"handleResSubscribeQuery() subscriber=%s notifier=%s auth=%u", + subscriber.c_str(),notifier.c_str(),ok); + return ok; +} + +// Handle online/offline resource.notify from contact +bool SubscriptionModule::handleResNotify(bool online, Message& msg) +{ + String* contact = msg.getParam("contact"); + if (TelEngine::null(contact)) { + // TODO: handle generic users + // TODO: handle offline without 'to' or without instance + if (!msg.getBoolValue("to_local",true)) + return false; + String* inst = msg.getParam("from_instance"); + if (TelEngine::null(inst)) + return false; + String* from = msg.getParam("from"); + String* to = msg.getParam("to"); + if(TelEngine::null(from) || TelEngine::null(to)) + return false; + DDebug(this,DebugAll,"handleResNotify(%s) from=%s instance=%s to=%s", + String::boolText(online),from->c_str(),inst->c_str(),to->c_str()); + PresenceUser* u = m_users.getUser(*to); + if (!u) + return false; + u->lock(); + Contact* c = u->findContact(*from); + if (c) { + if (online) { + Instance* i = c->m_instances.set(*inst,msg.getIntValue("priority")); + String* capsid = msg.getParam("caps.id"); + if (!TelEngine::null(capsid)) + i->setCaps(*capsid,msg); + } + else + c->m_instances.remove(*inst); + } + u->unlock(); + TelEngine::destruct(u); + return false; + } + String* inst = msg.getParam("instance"); + DDebug(this,DebugAll,"handleResNotify(%s) contact=%s instance=%s", + String::boolText(online),contact->c_str(),TelEngine::c_safe(inst)); + PresenceUser* u = m_users.getUser(*contact); + if (!u) + return false; + u->lock(); + bool notify = false; + bool newInstance = false; + if (online) { + // Update/add instance. Set notify + if (!TelEngine::null(inst)) { + notify = true; + int prio = msg.getIntValue("priority"); + Instance* i = u->instances().set(*inst,prio,&newInstance); + String* capsid = msg.getParam("caps.id"); + if (!TelEngine::null(capsid)) + i->setCaps(*capsid,msg); + if (newInstance) + DDebug(this,DebugAll,"handleResNotify(online) user=%s added instance=%s prio=%d", + contact->c_str(),inst->c_str(),prio); + } + } + else { + // Remove instance or clear the list + if (!TelEngine::null(inst)) { + Instance* i = u->instances().removeInstance(*inst,false); + if (i) { + notify = true; + DDebug(this,DebugAll,"handleResNotify(offline) user=%s removed instance=%s", + contact->c_str(),inst->c_str()); + TelEngine::destruct(i); + } + } + else { + notify = (0 != u->instances().skipNull()); + if (notify) { + DDebug(this,DebugAll,"handleResNotify(offline) user=%s removed %u instances", + contact->c_str(),u->instances().count()); + u->instances().clear(); + } + } + } + if (notify) { + const char* data = msg.getValue("data"); + // Notify contacts (from user) and new online user (from contacts) + // Send pending in subscription requests to user's new instance + // Re-send pending out subscription requests each time a new instance is notified + for (ObjList* o = u->m_list.skipNull(); o; o = o->skipNext()) { + Contact* c = static_cast(o->get()); + if (newInstance && c->m_subscription.pendingIn()) + subscribe(true,c->toString(),u->toString(),inst); + bool fromContact = newInstance && c->m_subscription.to(); + bool pendingOut = !fromContact && newInstance && c->m_subscription.pendingOut(); + if (!(c->m_subscription.from() || fromContact || pendingOut)) + continue; + PresenceUser* dest = m_users.getUser(*c); + if (!dest) { + // User not found, it may belong to other domain + // Send presence and probe it if our user is online + if (c->m_subscription.from()) { + if (online) + __plugin.notify(true,u->toString(),*c,*inst,String::empty(),data); + else + __plugin.notify(false,u->toString(),*c,inst ? *inst : String::empty()); + } + if (online) { + probe(u->toString(),*c); + if (pendingOut) + subscribe(true,u->toString(),c->toString()); + } + continue; + } + dest->lock(); + // Notify user's instance to all contact's instances + if (c->m_subscription.from()) + dest->instances().notifyInstance(online,false,u->toString(), + dest->toString(),inst ? *inst : String::empty(),data); + // Notify all contact's instances to the new user's instance + if (fromContact) + dest->instances().notifyUpdate(online,dest->toString(), + u->toString(),*inst); + else if (pendingOut) { + // Both parties are known: handle pending out internally + Message tmp("resource.subscribe"); + handleResSubscribe(true,u->toString(),c->toString(),tmp); + } + dest->unlock(); + TelEngine::destruct(dest); + } + // Notify the instance to all other user's instance + // Notify a new instance about other user's instances + if (!TelEngine::null(inst)) { + u->instances().notifySkip(online,false,u->toString(),*inst,data); + if (newInstance && online) + u->instances().notifySkip(online,true,u->toString(),*inst,data); + } + } + u->unlock(); + TelEngine::destruct(u); + return false; +} + +// Handle resource.notify with operation (un)subscribed +bool SubscriptionModule::handleResNotifySub(bool sub, const String& src, const String& dest, + Message& msg) +{ + DDebug(this,DebugAll,"handleResNotifySub(%s,%s,%s)", + String::boolText(sub),src.c_str(),dest.c_str()); + + PresenceUser* from = m_users.getUser(src); + PresenceUser* to = m_users.getUser(dest); + while (from) { + Lock lock(from); + Contact* c = from->findContact(dest); + bool notify = false; + // Add it to the list if subscribed and not found + if (!c && sub) { + c = new Contact(dest,SubscriptionState::From); + Message* m = c->buildUpdateDb(src,true); + m = queryDb(m); + if (m) { + from->appendContact(c); + TelEngine::destruct(m); + notify = true; + } + else + TelEngine::destruct(c); + } + if (!c) + break; + bool changed = c->m_subscription.pendingIn(); + c->m_subscription.reset(SubscriptionState::PendingIn); + if (sub) { + if (!c->m_subscription.from()) { + c->m_subscription.set(SubscriptionState::From); + changed = true; + notify = true; + } + } + else { + if (c->m_subscription.from()) { + c->m_subscription.reset(SubscriptionState::From); + changed = true; + notify = true; + } + } + Message* m = 0; + if (changed) + m = c->buildUpdateDb(src); + lock.drop(); + m = queryDb(m); + // Notify user roster change on success + if (m) { + TelEngine::destruct(m); + if (notify) + notifyRosterUpdate(src,dest); + } + // Notify user presence to contact if subscribed to its presence + if (notify) { + if (to) { + Lock2 lck(from,to); + notifyInstances(sub,*from,*to); + } + else + probe(src,dest); + } + break; + } + while (to) { + Lock lock(to); + Contact* c = to->findContact(src); + if (!c) + break; + bool changed = c->m_subscription.test(SubscriptionState::PendingOut); + c->m_subscription.reset(SubscriptionState::PendingOut); + bool notify = !sub && changed; + if (sub) { + if (!c->m_subscription.to()) { + c->m_subscription.set(SubscriptionState::To); + changed = true; + notify = true; + } + } + else { + if (c->m_subscription.to()) { + c->m_subscription.reset(SubscriptionState::To); + changed = true; + notify = true; + } + } + Message* m = 0; + if (changed) + m = c->buildUpdateDb(dest); + bool subscribed = c->m_subscription.to(); + lock.drop(); + m = queryDb(m); + // Notify user roster change on success + if (m) { + TelEngine::destruct(m); + if (notify) + notifyRosterUpdate(dest,src); + } + // Notify user presence to contact if subscribed to its presence + if (notify && subscribed) { + if (from) { + Lock2 lck(to,from); + notifyInstances(sub,*to,*from); + } + else + probe(dest,src); + } + break; + } + TelEngine::destruct(from); + TelEngine::destruct(to); + return false; +} + +// Handle resource.notify with operation probe +bool SubscriptionModule::handleResNotifyProbe(const String& from, const String& to, + Message& msg) +{ + bool toLocal = msg.getBoolValue("to_local"); + DDebug(this,DebugAll,"handleResNotifyProbe(%s,%s) toLocal=%u", + from.c_str(),to.c_str(),toLocal); + const String* src = 0; + const String* dest = 0; + if (toLocal) { + src = &from; + dest = &to; + } + else { + src = &to; + dest = &from; + } + PresenceUser* user = m_users.getUser(*dest); + if (!user) + return false; + user->lock(); + bool ok = false; + Contact* c = 0; + if (from != to) { + c = user->findContact(*src); + ok = c && c->m_subscription.from(); + } + else + ok = true; + bool sync = msg.getBoolValue("sync"); + if (ok) { + if (sync) { + unsigned int n = 0; + if (toLocal) + n = user->instances().addListParam(msg); + else if (c) + n = c->m_instances.addListParam(msg); + msg.setParam("instance.count",String(n)); + } + else { + String* inst = msg.getParam("from_instance"); + user->instances().notifyUpdate(true,*dest,*src,inst ? *inst : String::empty()); + } + } + user->unlock(); + TelEngine::destruct(user); + return ok || sync; +} + +// Update capabilities for all instances with the given caps id +void SubscriptionModule::updateCaps(const String& capsid, NamedList& list) +{ + m_users.lock(); + for (ObjList* o = m_users.users().skipNull(); o; o = o->skipNext()) { + PresenceUser* u = static_cast(o->get()); + u->instances().updateCaps(capsid,list); + for (ObjList* c = u->m_list.skipNull(); c; c = c->skipNext()) + (static_cast(c->get()))->m_instances.updateCaps(capsid,list); + } + m_users.unlock(); + // TODO: handle generic users +} + + +// Handle 'user.roster' messages with operation 'query' +bool SubscriptionModule::handleUserRosterQuery(const String& user, const String* contact, + Message& msg) +{ + DDebug(this,DebugAll,"handleUserRosterQuery() user=%s contact=%s", + user.c_str(),TelEngine::c_safe(contact)); + Message* m = 0; + NamedList p(""); + p.addParam("username",user); + if (TelEngine::null(contact)) + m = buildDb(m_account,m_userLoadQuery,p); + else { + p.addParam("contact",*contact); + m = buildDb(m_account,m_contactLoadQuery,p); + } + m = queryDb(m); + if (!m) + return false; + Array* a = 0; + if (m->getIntValue("rows") >= 1) + a = static_cast(m->userObject("Array")); + unsigned int n = 0; + if (a) { + int rows = a->getRows(); + int cols = a->getColumns(); + for (int row = 1; row < rows; row++) { + String cPrefix("contact."); + cPrefix << String(++n); + String prefix(cPrefix); + prefix << "."; + for (int col = 1; col < cols; col++) { + String* name = YOBJECT(String,a->get(col,0)); + if (!name || *name == "username") + continue; + String* value = YOBJECT(String,a->get(col,row)); + if (!value) + continue; + if (*name == "contact") + msg.addParam(cPrefix,*value); + else + msg.addParam(prefix + *name,*value); + } + } + } + if (n) + msg.addParam("contact.count",String(n)); + TelEngine::destruct(m); + return true; +} + +// Handle 'user.roster' messages with operation 'update' +bool SubscriptionModule::handleUserRosterUpdate(const String& user, const String& contact, + Message& msg) +{ + DDebug(this,DebugAll,"handleUserRosterUpdate() user=%s contact=%s", + user.c_str(),contact.c_str()); + NamedList p(""); + String params("username,contact"); + NamedString* cParams = msg.getParam("contact.parameters"); + if (!TelEngine::null(cParams)) + params.append(*cParams,","); + p.copyParams(msg,params); + Message* m = buildDb(m_account,m_contactSetQuery,p); + m = queryDb(m); + if (!m) + return false; + + // Load the contact to get all its data + // The data will be used to notify changes and handle contact + // subscription related notifications + // Notify the update before notifying the instances + Array* contactData = notifyRosterUpdate(user,contact,true); + if (!contactData) + return true; + + // Find the user and (re)load the contact (its subscription might change or + // it can be a new contact) + PresenceUser* u = m_users.getUser(user); + if (!u) { + TelEngine::destruct(contactData); + return true; + } + u->lock(); + SubscriptionState oldSub; + Contact* c = u->findContact(contact); + bool newContact = (c == 0); + if (c) { + oldSub.replace((int)c->m_subscription); + c->set(*contactData,1); + } + else { + c = Contact::build(*contactData,1); + if (c) + u->appendContact(c); + } + TelEngine::destruct(contactData); + // Notify instances + if (c) { + PresenceUser* dest = m_users.getUser(contact); + Lock lock(dest); + bool doProbe = false; + // To contact if it's subscribed to user's presence and it's new one + // or subscription changed + if (c->m_subscription.from() && (newContact || !oldSub.from())) { + if (dest) { + if (dest->instances().skipNull() && u->instances().skipNull()) + u->instances().notifyUpdate(true,user,contact,dest->instances()); + } + else + doProbe = true; + } + // From contact to user + if (c->m_subscription.to()) { + if (newContact) + doProbe = (dest == 0); + else if (!oldSub.to()) { + if (dest) { + if (dest->instances().skipNull() && u->instances().skipNull()) + dest->instances().notifyUpdate(true,contact,user,u->instances()); + } + else + doProbe = true; + } + } + lock.drop(); + TelEngine::destruct(dest); + if (doProbe && c->m_subscription.to()) + probe(user,contact); + } + u->unlock(); + TelEngine::destruct(u); + return true; +} + +// Handle 'user.roster' messages with operation 'delete' +bool SubscriptionModule::handleUserRosterDelete(const String& user, const String& contact, + Message& msg) +{ + DDebug(this,DebugAll,"handleUserRosterDelete() user=%s contact=%s", + user.c_str(),contact.c_str()); + Message* m = buildDb(m_account,m_contactDeleteQuery,msg); + m = queryDb(m); + if (!m) + return false; + TelEngine::destruct(m); + // Find the user before notifying the operation: notify instances before remove + PresenceUser* u = m_users.getUser(user); + if (u) { + u->lock(); + Contact* c = u->removeContact(contact,false); + if (c) { + // Notify 'offline' to both parties + if (c->m_subscription.to()) + notify(false,contact,user); + if (c->m_subscription.from()) + notify(false,user,contact); + // Contact is a known user: update user subscription in it's list and + // notify it if it has any instances + // Unknown user: unsubcribe it and request unsubscribe + PresenceUser* uc = m_users.getUser(contact); + if (uc) { + uc->lock(); + Contact* cc = uc->findContact(user); + if (cc) { + int flgs = SubscriptionState::From | SubscriptionState::To | + SubscriptionState::PendingOut; + bool update = cc->m_subscription.test(flgs); + bool changed = update || cc->m_subscription.pendingIn(); + cc->m_subscription.reset(flgs | SubscriptionState::PendingIn); + // Save data before update notification (use saved data in notification) + if (changed) { + Message* m = cc->buildUpdateDb(contact); + TelEngine::destruct(queryDb(m)); + } + if (update) + notifyRosterUpdate(contact,user,false,false); + } + uc->unlock(); + TelEngine::destruct(uc); + } + else { + subscribed(false,user,contact); + subscribe(false,user,contact); + } + TelEngine::destruct(uc); + TelEngine::destruct(c); + } + u->unlock(); + TelEngine::destruct(u); + } + Message* mu = message("user.roster"); + mu->addParam("notify","delete"); + mu->addParam("username",user); + mu->addParam("contact",contact); + Engine::enqueue(mu); + return true; +} + +// Handle 'user.update' messages with operation 'delete' +void SubscriptionModule::handleUserUpdateDelete(const String& user, Message& msg) +{ + DDebug(this,DebugAll,"handleUserUpdateDelete() user=%s",user.c_str()); + PresenceUser* u = m_users.getUser(user); + if (!u) + return; + u->lock(); + for (ObjList* o = u->m_list.skipNull(); o; o = o->skipNext()) { + Contact* c = static_cast(o->get()); + if (c->m_subscription.from()) + notify(false,user,c->toString()); + } + u->unlock(); + TelEngine::destruct(u); + // Remove the user from memory and database roster + m_users.removeUser(user); + NamedList p(""); + p.addParam("username",user); + Message* m = buildDb(m_account,m_userDeleteQuery,p); + TelEngine::destruct(queryDb(m)); +} + +// Handle 'msg.route' messages +bool SubscriptionModule::imRoute(Message& msg) +{ + String* caller = msg.getParam("caller"); + String* called = msg.getParam("called"); + if (TelEngine::null(caller) || TelEngine::null(called)) + return false; + DDebug(this,DebugAll,"%s caller=%s called=%s", + msg.c_str(),caller->c_str(),called->c_str()); + PresenceUser* u = m_users.getUser(*called); + if (!u) { + Debug(this,DebugStub,"%s caller=%s called=%s destination is an unknown user", + msg.c_str(),caller->c_str(),called->c_str()); + return false; + } + bool ok = true; + unsigned int n = 0; + u->lock(); + String* tmp = msg.getParam("called_instance"); + if (TelEngine::null(tmp)) { + String* skip = 0; + if (*caller == *called) + skip = msg.getParam("caller_instance"); + else if (!u->findContact(*caller)) + ok = false; + if (ok) + n = u->instances().addListParam(msg,skip); + } + else if (u->findContact(*caller) || *caller == *called) { + Instance* inst = u->instances().findInstance(*tmp); + if (inst) + inst->addListParam(msg,++n); + } + else + ok = false; + u->unlock(); + TelEngine::destruct(u); + if (ok) + msg.addParam("instance.count",String(n)); + return ok && n != 0; +} + +void SubscriptionModule::expireSubscriptions() +{ + u_int64_t time = Time::msecNow(); + unsigned int evCount = m_events.count(); + for (unsigned int i = 0;i < evCount;i ++) { + NamedPointer* p = static_cast(m_events.getParam(i)); + NamedList* nl = static_cast(p->userData()); + if (!nl) + continue; + unsigned int nlCount = nl->count(); + for (unsigned int j = 0;j < nlCount;j++) { + NamedPointer* p1 = static_cast(nl->getParam(j)); + if (!p1) + continue; + EventUser* eu = static_cast(p1->userData()); + if (!eu) + continue; + eu->expire(time); + if (eu->m_list.count() == 0) { + nl->clearParam(eu->user()); + j--; + } + } + } +} + +// Build a database message from account and query. +// Replace query params. Return Message pointer on success +Message* SubscriptionModule::buildDb(const String& account, const String& query, + const NamedList& params) +{ + XDebug(this,DebugAll,"buildDb(%s,%s)",account.c_str(),query.c_str()); + if (!(account && query)) + return 0; + Message* m = new Message("database"); + m->addParam("account",account); + String tmp = query; + params.replaceParams(tmp,true); + m->addParam("query",tmp); + return m; +} + +// Dispatch a database message +// Return Message pointer on success. Release msg on failure +Message* SubscriptionModule::queryDb(Message*& msg) +{ + if (!msg) + return 0; + bool ok = Engine::dispatch(msg) && !msg->getParam("error"); + if (!ok) { + Debug(this,DebugNote,"Database query=%s failed error=%s", + msg->getValue("query"),msg->getValue("error")); + TelEngine::destruct(msg); + } + return msg; +} + +bool SubscriptionModule::received(Message& msg, int id) +{ + switch (id) { + case Timer: + s_check = true; + break; + case ImRoute: + return imRoute(msg); + case Halt: + Lock lock(this); + if (m_expire) + m_expire->cancel(false); + lock.drop(); + while (m_expire) + Thread::yield(); + // Uninstall message handlers + for (ObjList* o = m_handlers.skipNull(); o; o = o->skipNext()) { + SubMessageHandler* h = static_cast(o->get()); + Engine::uninstall(h); + } + DDebug(this,DebugAll,"Halted"); + break; + } + return Module::received(msg,id); +} + +bool SubscriptionModule::commandExecute(String& retVal, const String& line) +{ + String l = line; + l.startSkip(name()); + l.trimSpaces(); + if (l.startSkip("status")) { + l.trimSpaces(); + String user = ""; +// extractName(l,user); + String contact = ""; +// extractName(l.substr(user.length() + 2,-1),contact); + if (user.null() || contact.null()) { + retVal << "Espected pair"; + DDebug(this,DebugInfo,"Command Execute 2 : return false user->null() || contact->null()"); + return false; + } + DDebug(this,DebugInfo,"Command Execute , operation status for: %s, to %s",user.c_str(),contact.c_str()); +// retVal << "Subscription state for user: " << user << " and contact: " +// << contact << " is: " << m_users.getSubscription(user,contact); + return true; + } + if (l.startSkip("unsubscribe")) { + l.trimSpaces(); + String* contact = new String(); + String* user = new String(); + ObjList* ob = l.split(' ',false); + int counter = 0; + for (ObjList* o = ob->skipNull(); o; o = o->skipNext()) { + switch (counter) { + case 0: + user = static_cast(o->get()); + break; + case 1: + contact = static_cast(o->get()); + break; + default: + retVal << "Espected pair"; + return false; + } + counter += 1; + } + if (user->null() || contact->null()) { + retVal << "Espected pair"; + return false; + } + // TODO unsubscribe the user + retVal << "PresenceUser: " << *user << " succesfuly unsubscribed from " << *contact << "'s presence"; + } + return false; +} + +bool SubscriptionModule::commandComplete(Message& msg, const String& partLine, + const String& partWord) +{ + if (partLine.null() && partWord.null()) + return false; + if (partLine.null() || (partLine == "help")) + Module::itemComplete(msg.retValue(),name(),partWord); + else if (partLine == name()) { + for (const char** list = s_cmds; *list; list++) + Module::itemComplete(msg.retValue(),*list,partWord); + return true; + } + + return Module::commandComplete(msg,partLine,partWord); +} + +// Notify 'from' instances to 'to' +void SubscriptionModule::notifyInstances(bool online, PresenceUser& from, PresenceUser& to) +{ + if (!to.instances().skipNull()) + return; + // Source has instances: notify them to destination + // Source has no instance: notify offline to destination + if (from.instances().skipNull()) { + if (online || !s_singleOffline) + from.instances().notifyUpdate(online,from.toString(),to.toString(),to.instances()); + else + notify(false,from.toString(),to.toString()); + } + else if (online) + to.instances().notifyInstance(false,false,from.toString(),to.toString(),String::empty(),0); +} + +} /* anonymous namespace */ + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/server/users.cpp b/modules/server/users.cpp new file mode 100644 index 00000000..c0bcea80 --- /dev/null +++ b/modules/server/users.cpp @@ -0,0 +1,474 @@ +/** + * users.cpp + * This file is part of the YATE Project http://YATE.null.ro + * + * Users module + * + * Yet Another Telephony Engine - a fully featured software PBX and IVR + * Copyright (C) 2004-2009 Null Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include + +using namespace TelEngine; + +namespace { + +class UsersModule; +class UserUpdateHandler; + +/** + * Class UserUpdateHandler + * Handles a user.update message + */ +class UserUpdateHandler : public MessageHandler +{ +public: + inline UserUpdateHandler(unsigned int priority = 100) + : MessageHandler("user.update", priority) + { } + virtual ~UserUpdateHandler() + { } + virtual bool received(Message& msg); +}; + + +/** + * Class UsersModule + * Module for handling users operations + */ +class UsersModule : public Module +{ +public: + enum Commands { + CmdUpdate = 1, + CmdAdd, + CmdDelete, + }; + UsersModule(); + virtual ~UsersModule(); + // Check if a message was sent by us + inline bool isModule(const Message& msg) { + String* module = msg.getParam("module"); + return module && *module == name(); + } + // Build a message. Fill the module parameter + inline Message* message(const char* msg) { + Message* m = new Message(msg); + m->addParam("module",name()); + return m; + } + //inherited methods + virtual void initialize(); + // uninstall relays and message handlers + bool unload(); + // User management + bool addUser(const NamedList& params, String* error = 0); + bool deleteUser(const NamedList& params, String* error = 0); + bool updateUser(const NamedList& params, String* error = 0); + bool searchUser(const NamedList& params); + // Notify user changes add/del/update + void notifyUser(const char* user, const char* notify); + // Build and dispatch a database message. Return true on success + bool queryDb(const String& account, const String& query, + const NamedList& params, String& error, bool search = false); + // parse the command line + bool parseParams(const String& line, NamedList& parsed, String& error); + // Module commands + static const TokenDict s_cmds[]; +protected: + // inherited methods + virtual bool received(Message& msg, int id); + virtual bool commandExecute(String& retVal, const String& line); + virtual bool commandComplete(Message& msg, const String& partLine, const String& partWord); +private: + //flag for first time initialization + bool m_init; + // Query strings + // SQL statement for inserting to database + String m_insertDB; + // SQL statement for updating information in the database + String m_updateDB; + String m_removeDB; + // SQL statement for interrogating the database about a contact with a resource + String m_selectDB; + // database connection + String m_accountDB; + + UserUpdateHandler* m_updateHandler; +}; + + +INIT_PLUGIN(UsersModule); + +const TokenDict UsersModule::s_cmds[] = { + {"update", CmdUpdate}, + {"add", CmdAdd}, + {"delete", CmdDelete}, + {0,0} +}; + +static const char* s_cmdsLine = "users {add user [parameter=value...]|delete user|update user [parameter=value...]}"; + +UNLOAD_PLUGIN(unloadNow) +{ + if (unloadNow && !__plugin.unload()) + return false; + return true; +} + +// Get a space separated word from a buffer. msgUnescape() it if requested +// Return false if empty +static bool getWord(String& buf, String& word, bool unescape = false) +{ + XDebug(&__plugin,DebugAll,"getWord(%s)",buf.c_str()); + int pos = buf.find(" "); + if (pos >= 0) { + word = buf.substr(0,pos); + buf = buf.substr(pos + 1); + } + else { + word = buf; + buf = ""; + } + if (!word) + return false; + if (unescape) + word.msgUnescape(); + return true; +} + + +/** + * UserUpdateHandler + */ +// could change +bool UserUpdateHandler::received(Message& msg) +{ + if (__plugin.isModule(msg)) + return false; + // TODO + // see message parameters and what to do with them + String* operation = msg.getParam("operation"); + String* user = msg.getParam("user"); + if (TelEngine::null(operation) || TelEngine::null(user)) { + msg.setParam("error","Mandatory parameters missing"); + return false; + } + + NamedList params(""); + params.addParam("user",*user); + params.copyParams(msg,"password"); + String msgPrefix = msg.getValue("message-prefix"); + if (!msgPrefix.null()) { + msgPrefix << "."; + for (unsigned int i = 0; i < msg.length(); i++) { + NamedString* ns = msg.getParam(i); + if (ns && ns->name().startsWith(msgPrefix)) + params.addParam(ns->name().substr(msgPrefix.length()),*ns); + } + } + bool ok = false; + if (*operation == "add") + ok = __plugin.addUser(params); + else if (*operation == "delete") + ok = __plugin.deleteUser(params); + else if (*operation == "update") + ok = __plugin.updateUser(params); + else + return false; + if (ok) + __plugin.notifyUser(*user,*operation); + else + msg.setParam("error","failure"); + return ok; +} + + +/** + * UsersModule + */ +UsersModule:: UsersModule() + : Module("users","misc"), + m_updateHandler(0) +{ + Output("Loaded module Users Management"); +} + +UsersModule::~UsersModule() +{ + Output("Unloaded module Users Management"); + TelEngine::destruct(m_updateHandler); +} + +void UsersModule::initialize() +{ + Output("Initializing module Users Management"); + + Configuration cfg(Engine::configFile("users")); + cfg.load(); + + if (!m_init) { + m_init = true; + setup(); + installRelay(Halt); + installRelay(Help); + + m_updateHandler = new UserUpdateHandler(); + Engine::install(m_updateHandler); + // queries init + m_insertDB = cfg.getValue("database", "add_user"); + m_updateDB = cfg.getValue("database", "update_user"); + m_removeDB = cfg.getValue("database", "remove_user"); + m_selectDB = cfg.getValue("database", "select_user"); + + // database connection init + m_accountDB = cfg.getValue("database", "account"); + } +} + +bool UsersModule::unload() +{ + DDebug(this,DebugAll,"unload()"); + if (!lock(500000)) + return false; + uninstallRelays(); + Engine::uninstall(m_updateHandler); + unlock(); + return true; +} + +bool UsersModule::addUser(const NamedList& params, String* error) +{ + if (TelEngine::null(m_insertDB)) + return false; + String tmp; + if (!error) + error = &tmp; + if (!searchUser(params)) { + if (queryDb(m_accountDB,m_insertDB,params,*error)) { + Debug(this,DebugAll,"Added user '%s'",params.getValue("user")); + return true; + } + if (error->null()) + *error = "Failure"; + } + else + *error = "Already exists"; + Debug(this,DebugInfo,"Failed to add user '%s' error='%s'", + params.getValue("user"),error->c_str()); + return false; +} + +bool UsersModule::deleteUser(const NamedList& params, String* error) +{ + if (TelEngine::null(m_removeDB)) + return false; + String tmp; + if (!error) + error = &tmp; + if (queryDb(m_accountDB,m_removeDB,params,*error)) { + Debug(this,DebugAll,"Deleted user '%s'",params.getValue("user")); + return true; + } + if (error->null()) + *error = "User not found"; + Debug(this,DebugInfo,"Failed to delete user '%s' error='%s'", + params.getValue("user"),error->c_str()); + return false; +} + +bool UsersModule::updateUser(const NamedList& params, String* error) +{ + if (TelEngine::null(m_updateDB)) + return false; + String tmp; + if (!error) + error = &tmp; + if (queryDb(m_accountDB,m_updateDB,params,*error)) { + Debug(this,DebugAll,"Updated user '%s'",params.getValue("user")); + return true; + } + if (!error) + *error = "User not found"; + Debug(this,DebugInfo,"Failed to update user '%s' error='%s'", + params.getValue("user"),error->c_str()); + return false; +} + +bool UsersModule::searchUser(const NamedList& params) +{ + if (TelEngine::null(m_selectDB)) + return false; + String error; + return queryDb(m_accountDB,m_selectDB,params,error,true); +} + +// Notify user changes add/del/update +void UsersModule::notifyUser(const char* user, const char* notify) +{ + Message* m = __plugin.message("user.update"); + m->addParam("notify",notify); + m->addParam("user",user); + Engine::enqueue(m); +} + +// Build and dispatch a database message. Return true on success +bool UsersModule::queryDb(const String& account, const String& query, + const NamedList& params, String& error, bool search) +{ + Message msg("database"); + msg.addParam("module",name()); + msg.addParam("account", account); + String tmp = query; + params.replaceParams(tmp,true); + msg.addParam("query", tmp); + msg.addParam("results", String::boolText(true)); + bool ok = Engine::dispatch(msg) && !msg.getParam("error"); + if (ok) { + if (query != m_insertDB) { + if (search) + ok = msg.getIntValue("rows") > 1; + else + ok = msg.getIntValue("affected") >= 1; + } + else { + Array* a = static_cast(msg.userObject("Array")); + String* res = a ? YOBJECT(String,a->get(0,1)) : 0; + ok = res && (res->toInteger() != 0); + } + } + if (!ok) + error = msg.getValue("error"); + return ok; +} + +bool UsersModule::parseParams(const String& line, NamedList& parsed, String& error) +{ + Debug(this,DebugAll,"parseParams(%s)",line.c_str()); + bool ok = true; + ObjList* list = line.split(' ',false); + for (ObjList* o = list->skipNull(); o; o = o->skipNext()) { + String* s = static_cast(o->get()); + int pos = s->find("="); + // Empty parameter name is not allowed + if (pos < 1) { + error << "Invalid parameter " << *s; + ok = false; + break; + } + String name = s->substr(0,pos); + String value = s->substr(pos + 1); + name.msgUnescape(); + value.msgUnescape(); + parsed.addParam(name,value); + DDebug(&__plugin,DebugAll,"parseParams() found '%s'='%s'",name.c_str(),value.c_str()); + } + TelEngine::destruct(list); + return ok; +} + +bool UsersModule::received(Message& msg, int id) +{ + if (id == Halt) + unload(); + else if (id == Help) { + String line = msg.getValue("line"); + if (line.null()) { + msg.retValue() << " " << s_cmdsLine << "\r\n"; + return false; + } + if (line != name()) + return false; + msg.retValue() << "Commands used to control the Users Management module\r\n"; + msg.retValue() << s_cmdsLine << "\r\n"; + return true; + } + return Module::received(msg,id); +} + +bool UsersModule::commandExecute(String& retVal, const String& line) +{ + String tmp(line); + if (!tmp.startSkip(name(),false)) + return false; + tmp.trimSpaces(); + XDebug(this,DebugAll,"commandExecute(%s)",tmp.c_str()); + // Retrieve the command + String cmdStr; + int cmd = 0; + if (getWord(tmp,cmdStr)) + cmd = lookup(cmdStr,s_cmds); + if (!cmd) { + retVal << "Unknown command\r\n"; + return true; + } + // Retrieve the user + String user; + if (!getWord(tmp,user,true)) { + retVal << "Empty username\r\n"; + return true; + } + // Execute the command + bool ok = false; + String error; + if (cmd == CmdUpdate || cmd == CmdAdd || cmd == CmdDelete) { + NamedList p(""); + p.addParam("user",user); + if (parseParams(tmp,p,error)) { + if (cmd == CmdUpdate) + ok = updateUser(p,&error); + else if (cmd == CmdAdd) + ok = addUser(p,&error); + else + ok = deleteUser(p,&error); + } + if (ok) + notifyUser(user,cmdStr); + } + else { + Debug(this,DebugStub,"Command '%s' not implemented",cmdStr.c_str()); + error = "Unknown command"; + } + retVal << name() << " " << cmdStr << (ok ? " succedded" : " failed"); + if (!ok && error) + retVal << ". " << error; + retVal << "\r\n"; + return true; +} + +bool UsersModule::commandComplete(Message& msg, const String& partLine, const String& partWord) +{ + if (partLine.null() && partWord.null()) + return false; + XDebug(this,DebugAll,"commandComplete() partLine='%s' partWord=%s", + partLine.c_str(),partWord.c_str()); + // No line or 'help': complete module name + if (partLine.null() || partLine == "help") + return Module::itemComplete(msg.retValue(),name(),partWord); + // Line is module name: complete module commands + if (partLine == name()) { + for (const TokenDict* list = s_cmds; list->token; list++) + Module::itemComplete(msg.retValue(),list->token,partWord); + return true; + } + return Module::commandComplete(msg,partLine,partWord); +} + +} + +/* vi: set ts=8 sw=4 sts=4 noet: */ diff --git a/modules/yjinglechan.cpp b/modules/yjinglechan.cpp index 3b87e16a..2d432f81 100644 --- a/modules/yjinglechan.cpp +++ b/modules/yjinglechan.cpp @@ -23,6 +23,14 @@ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ + +/* +============================================================================ +TODO: + Check SRTP handling. Check if secure (mandatory) is handled properly +============================================================================ +*/ + #include #include #include @@ -39,148 +47,42 @@ using namespace TelEngine; namespace { // anonymous -class YJBEngine; // Jabber engine. Initiate protocol from Yate run mode -class YJGEngine; // Jingle service -class YJBMessage; // Message service -class YJBStreamService; // Stream start/stop event service -class YJBClientPresence; // Presence service for client streams -class YJBPresence; // Presence service -class YJBIqService; // Handle 'iq' stanzas not processed by other services +class YJGEngine; // Jingle engine +class YJGEngineWorker; // Jingle engine worker class YJGConnection; // Jingle channel class YJGTransfer; // Transfer thread (route and execute) -class ResNotifyHandler; // resource.notify handler -class ResSubscribeHandler; // resource.subscribe handler -class UserLoginHandler; // user.login handler -class XmppGenerateHandler; // xmpp.generate handler -class XmppIqHandler; // xmpp.iq handler used to respond to unprocessed set/get stanzas +class YJGMessageHandler; // Module message handlers class YJGDriver; // The driver -// TODO: -// Negotiate DTMF method. Accept remote peer's method; - // URI #define BUILD_XMPP_URI(jid) (plugin.name() + ":" + jid) -/** - * YJBEngine - */ -class YJBEngine : public JBEngine -{ -public: - inline YJBEngine(Protocol proto) : JBEngine(proto) - {} - virtual bool exiting() const - { return Engine::exiting(); } - void initialize(); - // Setup the transport layer security for a stream - virtual bool encryptStream(JBStream* stream); -}; - -/** +/* * YJGEngine */ class YJGEngine : public JGEngine { public: - inline YJGEngine(YJBEngine* engine, int prio) - : JGEngine(engine,0,prio), m_requestSubscribe(true) - {} - inline bool requestSubscribe() const - { return m_requestSubscribe; } - void initialize(); + // Send a session's stanza (dispatch a jabber.iq message) + virtual bool sendStanza(JGSession* session, XmlElement*& stanza); + // Event processor virtual void processEvent(JGEvent* event); -private: - bool m_requestSubscribe; // Request subscribe before making a call }; -/** - * YJBMessage +/* + * YJGEngineWorker */ -class YJBMessage : public JBMessage +class YJGEngineWorker : public Thread { public: - inline YJBMessage(YJBEngine* engine, int prio) - : JBMessage(engine,0,prio) + inline YJGEngineWorker(Thread::Priority prio = Thread::Normal) + : Thread("YJGEngineWorker",prio) {} - void initialize(); - virtual void processMessage(JBEvent* event); + virtual void run(); }; -/** - * YJBStreamService - */ -class YJBStreamService : public JBService -{ -public: - YJBStreamService(JBEngine* engine, int prio) - : JBService(engine,"jabberstreamservice",0,prio) - {} - virtual ~YJBStreamService() - {} - void initialize(); -protected: - // Process stream termination events - virtual bool accept(JBEvent* event, bool& processed, bool& insert); -}; - -/** - * YJBClientPresence - */ -class YJBClientPresence : public JBService -{ -public: - YJBClientPresence(JBEngine* engine, int prio) - : JBService(engine,"clientpresence",0,prio) - {} - virtual ~YJBClientPresence() - {} - void initialize(); -protected: - // Process stream termination events - virtual bool accept(JBEvent* event, bool& processed, bool& insert); -}; - -/** - * YJBPresence - */ -class YJBPresence : public JBPresence -{ - friend class YUserPresence; -public: - inline YJBPresence(JBEngine* engine, int prio) - : JBPresence(engine,0,prio) - {} - void initialize(); - // Overloaded methods - virtual bool notifyProbe(JBEvent* event); - virtual bool notifySubscribe(JBEvent* event, Presence presence); - virtual void notifySubscribe(XMPPUser* user, Presence presence); - virtual bool notifyPresence(JBEvent* event, bool available); - virtual void notifyPresence(XMPPUser* user, JIDResource* resource); - virtual void notifyNewUser(XMPPUser* user); - // Create & enqueue a message from received presence parameter. - // Add status/operation/subscription parameters - static Message* message(int presence, const char* from, const char* to, - const char* subscription); -}; - -/** - * YJBIqService - */ -class YJBIqService : public JBService -{ -public: - YJBIqService(JBEngine* engine, int prio) - : JBService(engine,"jabberiqservice",0,prio) - {} - void initialize(); -protected: - // Process iq events - virtual bool accept(JBEvent* event, bool& processed, bool& insert); -}; - -/** +/* * YJGConnection */ class YJGConnection : public Channel @@ -216,7 +118,7 @@ public: }; // Outgoing constructor YJGConnection(Message& msg, const char* caller, const char* called, bool available, - const char* file); + const NamedList& caps, const char* file); // Incoming contructor YJGConnection(JGEvent* event); virtual ~YJGConnection(); @@ -263,20 +165,17 @@ public: // Process Jingle and Terminated events // Return false to terminate bool handleEvent(JGEvent* event); - void hangup(const char* reason, const char* text = 0); + void hangup(const char* reason = 0, const char* text = 0); // 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); + 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); - // Get the remote party address (actually this is the address of the - // local party's server) - void getRemoteAddr(String& dest); // Process chan.notify messages // Handle SOCKS status changes for file transfer bool processChanNotify(Message& msg); @@ -384,6 +283,8 @@ 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); Mutex m_mutex; // Lock transport and session State m_state; // Connection state @@ -398,10 +299,11 @@ private: String m_callerPrompt; // Text to be sent to called before calling it String m_formats; // Formats received in call.execute String m_subject; // Connection subject - bool m_sendRawRtpFirst; // Send raw-rtp transport as the first content of outgoing session + bool m_offerRawTransport; // Offer RAW transport on outgoing session + bool m_offerIceTransport; // Offer ICE transport on outgoing session // Crypto (for contents created by us) - bool m_useCrypto; - bool m_cryptoMandatory; + 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 @@ -413,7 +315,7 @@ private: 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 + 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 @@ -428,7 +330,7 @@ private: ObjList m_streamHosts; // The list of negotiated SOCKS stream hosts }; -/** +/* * Transfer thread (route and execute) */ class YJGTransfer : public Thread @@ -446,128 +348,109 @@ private: Message m_msg; }; -/** - * resource.notify message handler +/* + * Module message handlers */ -class ResNotifyHandler : public MessageHandler +class YJGMessageHandler : public MessageHandler { public: - ResNotifyHandler() : MessageHandler("resource.notify") {} + enum { + JabberIq = 50, // handleJabberIq() + ChanNotify = -2, // handleChanNotify() + EngineStart = -3, // handleEngineStart() + ResNotify = -4, // handleResNotify() + ResSubscribe = -5, // handleResSubscribe() + UserRegister = 10, // handleUserRegister() + UserUnRegister = 11, // handleUserRegister() + }; + YJGMessageHandler(int handler); +protected: virtual bool received(Message& msg); - static void process(const JabberID& from, const JabberID& to, - const String& status, bool subFrom, NamedList* params = 0); - static void sendPresence(JabberID& from, JabberID& to, const String& status, - NamedList* params = 0); +private: + int m_handler; }; -/** - * resource.subscribe message handler - */ -class ResSubscribeHandler : public MessageHandler -{ -public: - ResSubscribeHandler() : MessageHandler("resource.subscribe") {} - virtual bool received(Message& msg); -}; - -/** - * user.login handler - */ -class UserLoginHandler : public MessageHandler -{ -public: - UserLoginHandler() : MessageHandler("user.login") {} - virtual bool received(Message& msg); -}; - -/** - * xmpp.generate message handler - */ -class XmppGenerateHandler : public MessageHandler -{ -public: - inline XmppGenerateHandler() : MessageHandler("xmpp.generate") {} - virtual bool received(Message& msg); -}; - -/** - * xmpp.iq message handler - */ -class XmppIqHandler : public MessageHandler -{ -public: - inline XmppIqHandler(int prio = 1000) : MessageHandler("xmpp.iq",prio) {} - virtual bool received(Message& msg); -}; - -/** +/* * YJGDriver */ class YJGDriver : public Driver { public: - // Message handlers - enum { - ChanNotify = Private - }; - // Enumerate protocols supported by this module - enum Protocol { - Jabber = 0, - Xmpp = 1, - Jingle = 2, - ProtoCount = 3 - }; - // Additional driver status commands - enum StatusCommands { - StatusStreams = 0, // Show all streams - StatusCmdCount = 1 - }; YJGDriver(); virtual ~YJGDriver(); - // Check if the channels should send single DTMFs - inline bool singleTone() const - { return m_singleTone; } + // 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) { + 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); + } + // Retrieve the default resource + inline void defaultResource(String& buf) { + Lock lock(this); + ObjList* o = m_resources.skipNull(); + if (o) + buf = static_cast(o->get()); + } + // Check if a resource can be handled by the module + inline bool handleResource(const String& name) { + 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); - // Send IM messages - virtual bool imExecute(Message& msg, String& dest); // Message handler: Disconnect channels, destroy streams, clear rosters virtual bool received(Message& msg, int id); - // Try to create a JID from a message. - // First try to get the 'username' parameter of the message. Then the 'from' parmeter - // @param checkDomain True to check if jid's domain is valid - // Return false if node or domain are 0 or domain is invalid - bool getJidFrom(JabberID& jid, Message& msg, bool checkDomain = false); - // Assign param value to jid. - // @param checkDomain True to check if jid's domain is valid - // Return false if node or domain are 0 or domain is invalid - bool decodeJid(JabberID& jid, Message& msg, const char* param, - bool checkDomain = false); - // Create the presence notification command - XMLElement* getPresenceCommand(JabberID& from, JabberID& to, bool available, - XMLElement* presence = 0); - // Process presence. Notify connections - void processPresence(const JabberID& local, const JabberID& remote, - bool available, bool audio); - // Create a media string from a list - void createMediaString(String& dest, ObjList& formats, char sep); - // Find a connection by local and remote jid, optionally ignore local - // resource (always ignore if local has no resource) - YJGConnection* find(const JabberID& local, const JabberID& remote, bool anyResource = false); - // Build and add XML child elements from a received message - bool addChildren(NamedList& msg, XMLElement* xml = 0, ObjList* list = 0); - // Get the destination (callto) from a call/im execute message - bool getExecuteDest(Message& msg, String& dest); - // Process a message received by a stream - void processImMsg(JBEvent& event); + // Handle jabber.iq messages + bool handleJabberIq(Message& msg); + // Dispatch jabber.iq messages + bool dispatchJabberIq(JGSession* session, XmlElement*& xml); + // Handle resource.notify messages + bool handleResNotify(Message& msg); + // Handle resource.subscribe messages + bool handleResSubscribe(Message& msg); + // Handle chan.notify messages + bool handleChanNotify(Message& msg); + // Handle msg.execute messages. Send chan.text if enabled + bool handleImExecute(Message& msg); + // Handle user.(un)unregister messages. Send resource.notify if enabled + bool handleUserRegister(Message& msg, bool reg); + // 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 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 @@ -575,67 +458,56 @@ public: Lock lock(this); return m_ftProxy ? new JGStreamHost(*m_ftProxy) : 0; } - - // Check if this module handles a given protocol - static bool canHandleProtocol(const String& proto) { - for (unsigned int i = 0; i < ProtoCount; i++) - if (proto == s_protocol[i]) - return true; - return false; - } - // Check if this module handles a given protocol - static const char* defProtoName() - { return s_protocol[Jabber].c_str(); } - // Protocols supported by this module - static const String s_protocol[ProtoCount]; -protected: - // Handle command complete requests - virtual bool commandComplete(Message& msg, const String& partLine, - const String& partWord); - // Additional driver status commands - static String s_statusCmd[StatusCmdCount]; private: - // Check and build caller and called for Component run mode - // Caller: Set user if missing. Get default server identity for Yate Component - // Try to get an available resource for the called party - bool setComponentCall(JabberID& caller, JabberID& called, const char* cr, - const char* cd, bool& available, String& error); + // Update the list of domains + void setDomains(const String& list); bool m_init; - bool m_singleTone; // Send single/batch DTMFs - bool m_installIq; // Install the 'iq' service in jabber - // engine and xmpp. message handlers - bool m_imToChanText; // Send received IM messages as chan.text if a channel is found + String m_localAddress; // The local machine's address + String m_anonymousCaller; // Caller username when missing JGStreamHost* m_ftProxy; // Default file transfer proxy - String m_statusCmd; // + ObjList m_handlers; // Message handlers list + ObjList m_domains; // Domains handled by the module + 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 String s_localAddress; // The local machine's address static unsigned int s_pendingTimeout = 10000; // Outgoing call pending timeout -static String s_anonymousCaller = "unk_caller"; // Caller name when missing -static bool s_attachPresToCmd = false; // Attach presence to command (when used) -static bool s_userRoster = false; // Send client roster with user.roster or resource.notify -static bool s_useCrypto = false; -static bool s_cryptoMandatory = false; +static bool s_requestSubscribe = true; // Request subscribe before making a non client + // call with target without resource +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 String s_priority = "-1"; // Resource priority for presence generated by this module static JGSession::Version s_sessVersion = JGSession::VersionUnknown; // Default jingle session version for outgoing calls -static YJBEngine* s_jabber = 0; +static bool s_serverMode = true; // Server/client mode static YJGEngine* s_jingle = 0; -static YJBMessage* s_message = 0; -static YJBPresence* s_presence = 0; -static YJBClientPresence* s_clientPresence = 0; -static YJBStreamService* s_stream = 0; -static YJBIqService* s_iqService = 0; -const String YJGDriver::s_protocol[YJGDriver::ProtoCount] = {"jabber", "xmpp", "jingle"}; static YJGDriver plugin; // The driver +// 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.register", YJGMessageHandler::UserRegister}, + {"user.unregister", YJGMessageHandler::UserUnRegister}, + {0,0} +}; + // Error mapping static TokenDict s_errMap[] = { {"normal", JGSession::ReasonOk}, @@ -644,40 +516,42 @@ static TokenDict s_errMap[] = { {"busy", JGSession::ReasonBusy}, {"rejected", JGSession::ReasonDecline}, {"nomedia", JGSession::ReasonMedia}, - {"transferred", JGSession::ReasonTransfer}, - {"failure", JGSession::ReasonUnknown}, + {"cancelled", JGSession::ReasonCancel}, + {"failure", JGSession::ReasonGeneral}, {"noroute", JGSession::ReasonDecline}, - {"noconn", JGSession::ReasonUnknown}, - {"noauth", JGSession::ReasonUnknown}, - {"nocall", JGSession::ReasonUnknown}, - {"noanswer", JGSession::ReasonUnknown}, - {"forbidden", JGSession::ReasonUnknown}, - {"offline", JGSession::ReasonUnknown}, - {"congestion", JGSession::ReasonUnknown}, - {"looping", JGSession::ReasonUnknown}, - {"shutdown", JGSession::ReasonUnknown}, + {"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::ReasonNoError}, - {"failure", JGSession::ReasonNoApp}, + {"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} }; -// Get the number of private threads of a given type -// Force to 1 for client run mode -// Force at least 1 otherwise -inline int threadCount(const NamedList& params, const char* param) -{ - if (s_jabber->protocol() == JBEngine::Client) - return 1; - int t = params.getIntValue(param); - return t < 1 ? 1 : t; -} -inline void addValidParam(Message& m, const char* param, const char* value) +static inline void addValidParam(Message& m, const char* param, const char* value) { if (!null(value)) m.addParam(param,value); @@ -698,125 +572,13 @@ static void setMedia(JGRtpMediaList& dest, const String& formats, } -/** - * YJBEngine - */ -void YJBEngine::initialize() -{ - debugChain(&plugin); - NamedList dummy(""); - NamedList* sect = s_cfg.getSection("general"); - if (!sect) - sect = &dummy; - // Force private processing. Force 1 thread for client run mode - sect->setParam("private_process_threads",String(threadCount(*sect,"private_process_threads"))); - sect->setParam("private_receive_threads",String(threadCount(*sect,"private_receive_threads"))); - JBEngine::initialize(*sect); - - String defComponent; - // Set server list if not client - unsigned int count = (protocol() != Client) ? s_cfg.sections() : 0; - for (unsigned int i = 0; i < count; i++) { - const NamedList* comp = s_cfg.getSection(i); - String name = comp ? comp->c_str() : ""; - if (!name || name == "general" || name == "codecs") - continue; - - const char* address = comp->getValue("address"); - String tmp = comp->getValue("port"); - int port = tmp.toInteger(); - if (!(address && port)) { - Debug(this,DebugNote, - "Invalid address=%s or port=%s in configuration for %s", - address,tmp.c_str(),name.c_str()); - continue; - } - const char* password = comp->getValue("password"); - // Check identity. Construct the full identity - String identity = comp->getValue("identity"); - if (!identity) - identity = name; - String fullId; - bool keepRoster = false; - if (identity == name) { - String subdomain = comp->getValue("subdomain",s_cfg.getValue( - "general","default_resource",defaultResource())); - identity = subdomain; - identity << '.' << name; - fullId = name; - } - else { - keepRoster = true; - fullId << '.' << name; - if (identity.endsWith(fullId)) { - if (identity.length() == fullId.length()) { - Debug(this,DebugNote,"Invalid identity=%s in configuration for %s", - identity.c_str(),name.c_str()); - continue; - } - fullId = identity; - } - else { - fullId = identity; - fullId << '.' << name; - } - identity = fullId; - } - if (!identity) - continue; - int flags = XMPPUtils::decodeFlags(comp->getValue("options"), - XMPPServerInfo::s_flagName); - if (!comp->getBoolValue("auto_restart",true)) - flags |= XMPPServerInfo::NoAutoRestart; - if (keepRoster) - flags |= XMPPServerInfo::KeepRoster; - XMPPServerInfo* server = new XMPPServerInfo(name,address,port, - password,identity,fullId,flags); - bool startup = comp->getBoolValue("startup"); -#ifdef DEBUG - String f; - XMPPUtils::buildFlags(f,flags,XMPPServerInfo::s_flagName); - DDebug(this,DebugAll, - "Add server '%s' %s:%d ident=%s full-ident=%s options=%s", - name.c_str(),address,port,identity.c_str(),fullId.c_str(), - f.c_str()); -#endif - appendServer(server,startup); - if (!defComponent || comp->getBoolValue("default")) - defComponent = name; - } - // Set default component server - if (protocol() == Component) - setComponentServer(defComponent); -} - -// Setup the transport layer security for a stream -bool YJBEngine::encryptStream(JBStream* stream) -{ - if (!stream) - return false; - Message msg("socket.ssl"); - msg.userData(stream); - msg.addParam("server",String::boolText(!stream->outgoing())); - return Engine::dispatch(msg); -} - - -/** +/* * YJGEngine */ -void YJGEngine::initialize() +// Send a session's stanza (dispatch a jabber.iq message) +bool YJGEngine::sendStanza(JGSession* session, XmlElement*& stanza) { - debugChain(&plugin); - NamedList dummy(""); - NamedList* sect = s_cfg.getSection("general"); - if (!sect) - sect = &dummy; - // Force private processing - sect->setParam("private_process_threads",String(threadCount(*sect,"private_process_threads"))); - JGEngine::initialize(*sect); - // Init data - m_requestSubscribe = sect->getBoolValue("request_subscribe",true); + return plugin.dispatchJabberIq(session,stanza); } // Process jingle events @@ -849,500 +611,65 @@ void YJGEngine::processEvent(JGEvent* event) } else { Debug(this,DebugWarn,"Session ref failed for new connection"); - event->session()->hangup(JGSession::ReasonUnknown,"Internal error"); + event->session()->hangup(event->session()->createReason(JGSession::ReasonGeneral)); } } else { DDebug(this,DebugAll,"Invalid (non initiate) event for new session"); - event->confirmElement(XMPPError::SRequest,"Unknown session"); + event->confirmElement(XMPPError::Request,"Unknown session"); } } delete event; } -/** - * YJBMessage +/* + * YJGEngineWorker */ -void YJBMessage::initialize() +void YJGEngineWorker::run() { - debugChain(&plugin); - NamedList dummy(""); - NamedList* sect = s_cfg.getSection("general"); - if (!sect) - sect = &dummy; - // Force sync (not enqueued) message processing - sect->setParam("sync_process","true"); - JBMessage::initialize(*sect); -} - -// Process a Jabber message -void YJBMessage::processMessage(JBEvent* event) -{ - if (!event) - return; - plugin.processImMsg(*event); -} - - -/** - * YJBStreamService - */ -void YJBStreamService::initialize() -{ - debugChain(&plugin); -} - -// Process stream termination events -bool YJBStreamService::accept(JBEvent* event, bool& processed, bool& insert) -{ - JBStream* stream = event ? event->stream() : 0; - if (!stream) - return false; - if (event->type() != JBEvent::Terminated && - event->type() != JBEvent::Running && - event->type() != JBEvent::Destroy) - return false; - - Message* m = new Message("user.notify"); - m->addParam("module",plugin.name()); - m->addParam("account",stream->name()); - m->addParam("protocol",plugin.defProtoName()); - m->addParam("username",stream->local().node()); - m->addParam("server",stream->local().domain()); - m->addParam("jid",stream->local()); - m->addParam("registered",String::boolText(event->type() == JBEvent::Running)); - if (event->type() != JBEvent::Running && event->text()) - m->addParam("reason",event->text()); - bool restart = (stream->state() != JBStream::Destroy && stream->flag(JBStream::AutoRestart)); - m->addParam("autorestart",String::boolText(restart)); - Engine::enqueue(m); - return false; -} - -/** - * YJBClientPresence - */ -void YJBClientPresence::initialize() -{ - debugChain(&plugin); -} - -// Process client presence and roster updates -bool YJBClientPresence::accept(JBEvent* event, bool& processed, bool& insert) -{ - if (!event) - return false; - - processed = true; + Debug(&plugin,DebugAll,"%s start running",currentName()); while (true) { - if (event->type() != JBEvent::Presence && - event->type() != JBEvent::IqClientRosterUpdate) { - Debug(this,DebugStub,"Can't accept unexpected event=%s [%p]", - event->name(),this); - processed = false; + if (Thread::check(false) || Engine::exiting()) break; - } - - // User roster update - if (event->type() == JBEvent::IqClientRosterUpdate) { - if (!event->child()) - break; - // Send the whole roster in one message - if (s_userRoster) { - Message* m = new Message("user.roster"); - m->addParam("module",plugin.name()); - m->addParam("protocol",plugin.defProtoName()); - if (event->stream() && event->stream()->name()) - m->addParam("account",event->stream()->name()); - else if (event->to().node()) - m->addParam("username",event->to().node()); - XMLElement* iq = event->releaseXML(); - XMLElement* query = iq->findFirstChild("query"); - if (query) { - XMLElement* item = 0; - int count = 0; - for (;;) { - item = query->findNextChild(item,"item"); - if (!item) - break; - String jid = item->getAttribute("jid"); - if (!jid) - continue; - count++; - String base("contact."); - base << count; - m->addParam(base,jid.toLower()); - const char* tmp = item->getAttribute("name"); - if (tmp) - m->addParam(base + ".name",tmp); - tmp = item->getAttribute("subscription"); - if (tmp) - m->addParam(base + ".subscription",tmp); - // Copy children - XMLElement* child = item->findFirstChild(); - for (; child; child = item->findNextChild(child)) - m->addParam(base + "." + child->name(),child->getText()); - } - TelEngine::destruct(query); - m->addParam("contact.count",String(count)); - } - m->addParam(new NamedPointer("xml",iq,"roster")); - Engine::enqueue(m); - break; - } - // Send the roster in individual resource.notify - XMLElement* item = event->child()->findFirstChild(XMLElement::Item); - for (; item; item = event->child()->findNextChild(item,XMLElement::Item)) { - Message* m = YJBPresence::message(-1,0,event->to().bare(), - item->getAttribute("subscription")); - if (event->stream() && event->stream()->name()) - m->setParam("account",event->stream()->name()); - m->setParam("contact",String(item->getAttribute("jid")).toLower()); - m->addParam("roster",String::boolText(true)); - addValidParam(*m,"contactname",item->getAttribute("name")); - addValidParam(*m,"ask",item->getAttribute("ask")); - // Copy children - XMLElement* child = item->findFirstChild(); - for (; child; child = item->findNextChild(child)) - addValidParam(*m,child->name(),child->getText()); - Engine::enqueue(m); - } - break; - } - - // Presence - const char* sub = 0; - if (event->stream() && event->stream()->type() == JBEngine::Client) { - JBClientStream* stream = static_cast(event->stream()); - Lock lock(stream->roster()); - XMPPUser* user = stream->getRemote(event->from()); - if (user) { - sub = XMPPDirVal::lookup((int)user->subscription()); - TelEngine::destruct(user); - } - } - - Message* m = 0; - JBPresence::Presence pres = JBPresence::presenceType(event->stanzaType()); - - if (pres == JBPresence::None || pres == JBPresence::Unavailable) { - bool capAudio = false; - bool available = (pres == JBPresence::None); - JIDResource* res = 0; - if (event->element()) { - res = new JIDResource(event->from().resource()); - if (res->fromXML(event->element())) { - capAudio = res->hasCap(JIDResource::CapAudio); - available = res->available(); - } - } - // Notify presence to module and enqueue message in engine - plugin.processPresence(event->to(),event->from(),available,capAudio); - m = YJBPresence::message(pres,event->from(),event->to(),sub); - if (res) { - m->addParam("audio",String::boolText(capAudio)); - ObjList* o = res->infoXml()->skipNull(); - unsigned int n = 1; - if (res->show() != JIDResource::ShowNone) { - const char* show = JIDResource::showText(res->show()); - if (!TelEngine::null(show)) { - m->setParam("message-prefix","jingle"); - m->addParam("jingle." + String(n),"show"); - m->addParam("jingle." + String(n) + ".",show); - n++; - } - } - if (o || res->status()) { - String prefix = "jingle"; - m->setParam("message-prefix",prefix); - prefix << "."; - // Set status: avoid some meaningful values - if (res->status()) { - if (res->status() != "subscribed" && - res->status() != "unsubscribed" && - res->status() != "offline") - m->setParam("status",res->status()); - else { - m->addParam(prefix + String(n),"status"); - m->addParam(prefix + String(n) + ".",res->status()); - n++; - } - } - for (; o; o = o->skipNext(), n++) { - XMLElement* e = static_cast(o->get()); - e->toList(*m,String(prefix + String(n))); - } - } - TelEngine::destruct(res); - } - } + JGEvent* ev = s_jingle->getEvent(Time::msecNow()); + if (ev) + s_jingle->processEvent(ev); else - switch (pres) { - case JBPresence::Subscribe: - case JBPresence::Unsubscribe: - case JBPresence::Subscribed: - case JBPresence::Unsubscribed: - case JBPresence::Probe: - m = YJBPresence::message(pres,event->from().bare(), - event->to().bare(),sub); - break; - case JBPresence::Error: - if (event->text()) { - m = YJBPresence::message(pres,event->from().bare(), - event->to().bare(),sub); - m->setParam("error",event->text()); - } - break; - default: - Debug(this,DebugStub,"accept() not implemented for presence=%s [%p]", - event->stanzaType().c_str(),this); - processed = false; - } - - if (m) { - if (event->stream() && event->stream()->name()) - m->setParam("account",event->stream()->name()); - Engine::enqueue(m); - } - break; + Thread::idle(false); } - - return processed; + Debug(&plugin,DebugAll,"%s stop running",currentName()); } -/** - * YJBPresence - */ -void YJBPresence::initialize() -{ - debugChain(&plugin); - NamedList dummy(""); - NamedList* sect = s_cfg.getSection("general"); - if (!sect) - sect = &dummy; - // Force private processing - sect->setParam("private_process_threads",String(threadCount(*sect,"private_process_threads"))); - JBPresence::initialize(*sect); -} - -bool YJBPresence::notifyProbe(JBEvent* event) -{ - XDebug(this,DebugAll,"notifyProbe local=%s remote=%s [%p]", - event->to().c_str(),event->from().c_str(),this); - Engine::enqueue(message(JBPresence::Probe,event->from().bare(),event->to().bare(),0)); - return true; -} - -bool YJBPresence::notifySubscribe(JBEvent* event, Presence presence) -{ - XDebug(this,DebugAll,"notifySubscribe(%s) local=%s remote=%s [%p]", - presenceText(presence),event->to().c_str(),event->from().c_str(),this); - // Respond if auto subscribe - if (!ignoreNonRoster() && event->stream() && autoSubscribe().from() && - (presence == JBPresence::Subscribe || presence == JBPresence::Unsubscribe)) { - if (presence == JBPresence::Subscribe) - presence = JBPresence::Subscribed; - else - presence = JBPresence::Unsubscribed; - XMLElement* xml = createPresence(event->to().bare(),event->from().bare(),presence); - event->stream()->sendStanza(xml); - return true; - } - // Enqueue message - Engine::enqueue(message(presence,event->from().bare(),event->to().bare(),0)); - return true; -} - -void YJBPresence::notifySubscribe(XMPPUser* user, Presence presence) -{ - if (!user) - return; - XDebug(this,DebugAll,"notifySubscribe(%s) local=%s remote=%s [%p]", - presenceText(presence),user->local()->jid().bare().c_str(), - user->jid().bare().c_str(),this); - Engine::enqueue(message(presence,user->jid().bare(),user->local()->jid().bare(),0)); -} - -bool YJBPresence::notifyPresence(JBEvent* event, bool available) -{ - // Check audio properties and availability for received resource - bool capAudio = false; - if (event && event->element()) { - JIDResource* res = new JIDResource(event->from().resource()); - if (res->fromXML(event->element())) { - capAudio = res->hasCap(JIDResource::CapAudio); - available = res->available(); - } - TelEngine::destruct(res); - } - Debug(this,DebugAll,"notifyPresence local=%s remote=%s available=%s [%p]", - event->to().c_str(),event->from().c_str(),String::boolText(available),this); - // Notify presence to module and enqueue message in engine - plugin.processPresence(event->to(),event->from(),available,capAudio); - Engine::enqueue(message(available ? JBPresence::None : JBPresence::Unavailable, - event->from().bare(),event->to().bare(),0)); - return true; -} - -// Notify plugin and enqueue message in engine -void YJBPresence::notifyPresence(XMPPUser* user, JIDResource* resource) -{ - if (!(user && resource)) - return; - JabberID remote(user->jid().node(),user->jid().domain(),resource->name()); - Debug(this,DebugAll,"notifyPresence local=%s remote=%s available=%s [%p]", - user->local()->jid().c_str(),remote.c_str(), - String::boolText(resource->available()),this); - plugin.processPresence(user->local()->jid(),remote,resource->available(), - resource->hasCap(JIDResource::CapAudio)); - Engine::enqueue(message(resource->available() ? JBPresence::None : JBPresence::Unavailable, - user->jid().bare(),user->local()->jid().bare(), - String::boolText(user->subscription().to()))); -} - -void YJBPresence::notifyNewUser(XMPPUser* user) -{ - if (!user) - return; - DDebug(this,DebugAll,"notifyNewUser local=%s remote=%s. Adding default resource [%p]", - user->local()->jid().bare().c_str(),user->jid().bare().c_str(),this); - // Add local resource - user->addLocalRes(new JIDResource(s_jabber->defaultResource(),JIDResource::Available, - JIDResource::CapAudio)); -} - -Message* YJBPresence::message(int presence, const char* from, const char* to, - const char* subscription) -{ - Message* m = 0; - const char* status = 0; - const char* operation = 0; - switch (presence) { - case JBPresence::None: - m = new Message("resource.notify"); - status = "online"; - break; - case JBPresence::Unavailable: - m = new Message("resource.notify"); - status = "offline"; - break; - case JBPresence::Subscribed: - m = new Message("resource.notify"); - status = "subscribed"; - break; - case JBPresence::Unsubscribed: - m = new Message("resource.notify"); - status = "unsubscribed"; - break; - case JBPresence::Probe: - m = new Message("resource.notify"); - operation = "probe"; - break; - case JBPresence::Subscribe: - m = new Message("resource.subscribe"); - operation = "subscribe"; - break; - case JBPresence::Unsubscribe: - m = new Message("resource.subscribe"); - operation = "unsubscribe"; - break; - default: - m = new Message("resource.notify"); - } - m->addParam("module",plugin.name()); - m->addParam("protocol",plugin.defProtoName()); - m->addParam("to",to); - if (!TelEngine::null(from)) { - JabberID jid(from); - m->addParam("contact",String(jid.bare()).toLower()); - if (jid.resource()) - m->addParam("instance",jid.resource()); - } - addValidParam(*m,"from",from); - addValidParam(*m,"operation",operation); - addValidParam(*m,"subscription",subscription); - addValidParam(*m,"status",status); - return m; -} - - -/** - * YJBIqService - */ -void YJBIqService::initialize() -{ - debugChain(&plugin); -} - -// Process events -bool YJBIqService::accept(JBEvent* event, bool& processed, bool& insert) -{ - if (!(event && event->element())) - return false; - - processed = (event->element()->type() == XMLElement::Iq); - if (!processed) { - // Don't show the debug if it's a WriteFail event: this event may - // carry any failed stanza - if (event->type() != JBEvent::WriteFail) - Debug(this,DebugStub,"Can't accept unexpected event=%s [%p]", - event->name(),this); - return false; - } - - bool incoming = (event->type() != JBEvent::WriteFail); - Message* m = new Message("xmpp.iq"); - m->addParam("module",plugin.name()); - if (event->stream()) - m->addParam("account",event->stream()->name()); - const JabberID* from = &(event->from()); - const JabberID* to = &(event->to()); - // Received stanza: get source/destination JID from stream if missing - if (incoming) { - if (to->null() && event->stream()) - to = &(event->stream()->local()); - if (from->null() && event->stream()) - from = &(event->stream()->remote()); - } - addValidParam(*m,"from",*from); - addValidParam(*m,"to",*to); - m->addParam("type",event->stanzaType()); - addValidParam(*m,"id",event->id()); - addValidParam(*m,"username",from->node()); - if (!to->null()) - m->addParam("calleduri",BUILD_XMPP_URI(*to)); - if (!incoming) - m->addParam("failure",String::boolText(true)); - XMLElement* xml = event->releaseXML(); - XMLElement* child = xml->findFirstChild(); - m->addParam(new NamedPointer("xml",xml,child?child->name():0)); - TelEngine::destruct(child); - Engine::enqueue(m); - return true; -} - - -/** +/* * YJGConnection */ // Outgoing call YJGConnection::YJGConnection(Message& msg, const char* caller, const char* called, - bool available, const char* file) + bool available, const NamedList& caps, const char* file) : 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_local(caller), m_remote(called), m_audioContent(0), - m_callerPrompt(msg.getValue("callerprompt")), m_sendRawRtpFirst(true), - m_useCrypto(s_useCrypto), m_cryptoMandatory(s_cryptoMandatory), + m_callerPrompt(msg.getValue("callerprompt")), + m_offerRawTransport(true), m_offerIceTransport(true), + m_secure(s_useCrypto), m_secureRequired(s_cryptoMandatory), m_hangup(false), m_timeout(0), m_transferring(false), m_recvTransferStanza(0), m_dataFlags(0), m_ftStatus(FTNone), m_ftHostDirection(FTHostNone) { - NamedString* ver = msg.getParam("ojingle_version"); - if (ver) - m_sessVersion = JGSession::lookupVersion(*ver); + m_secure = msg.getBoolValue("secure",m_secure); + m_secureRequired = msg.getBoolValue("secure_required",m_secureRequired); + overrideJingleVersion(msg,false); + if (available) + overrideJingleVersion(caps,true); + if (m_sessVersion != JGSession::Version0) { + m_offerRawTransport = s_offerRawTransport; + m_offerIceTransport = s_offerIceTransport; + } + else + m_offerRawTransport = false; m_subject = msg.getValue("subject"); String uri = msg.getValue("diverteruri",msg.getValue("diverter")); // Skip protocol if present @@ -1417,24 +744,19 @@ YJGConnection::YJGConnection(JGEvent* event) m_state(Active), m_session(event->session()), m_rtpStarted(false), m_acceptRelay(s_acceptRelay), m_sessVersion(event->session()->version()), m_local(event->session()->local()), m_remote(event->session()->remote()), - m_audioContent(0), m_sendRawRtpFirst(true), - m_useCrypto(s_useCrypto), m_cryptoMandatory(s_cryptoMandatory), + m_audioContent(0), + m_offerRawTransport(true), m_offerIceTransport(true), + m_secure(s_useCrypto), m_secureRequired(s_cryptoMandatory), m_hangup(false), m_timeout(0), m_transferring(false), m_recvTransferStanza(0), m_dataFlags(0), m_ftStatus(FTNone), m_ftHostDirection(FTHostNone) { if (event->jingle()) { // Check if this call is transferred - XMLElement* trans = event->jingle()->findFirstChild(XMLElement::Transfer); - if (trans) { - m_transferFrom.set(trans->getAttribute("from")); - TelEngine::destruct(trans); - } + XmlElement* trans = XMPPUtils::findFirstChild(*event->jingle(),XmlTag::Transfer); + if (trans) + m_transferFrom = trans->getAttribute("from"); // Get subject - XMLElement* subject = event->jingle()->findFirstChild(XMLElement::Subject); - if (subject) { - m_subject = subject->getText(); - TelEngine::destruct(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(), @@ -1442,6 +764,8 @@ YJGConnection::YJGConnection(JGEvent* event) 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; @@ -1518,7 +842,7 @@ YJGConnection::YJGConnection(JGEvent* event) m_state = Pending; setReason("failure"); Debug(this,DebugNote,"%s [%p]",error,this); - event->confirmElement(XMPPError::SBadRequest,error); + event->confirmElement(XMPPError::BadRequest,error); } // Startup @@ -1533,7 +857,7 @@ YJGConnection::YJGConnection(JGEvent* event) YJGConnection::~YJGConnection() { TelEngine::destruct(m_recvTransferStanza); - hangup(0); + hangup(); disconnected(true,m_reason); Debug(this,DebugCall,"Destroyed [%p]",this); } @@ -1543,9 +867,9 @@ bool YJGConnection::route() { Message* m = message("call.preroute",false,true); m->addParam("username",m_remote.node()); - if (m_session && m_session->stream() && - m_session->stream()->type() == JBEngine::Client) - m->addParam("in_line",m_session->stream()->name()); +// if (m_session && m_session->stream() && +// m_session->stream()->type() == JBEngine::Client) +// m->addParam("in_line",m_session->stream()->name()); m->addParam("called",m_local.node()); m->addParam("calleduri",BUILD_XMPP_URI(m_local)); m->addParam("caller",m_remote.node()); @@ -1587,6 +911,8 @@ bool YJGConnection::route() 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); Channel::callAccept(msg); } @@ -1594,7 +920,9 @@ void YJGConnection::callRejected(const char* error, const char* reason, const Message* msg) { Debug(this,DebugCall,"callRejected. error=%s reason=%s [%p]",error,reason,this); - hangup(error ? error : reason,reason); + if (!reason) + reason = "rejected"; + hangup(error,reason); Channel::callRejected(error,reason,msg); } @@ -1627,11 +955,8 @@ bool YJGConnection::msgRinging(Message& msg) if (m_ftStatus != FTNone) return true; m_mutex.lock(); - if (m_session && m_session->hasFeature(XMPPNamespace::JingleAppsRtpInfo)) { - XMLElement* xml = XMPPUtils::createElement(XMLElement::Ringing, - XMPPNamespace::JingleAppsRtpInfo); - m_session->sendInfo(xml); - } + if (m_session) + m_session->sendInfo(m_session->createRtpInfoXml(JGSession::RtpRinging)); m_mutex.unlock(); setEarlyMediaOut(msg); return true; @@ -1715,7 +1040,7 @@ bool YJGConnection::msgUpdate(Message& msg) SET_ERROR_BREAK("failure","Already on hold"); } // Send XML. Copy any additional params - XMLElement* hold = XMPPUtils::createElement(XMLElement::Hold, + XmlElement* hold = XMPPUtils::createElement(XmlTag::Hold, XMPPNamespace::JingleAppsRtpInfo); unsigned int n = msg.length(); for (unsigned int i = 0; i < n; i++) { @@ -1751,7 +1076,7 @@ bool YJGConnection::msgUpdate(Message& msg) if (dataFlags(OnHoldRemote)) SET_ERROR_BREAK("failure","Already on hold by the other party"); // Send XML. Copy additional attributes - XMLElement* active = XMPPUtils::createElement(XMLElement::Active, + XmlElement* active = XMPPUtils::createElement(XmlTag::Active, XMPPNamespace::JingleAppsRtpInfo); unsigned int n = msg.length(); for (unsigned int i = 0; i < n; i++) { @@ -1786,10 +1111,8 @@ bool YJGConnection::msgText(Message& msg, const char* text) { DDebug(this,DebugCall,"msgText. '%s' [%p]",text,this); Lock lock(m_mutex); - if (m_session) { - m_session->sendMessage(text); - return true; - } + if (m_session) + return s_jingle->sendMessage(m_session,text); return false; } @@ -1813,7 +1136,7 @@ bool YJGConnection::msgTone(Message& msg, const char* tone) Lock lock(m_mutex); if (!m_session) return true; - if (plugin.singleTone()) { + if (s_singleTone) { char s[2] = {0,0}; while (*tone) { s[0] = *tone++; @@ -1840,7 +1163,7 @@ bool YJGConnection::msgTransfer(Message& msg) if (chanId) { bool ok = false; plugin.lock(); - YJGConnection* conn = static_cast(plugin.Driver::find(*chanId)); + YJGConnection* conn = static_cast(plugin.find(*chanId)); if (conn) { ok = conn->getSid(m_transferSid); if (!m_transferTo) @@ -1867,17 +1190,17 @@ bool YJGConnection::msgTransfer(Message& msg) } // 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); +// 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, + 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(new XMLElement(XMLElement::Subject,0,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(), @@ -1908,9 +1231,25 @@ void YJGConnection::hangup(const char* reason, const char* text) if (m_session) { m_session->userData(0); int res = lookup(m_reason,s_errMap,JGSession::ReasonUnknown); - if (res == JGSession::ReasonUnknown && !text) - text = m_reason; - m_session->hangup(res,text); + Debug(this,DebugAll,"Hangup. %s %s [%p]", + m_reason.c_str(),lookup(res,JGSession::s_reasons),this); + XmlElement* xml = 0; + switch (res) { + case JGSession::CryptoRequired: + case JGSession::InvalidCrypto: + xml = m_session->createReason(JGSession::ReasonOk,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); TelEngine::destruct(m_session); } Debug(this,DebugCall,"Hangup. reason=%s [%p]",m_reason.c_str(),this); @@ -1934,11 +1273,8 @@ bool YJGConnection::handleEvent(JGEvent* event) Debug(this,DebugInfo, "Session terminated with reason='%s' text='%s' [%p]", reason,event->text().c_str(),this); - // Check for Jingle reasons - int res = JGSession::lookupReason(reason,JGSession::ReasonNone); - if (res != JGSession::ReasonNone) - reason = lookup(res,s_errMap,reason); - setReason(reason); + int jingleReason = lookup(reason,JGSession::s_reasons,JGSession::ReasonGeneral); + setReason(lookup(jingleReason,s_errMap,reason)); return false; } @@ -1948,7 +1284,6 @@ bool YJGConnection::handleEvent(JGEvent* event) break; case JGEvent::ResultOk: case JGEvent::ResultError: - case JGEvent::ResultWriteFail: case JGEvent::ResultTimeout: response = true; break; @@ -1970,15 +1305,12 @@ bool YJGConnection::handleEvent(JGEvent* event) String usedHost; bool ok = rspOk; if (rspOk && event->element()) { - XMLElement* query = event->element()->findFirstChild(XMLElement::Query); - XMLElement* used = 0; + XmlElement* query = XMPPUtils::findFirstChild(*event->element(),XmlTag::Query); if (query) { - used = query->findFirstChild(XMLElement::StreamHostUsed); + XmlElement* used = XMPPUtils::findFirstChild(*query,XmlTag::StreamHostUsed); if (used) usedHost = used->getAttribute("jid"); } - TelEngine::destruct(query); - TelEngine::destruct(used); } if (!ok) { // Result error: continue if we still can receive hosts @@ -2056,15 +1388,15 @@ bool YJGConnection::handleEvent(JGEvent* event) if (m_ftStatus == FTNone) processActionTransportInfo(event); else - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; case JGSession::ActTransportAccept: // TODO: handle it when (if) we'll send transport-replace - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; case JGSession::ActTransportReject: // TODO: handle it when (if) we'll send transport-replace - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; case JGSession::ActTransportReplace: // TODO: handle it @@ -2075,7 +1407,7 @@ bool YJGConnection::handleEvent(JGEvent* event) break; case JGSession::ActContentAccept: if (m_ftStatus != FTNone) { - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; } event->confirmElement(); @@ -2098,16 +1430,16 @@ bool YJGConnection::handleEvent(JGEvent* event) if (m_ftStatus == FTNone) processActionContentAdd(event); else - event->confirmElement(XMPPError::SRequest); + 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::SNotAllowed); + event->confirmElement(XMPPError::NotAllowed); break; case JGSession::ActContentReject: if (m_ftStatus != FTNone) { - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; } // XEP-0166 Notes - 16: terminate the session if there are no more contents @@ -2192,7 +1524,7 @@ bool YJGConnection::handleEvent(JGEvent* event) if (m_ftStatus == FTNone) processTransferRequest(event); else - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; case JGSession::ActRinging: if (m_ftStatus == FTNone) { @@ -2200,7 +1532,7 @@ bool YJGConnection::handleEvent(JGEvent* event) Engine::enqueue(message("call.ringing",false,true)); } else - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; case JGSession::ActHold: case JGSession::ActActive: @@ -2208,7 +1540,7 @@ bool YJGConnection::handleEvent(JGEvent* event) if (m_ftStatus == FTNone) handleAudioInfoEvent(event); else - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; case JGSession::ActTrying: case JGSession::ActReceived: @@ -2218,18 +1550,18 @@ bool YJGConnection::handleEvent(JGEvent* event) event,event->actionName(),this); } else - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; case JGSession::ActStreamHost: if (m_ftStatus != FTNone) { // Check if allowed if (m_ftHostDirection != FTHostRemote) { - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; } // Check if we already received it if (m_ftStatus != FTIdle) { - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; } event->setConfirmed(); @@ -2251,7 +1583,7 @@ bool YJGConnection::handleEvent(JGEvent* event) } } else - event->confirmElement(XMPPError::SRequest); + event->confirmElement(XMPPError::Request); break; default: Debug(this,DebugNote, @@ -2265,7 +1597,7 @@ bool YJGConnection::handleEvent(JGEvent* event) // 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) +bool YJGConnection::presenceChanged(bool available, NamedList* params) { Lock lock(m_mutex); if (m_state == Terminated) @@ -2282,25 +1614,32 @@ bool YJGConnection::presenceChanged(bool available) // 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) { + 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; + XmlElement* transfer = 0; if (m_transferFrom) transfer = JGSession::buildTransfer(String::empty(),m_transferFrom); - if (m_sessVersion == JGSession::Version1) { - if (m_sendRawRtpFirst) { - addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp)); - addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp)); - } - else { - addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp)); - addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp)); - } - } - else if (m_sessVersion == JGSession::Version0) + if (m_offerRawTransport) + addContent(true,buildAudioContent(JGRtpCandidates::RtpRawUdp)); + if (m_offerIceTransport) addContent(true,buildAudioContent(JGRtpCandidates::RtpIceUdp)); m_session = s_jingle->call(m_sessVersion,m_local,m_remote,m_audioContents,transfer, m_callerPrompt,m_subject); @@ -2339,21 +1678,21 @@ 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; + XmlElement* trans = 0; const char* reason = 0; - XMPPError::Type error = XMPPError::SBadRequest; + XMPPError::Type error = XMPPError::BadRequest; while (true) { if (!canTransfer()) { - error = XMPPError::SRequest; + error = XMPPError::Request; reason = "Unacceptable in current state"; break; } - trans = event->jingle() ? event->jingle()->findFirstChild(XMLElement::Transfer) : 0; + trans = event->jingle() ? XMPPUtils::findFirstChild(*event->jingle(),XmlTag::Transfer) : 0; if (!trans) { reason = "Transfer element is misssing"; break; } - m_transferTo.set(trans->getAttribute("to")); + m_transferTo = trans->getAttribute("to"); // Check transfer target if (!m_transferTo) { reason = "Transfer target is misssing or incomplete"; @@ -2365,22 +1704,16 @@ bool YJGConnection::processTransferRequest(JGEvent* event) reason = "Can't replace the same session"; break; } - m_transferFrom.set(trans->getAttribute("from")); + m_transferFrom = trans->getAttribute("from"); break; } String subject; - if (!reason && trans) { - XMLElement* s = trans->findFirstChild(XMLElement::Subject); - if (s) { - subject = s->getText(); - TelEngine::destruct(s); - } - } - TelEngine::destruct(trans); + if (!reason && trans) + subject = XMPPUtils::subject(*trans); if (!reason) { TelEngine::destruct(m_recvTransferStanza); - m_recvTransferStanza = event->releaseXML(); + m_recvTransferStanza = event->releaseXml(); event->setConfirmed(); m_transferring = true; Debug(this,DebugCall,"Starting transfer to=%s from=%s sid=%s [%p]", @@ -2410,39 +1743,22 @@ void YJGConnection::transferTerminated(bool ok, const char* reason) Debug(this,DebugNote,"Transfer failed error='%s' [%p]",reason,this); } if (m_session && m_recvTransferStanza) { - XMPPError::Type err = ok ? XMPPError::NoError : XMPPError::SUndefinedCondition; - m_session->confirm(m_recvTransferStanza,err,reason,XMPPError::TypeCancel); - m_recvTransferStanza = 0; + 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 = ""; - m_transferFrom = ""; + m_transferTo.set(""); + m_transferFrom.set(""); m_transferSid = ""; } -// Get the remote party address (actually this is the address of the -// local party's server) -void YJGConnection::getRemoteAddr(String& dest) -{ - if (m_session && m_session->stream()) { - dest = m_session->stream()->addr().host(); - return; - } - if (!s_jabber) - return; - JBStream* stream = 0; - if (s_jabber->protocol() == JBEngine::Component) - stream = s_jabber->getStream(); - else - stream = s_jabber->getStream(&m_local,false); - if (stream) - dest = stream->addr().host(); - TelEngine::destruct(stream); -} - // Process an ActContentAdd event void YJGConnection::processActionContentAdd(JGEvent* event) { @@ -2452,7 +1768,7 @@ void YJGConnection::processActionContentAdd(JGEvent* event) ObjList ok; ObjList remove; if (!processContentAdd(*event,ok,remove)) { - event->confirmElement(XMPPError::SConflict,"Duplicate content(s)"); + event->confirmElement(XMPPError::Conflict,"Duplicate content(s)"); return; } @@ -2524,7 +1840,7 @@ void YJGConnection::processActionTransportInfo(JGEvent* event) resetCurrentAudioContent(isAnswered(),!isAnswered()); } else - event->confirmElement(XMPPError::SNotAcceptable); + event->confirmElement(XMPPError::NotAcceptable); enqueueCallProgress(); } @@ -2646,7 +1962,7 @@ void YJGConnection::removeCurrentAudioContent(bool removeReq) c->setEarlyMedia(); // Copy media c->m_rtpMedia.m_media = m_audioContent->m_rtpMedia.m_media; - c->m_rtpMedia.m_cryptoMandatory = m_audioContent->m_rtpMedia.m_cryptoMandatory; + c->m_rtpMedia.m_cryptoRequired = m_audioContent->m_rtpMedia.m_cryptoRequired; for (ObjList* o = m_audioContent->m_rtpMedia.skipNull(); o; o = o->skipNext()) { JGRtpMedia* m = static_cast(o->get()); c->m_rtpMedia.append(new JGRtpMedia(*m)); @@ -2783,7 +2099,7 @@ bool YJGConnection::startRtp() rtpRemote->m_address) { m_rtpStarted = true; // Start STUN - Message* msg = new Message("socket.stun"); + Message* msg = plugin.message("socket.stun"); msg->userData(m.userData()); // FIXME: check if these parameters are correct msg->addParam("localusername",m_audioContent->m_rtpRemoteCandidates.m_ufrag + @@ -2798,7 +2114,7 @@ bool YJGConnection::startRtp() else if (m_audioContent->m_rtpLocalCandidates.m_type == JGRtpCandidates::RtpRawUdp) { // Send trying if (m_session) { - XMLElement* trying = XMPPUtils::createElement(XMLElement::Trying, + XmlElement* trying = XMPPUtils::createElement(XmlTag::Trying, XMPPNamespace::JingleTransportRawUdpInfo); m_session->sendInfo(trying); } @@ -3024,14 +2340,14 @@ JGSessionContent* YJGConnection::buildAudioContent(JGRtpCandidates::Type type, // Add codecs c->m_rtpMedia.m_media = JGRtpMediaList::Audio; - if (m_useCrypto && m_cryptoMandatory) - c->m_rtpMedia.m_cryptoMandatory = true; + if (m_secure && m_secureRequired) + c->m_rtpMedia.m_cryptoRequired = true; if (useFormats) setMedia(c->m_rtpMedia,m_formats,s_usedCodecs); c->m_rtpLocalCandidates.m_type = c->m_rtpRemoteCandidates.m_type = type; - if (type == JGRtpCandidates::RtpRawUdp || m_useCrypto) + if (type == JGRtpCandidates::RtpRawUdp || m_secure) initLocalCandidates(*c,false); return c; @@ -3093,18 +2409,18 @@ bool YJGConnection::initLocalCandidates(JGSessionContent& content, bool sendTran m.addParam("media","audio"); m.addParam("getsession","true"); m.addParam("anyssrc","true"); - if (s_localAddress) - m.addParam("localip",s_localAddress); - else { + if (!plugin.addLocalIp(m)) { JGRtpCandidate* remote = content.m_rtpRemoteCandidates.findByComponent(1); if (remote && remote->m_address) m.addParam("remoteip",remote->m_address); +#if 0 else { String rem; getRemoteAddr(rem); if (rem) m.addParam("remoteip",rem); } +#endif } ObjList* cr = content.m_rtpMedia.m_cryptoRemote.skipNull(); if (cr) { @@ -3113,7 +2429,7 @@ bool YJGConnection::initLocalCandidates(JGSessionContent& content, bool sendTran m.addParam("crypto_suite",crypto->m_suite); m.addParam("crypto_key",crypto->m_keyParams); } - else if (m_useCrypto) + else if (m_secure) m.addParam("secure",String::boolText(true)); if (!Engine::dispatch(m)) { @@ -3127,8 +2443,12 @@ bool YJGConnection::initLocalCandidates(JGSessionContent& content, bool sendTran JGCrypto* crypto = new JGCrypto("1",*cSuite,m.getValue("ocrypto_key")); content.m_rtpMedia.m_cryptoLocal.append(crypto); } + else if (m_secure && m_secureRequired) { + // TODO: Terminate the call or try to use another content - rtp->m_address = m.getValue("localip",s_localAddress); + } + + plugin.setLocalIp(rtp->m_address,m); rtp->m_port = m.getValue("localport","-1"); if (incGeneration) { @@ -3216,7 +2536,6 @@ void YJGConnection::enqueueCallProgress() { if (!(m_audioContent && m_audioContent->isEarlyMedia())) return; - Message* m = message("call.progress"); String formats; m_audioContent->m_rtpMedia.createList(formats,true); @@ -3466,22 +2785,22 @@ void YJGConnection::handleAudioInfoEvent(JGEvent* event) bool hold = event->action() == JGSession::ActHold; if (hold || event->action() == JGSession::ActActive) { if ((hold && !dataFlags(OnHold)) || (!hold && dataFlags(OnHoldRemote))) { - XMLElement* what = event->jingle() ? event->jingle()->findFirstChild( - hold ? XMLElement::Hold : XMLElement::Active) : 0; + 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; - const char* name = what->name(); 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 - what->toList(*m,name); - m->setParam(name,String::boolText(true)); - TelEngine::destruct(what); + 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) @@ -3495,20 +2814,20 @@ void YJGConnection::handleAudioInfoEvent(JGEvent* event) resetCurrentAudioContent(true,false); } else - err = XMPPError::SFeatureNotImpl; + err = XMPPError::FeatureNotImpl; } // Respond with error if put on hold by the other party else if (dataFlags(OnHoldLocal)) { - err = XMPPError::SRequest; + err = XMPPError::Request; text = "Already on hold by the other party"; } } else if (event->action() == JGSession::ActMute) { // TODO: implement - err = XMPPError::SFeatureNotImpl; + err = XMPPError::FeatureNotImpl; } else - err = XMPPError::SFeatureNotImpl; + err = XMPPError::FeatureNotImpl; // Confirm received element if (err == XMPPError::NoError) { @@ -3516,14 +2835,28 @@ void YJGConnection::handleAudioInfoEvent(JGEvent* event) event->confirmElement(); } else { - XMPPError e; Debug(this,DebugInfo,"Denying '%s' request error='%s' reason='%s' [%p]", - event->actionName(),e[err],text,this); + 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) { + DDebug(this,DebugInfo,"Jingle version set to %s from %s", + ver->c_str(),caps ? "resource caps" : "routing"); + m_sessVersion = v; + } +} + + +/* * Transfer thread (route and execute) */ YJGTransfer::YJGTransfer(YJGConnection* conn, const char* subject) @@ -3556,7 +2889,7 @@ YJGTransfer::YJGTransfer(YJGConnection* conn, const char* subject) m_msg.addParam("diverteruri",BUILD_XMPP_URI(m_from)); if (!null(subject)) m_msg.addParam("subject",subject); - m_msg.addParam("reason",lookup(JGSession::ReasonTransfer,s_errMap)); + m_msg.addParam("reason",lookup(JGSession::Transferred,s_errMap)); } } @@ -3609,7 +2942,7 @@ void YJGTransfer::run() } // Notify termination to transferor plugin.lock(); - YJGConnection* conn = static_cast(plugin.Driver::find(m_transferorID)); + YJGConnection* conn = static_cast(plugin.find(m_transferorID)); if (conn) conn->transferTerminated(!error,error); #ifdef DEBUG @@ -3622,620 +2955,66 @@ void YJGTransfer::run() } -/** - * resource.notify message handler +/* + * JBMessageHandler */ -bool ResNotifyHandler::received(Message& msg) +YJGMessageHandler::YJGMessageHandler(int handler) + : MessageHandler(lookup(handler,s_msgHandler),handler < 0 ? 100 : handler), + m_handler(handler) { - // Avoid loopback message (if the same module: it's a message sent by this module) - if (plugin.name() == msg.getValue("module")) - return false; +} - // Check status - NamedString* status = msg.getParam("status"); - if (!status || status->null()) - return false; - - if (s_jabber && s_jabber->protocol() == JBEngine::Client) { - NamedString* account = msg.getParam("account"); - if (!account || account->null()) +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 UserRegister: + case UserUnRegister: + plugin.handleUserRegister(msg,m_handler == UserRegister); return false; - JBClientStream* stream = static_cast(s_jabber->findStream(*account)); - if (!stream) + case EngineStart: + plugin.handleEngineStart(msg); return false; - JabberID to(msg.getValue("to")); - if (!to) { - to.set(msg.getValue("contact")); - to.resource(msg.getValue("instance",to.resource())); - } - XDebug(&plugin,DebugAll,"%s account=%s to=%s status=%s", - msg.c_str(),account->c_str(),to,status->c_str()); - XMLElement* pres = 0; - bool ok = (*status == "subscribed"); - if (ok || *status == "unsubscribed") - pres = JBPresence::createPresence(0,to, - ok?JBPresence::Subscribed:JBPresence::Unsubscribed); - else { - Lock lock(stream->streamMutex()); - JIDResource* res = stream->getResource(); - if (res && res->ref()) { - res->priority(msg.getIntValue("priority",res->priority())); - if (*status != "offline") { - res->status(""); - if (*status == "online") - res->setPresence(true); - else - res->status(*status); - res->show(JIDResource::showType(msg.getValue("show"))); - } - else - res->setPresence(false); - pres = JBPresence::createPresence(stream->local().bare(),to, - res->available()?JBPresence::None:JBPresence::Unavailable); - res->addTo(pres,true); - TelEngine::destruct(res); - } - } - ok = false; - if (pres) { - plugin.addChildren(msg,pres); - JBStream::Error err = stream->sendStanza(pres); - ok = (err == JBStream::ErrorNone) || (err == JBStream::ErrorPending); - } - TelEngine::destruct(stream); - return ok; + default: + Debug(&plugin,DebugStub,"YJGMessageHandler(%s) not handled!",msg.c_str()); } - - if (!s_presence) - return false; - - JabberID from,to; - // *** Check from/to - bool broadcast = false; - if (!plugin.getJidFrom(from,msg,true)) - return false; - if (!s_presence->autoRoster()) { - to.set(msg.getValue("to")); - if (!to) { - to.set(msg.getValue("contact")); - to.resource(msg.getValue("instance",to.resource())); - } - } - else { - bool decodeTo = true; - if (s_presence->addOnPresence().to() || s_presence->addOnSubscribe().to()) { - broadcast = (0 == msg.getParam("to") && 0 == msg.getParam("contact")); - decodeTo = !broadcast; - } - if (decodeTo) { - if (msg.getParam("to")) { - if (!plugin.decodeJid(to,msg,"to")) - return false; - } - else if (plugin.decodeJid(to,msg,"contact")) - to.resource(msg.getValue("instance",to.resource())); - else - return false; - } - } - // *** Everything is OK. Process the message - XDebug(&plugin,DebugAll,"Received '%s' from '%s' with status '%s'", - msg.c_str(),from.c_str(),status->c_str()); - // Broadcast - if (broadcast) { - NamedString* status = msg.getParam("status"); - if (status && (*status == "subscribed" || *status == "unsubscribed")) - return false; - XMPPUserRoster* roster = s_presence->getRoster(from,false,0); - if (!roster) { - Debug(&plugin,DebugNote,"Can't send presence from '%s': no roster",from.c_str()); - return false; - } - bool unavail = (status && *status == "offline"); - roster->lock(); - for (ObjList* o = roster->users().skipNull(); o; o = o->skipNext()) { - XMPPUser* user = static_cast(o->get()); - const char* name = from.resource(); - if (!name) - name = s_jabber->defaultResource(); - JIDResource* res = 0; - bool changed = false; - if (name) { - changed = user->addLocalRes(new JIDResource(name, - unavail ? JIDResource::Unavailable : JIDResource::Available, - JIDResource::CapAudio),false); - res = user->localRes().get(name); - } - else - res = user->getAudio(true,true); - if (!res) - continue; - res->infoXml()->clear(); - res->priority(msg.getIntValue("priority",res->priority())); - plugin.addChildren(msg,0,res->infoXml()); - if (unavail) - changed = res->setPresence(false) || changed; - else { - changed = res->setPresence(true) || changed; - if (status && *status == "online") { - if (!res->status().null()) { - res->status(""); - changed = true; - } - } - else if (status && *status != res->status()) { - res->status(*status); - changed = true; - } - } - if (changed && user->subscription().from()) - user->sendPresence(res,0,true); - // Remove if unavailable - if (!res->available()) - user->removeLocalRes(res); - } - roster->unlock(); - TelEngine::destruct(roster); - } - else - if (s_presence->addOnPresence().to() || s_presence->addOnSubscribe().to()) - process(from,to,*status,msg.getBoolValue("subscription",false),&msg); - else - sendPresence(from,to,*status,&msg); - return true; -} - -void ResNotifyHandler::process(const JabberID& from, const JabberID& to, - const String& status, bool subFrom, NamedList* params) -{ - if (!s_presence) - return; - DDebug(&plugin,DebugAll,"ResNotifyHandler::process() from=%s to=%s status=%s", - from.c_str(),to.c_str(),status.c_str()); - - bool pres = (status != "subscribed") && (status != "unsubscribed"); - bool add = pres ? s_presence->addOnPresence().to() : s_presence->addOnSubscribe().to(); - XMPPUserRoster* roster = s_presence->getRoster(from,add,0); - if (!roster) - return; - XMPPUser* user = roster->getUser(to,false,0); - - bool newUser = (0 == user); - // Add new user - if (newUser) { - user = new XMPPUser(roster,to.node(),to.domain(), - subFrom ? XMPPDirVal::From : XMPPDirVal::None,false,false); - if (!user->ref()) - user = 0; - } - TelEngine::destruct(roster); - if (!user) - return; - Lock lock(user); - // Process - for (;;) { - // Subscription response - if (!pres) { - if (status == "subscribed") { - // Send only if not already subscribed to us - if (!user->subscription().from()) - user->sendSubscribe(JBPresence::Subscribed,0); - break; - } - if (status == "unsubscribed") { - // Send only if not already unsubscribed from us - if (user->subscription().from()) - user->sendSubscribe(JBPresence::Unsubscribed,0); - break; - } - break; - } - - // Presence - JIDResource::Presence p = (status != "offline") ? - JIDResource::Available : JIDResource::Unavailable; - const char* name = from.resource(); - if (!name) - name = s_jabber->defaultResource(); - JIDResource* res = 0; - bool changed = false; - if (name) { - changed = user->addLocalRes(new JIDResource(name,p,JIDResource::CapAudio),false); - res = user->localRes().get(name); - } - else - res = user->getAudio(true,true); - if (!res) { - DDebug(&plugin,DebugNote, - "ResNotifyHandler::process() from=%s to=%s status=%s: no resource named '%s'", - from.c_str(),to.c_str(),status.c_str(),name); - break; - } - res->infoXml()->clear(); - if (params) { - res->priority(params->getIntValue("priority",res->priority())); - plugin.addChildren(*params,0,res->infoXml()); - } - if (p == JIDResource::Unavailable) - changed = res->setPresence(false) || changed; - else { - changed = res->setPresence(true) || changed; - if (status == "online") { - if (!res->status().null()) { - res->status(""); - changed = true; - } - } - else { - if (status != res->status()) { - res->status(status); - changed = true; - } - } - } - - if (changed && user->subscription().from()) - user->sendPresence(res,0,true); - // Remove if unavailable - if (!res->available()) - user->removeLocalRes(res); - break; - } - lock.drop(); - TelEngine::destruct(user); -} - -void ResNotifyHandler::sendPresence(JabberID& from, JabberID& to, - const String& status, NamedList* params) -{ - if (!s_presence) - return; - JBPresence::Presence jbPresence; - bool command = !s_presence->autoRoster(); - // Get presence type from status - if (status == "online") - jbPresence = JBPresence::None; - else if (status == "offline") - jbPresence = JBPresence::Unavailable; - else { - if (status == "subscribed") - jbPresence = JBPresence::Subscribed; - else if (status == "unsubscribed") - jbPresence = JBPresence::Unsubscribed; - else - jbPresence = JBPresence::None; - if (command && (jbPresence != JBPresence::None)) { - XDebug(&plugin,DebugNote,"Can't send command for status='%s'",status.c_str()); - return; - } - } - // Check if we can get a stream - JBStream* stream = s_jabber->getStream(); - if (!stream) - return; - // Create XML element to be sent - bool available = (jbPresence == JBPresence::None); - XMLElement* stanza = 0; - // Build the presence element: - // Command: no 'from'/'to' - XMLElement* pres = 0; - if (!command) - pres = stanza = JBPresence::createPresence(from,to,jbPresence); - else if (s_attachPresToCmd && params) - pres = JBPresence::createPresence(0,0,jbPresence); - if (pres) { - // Create resource info if available or command - if (available) { - JIDResource* resource = new JIDResource(from.resource(),JIDResource::Available, - JIDResource::CapAudio,params ? params->getIntValue("priority") : -1); - if (status != "online") - resource->status(status); - resource->addTo(pres); - TelEngine::destruct(resource); - } - // Add extra children to presence - if (params) - plugin.addChildren(*params,pres); - } - if (command) { - if (to.domain().null()) - to.domain(s_jabber->componentServer().c_str()); - stanza = plugin.getPresenceCommand(from,to,available,pres); - } - // Send - DDebug(&plugin,DebugAll,"Sending presence%s '%s' from '%s' to '%s'", - command ? " command" : "", - String::boolText(available),from.c_str(),to.c_str()); - stream->sendStanza(stanza); - TelEngine::destruct(stream); -} - -/** - * resource.subscribe message handler - */ -bool ResSubscribeHandler::received(Message& msg) -{ - // Avoid loopback message (if the same module: it's a message sent by this module) - if (plugin.name() == msg.getValue("module")) - return false; - - // Check operation - NamedString* oper = msg.getParam("operation"); - if (!oper) - return false; - JBPresence::Presence presence; - if (*oper == "subscribe") - presence = JBPresence::Subscribe; - else if (*oper == "probe") - presence = JBPresence::Probe; - else if (*oper == "unsubscribe") - presence = JBPresence::Unsubscribe; - else - return false; - - XMLElement* pres = 0; - JBStream* stream = 0; - bool ok = false; - while (true) { - // Client stream - NamedString* account = msg.getParam("account"); - if (account) { - stream = s_jabber->findStream(*account); - if (stream) { - XDebug(&plugin,DebugAll,"%s account=%s to=%s operation=%s", - msg.c_str(),account->c_str(),msg.getValue("to"),oper->c_str()); - pres = JBPresence::createPresence(stream->local(), - msg.getValue("to"),presence); - break; - } - } - - // Component stream - if (!s_presence || s_jabber->protocol() == JBEngine::Client) - break; - JabberID from,to; - // Check from/to - if (!plugin.decodeJid(from,msg,"from",true)) - break; - if (!plugin.decodeJid(to,msg,"to")) - break; - XDebug(&plugin,DebugAll,"%s from=%s to=%s operation=%s", - msg.c_str(),from.c_str(),to.c_str(),oper->c_str()); - // Don't automatically add - if ((presence == JBPresence::Probe && !s_presence->addOnProbe().to()) || - ((presence == JBPresence::Subscribe || presence == JBPresence::Unsubscribe) && - !s_presence->addOnSubscribe().to())) { - stream = s_jabber->getStream(); - if (stream) - pres = JBPresence::createPresence(from,to,presence); - break; - } - // Add roster/user - XMPPUserRoster* roster = s_presence->getRoster(from,true,0); - XMPPUser* user = roster->getUser(to,false,0); - // Add new user and local resource - if (!user) { - user = new XMPPUser(roster,to.node(),to.domain(),XMPPDirVal::From, - false,false); - s_presence->notifyNewUser(user); - if (!user->ref()) { - TelEngine::destruct(roster); - break; - } - } - TelEngine::destruct(roster); - // Process - ok = true; - user->lock(); - for (;;) { - if (presence == JBPresence::Subscribe || - presence == JBPresence::Unsubscribe) { - bool sub = (presence == JBPresence::Subscribe); - // Already (un)subscribed: notify. NO: send request - if (sub != user->subscription().to()) { - user->sendSubscribe(presence,0); - user->probe(0); - } - else - s_presence->notifySubscribe(user,sub?JBPresence::Subscribed:JBPresence::Unsubscribed); - break; - } - // Respond if user has a resource with audio capabilities - JIDResource* res = user->getAudio(false,true); - if (res) { - user->notifyResource(true,res->name()); - break; - } - // No audio resource for remote user: send probe - // Send probe fails: Assume remote user unavailable - if (!user->probe(0)) { - XMLElement* xml = JBPresence::createPresence(to,from,JBPresence::Unavailable); - JBEvent* event = new JBEvent(JBEvent::Presence,0,xml); - s_presence->notifyPresence(event,false); - TelEngine::destruct(event); - } - break; - } - user->unlock(); - TelEngine::destruct(user); - break; - } - - if (stream && !ok) { - JBStream::Error err = stream->sendStanza(pres); - pres = 0; - ok = (err == JBStream::ErrorNone) || (err == JBStream::ErrorPending); - } - TelEngine::destruct(stream); - TelEngine::destruct(pres); - return ok; -} - -/** - * UserLoginHandler - */ -bool UserLoginHandler::received(Message& msg) -{ - if (!(s_jabber && s_jabber->protocol() == JBEngine::Client)) - return false; - if (!plugin.canHandleProtocol(msg.getValue("protocol"))) - return false; - NamedString* account = msg.getParam("account"); - if (!account || account->null()) - return false; - // Check operation - NamedString* oper = msg.getParam("operation"); - bool login = !oper || *oper == "login" || *oper == "create"; - if (!login && (!oper || (*oper != "logout" && *oper != "delete"))) - return false; - - Debug(&plugin,DebugAll,"user.login for account=%s operation=%s", - account->c_str(),oper?oper->c_str():""); - - JBClientStream* stream = static_cast(s_jabber->findStream(*account)); - bool ok = false; - if (login) { - if (!stream) - stream = s_jabber->createClientStream(msg); - else - msg.setParam("error","User already logged in"); - ok = (0 != stream) && stream->state() != JBStream::Destroy; - } - else if (stream) { - if (stream->state() == JBStream::Running) { - XMLElement* xml = JBPresence::createPresence(0,0,JBPresence::Unavailable); - stream->sendStanza(xml); - } - const char* reason = msg.getValue("reason"); - if (!reason) - reason = Engine::exiting() ? "" : "Logout"; - XMPPError::Type err = Engine::exiting()?XMPPError::Shutdown:XMPPError::NoError; - stream->terminate(true,0,err,reason,true); - ok = true; - } - TelEngine::destruct(stream); - return ok; -} - -/** - * XmppGenerateHandler - */ -bool XmppGenerateHandler::received(Message& msg) -{ - // Process only mesages not enqueued by this module - if (!s_jabber || plugin.name() == msg.getValue("module")) - return false; - - // Check protocol only if present - const char* proto = msg.getValue("protocol"); - if (proto && !plugin.canHandleProtocol(proto)) - return false; - - // Try to get a stream to sent the stanza - JBStream* stream = 0; - if (s_jabber->protocol() == JBEngine::Client) { - NamedString* account = msg.getParam("account"); - if (!account) - return false; - stream = s_jabber->findStream(*account); - } - else { - JabberID f(msg.getValue("from")); - stream = s_jabber->getStream(f.null()?0:&f); - } - if (!stream) - return false; - - // Get and send stanza - bool ok = false; - XMLElement* stanza = XMLElement::getXml(msg,true); - if (stanza) { - JBStream::Error res = stream->sendStanza(stanza,msg.getValue("id")); - ok = (res == JBStream::ErrorNone || res == JBStream::ErrorPending); - } - TelEngine::destruct(stream); - return ok; + return false; } -/** - * XmppIqHandler - */ -bool XmppIqHandler::received(Message& msg) -{ - // Process only mesages enqueued by this module - if (plugin.name() != msg.getValue("module")) - return false; - // Ignore failed stanzas - if (msg.getBoolValue("failure")) - return false; - // Respond only to type 'set' or 'get' - NamedString* type = msg.getParam("type"); - if (!type || (*type != "set" && *type != "get")) - return false; - - NamedString* account = msg.getParam("account"); - const char* from = msg.getValue("from"); - const char* to = msg.getValue("to"); - const char* id = msg.getValue("id"); - Debug(&plugin,DebugAll,"%s: account=%s from=%s to=%s id=%s returned to module", - msg.c_str(),account?account->c_str():"",from,to,id); - - JBStream* stream = 0; - if (account) - stream = s_jabber->findStream(*account); - else { - JabberID f(from); - stream = s_jabber->getStream(f.null()?0:&f); - } - if (!stream) - return false; - - // Don't send error without id or received element: - // the sender won't be able to match the response - XMLElement* recvStanza = XMLElement::getXml(msg,true); - if (id || recvStanza) { - XMLElement* stanza = XMPPUtils::createIq(XMPPUtils::IqError,to,from,id); - // Add the first child of the received element - stanza->addChild(recvStanza->removeChild()); - stanza->addChild(XMPPUtils::createError(XMPPError::TypeModify,XMPPError::SFeatureNotImpl)); - stream->sendStanza(stanza); - } - TelEngine::destruct(recvStanza); - TelEngine::destruct(stream); - // Return true to make sure nobody will respond again!!! - return true; -} - -/** +/* * YJGDriver */ -String YJGDriver::s_statusCmd[StatusCmdCount] = {"streams"}; - YJGDriver::YJGDriver() - : Driver("jingle","varchans"), m_init(false), m_singleTone(true), m_installIq(true), - m_imToChanText(false), m_ftProxy(0) + : Driver("jingle","varchans"), m_init(false), m_ftProxy(0), + m_entityCaps(0) { Output("Loaded module YJingle"); - m_statusCmd << "status " << name(); - Engine::extraPath("jingle"); + s_serverMode = !Engine::clientMode(); + if (s_serverMode) + Engine::extraPath("jabber"); } YJGDriver::~YJGDriver() { Output("Unloading module YJingle"); - TelEngine::destruct(s_jingle); - TelEngine::destruct(s_message); - TelEngine::destruct(s_presence); - TelEngine::destruct(s_clientPresence); - TelEngine::destruct(s_stream); - TelEngine::destruct(s_iqService); - TelEngine::destruct(s_jabber); + 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(""); @@ -4247,113 +3026,99 @@ void YJGDriver::initialize() m_init = true; // Init all known codecs - s_knownCodecs.add("0", "PCMU", "8000", "1", "mulaw"); - s_knownCodecs.add("2", "G726-32", "8000", "1", "g726"); - s_knownCodecs.add("3", "GSM", "8000", "1", "gsm"); - s_knownCodecs.add("4", "G723", "8000", "1", "g723"); - s_knownCodecs.add("7", "LPC", "8000", "1", "lpc10"); - s_knownCodecs.add("8", "PCMA", "8000", "1", "alaw"); - s_knownCodecs.add("9", "G722", "8000", "1", "g722"); - s_knownCodecs.add("11", "L16", "8000", "1", "slin"); - s_knownCodecs.add("15", "G728", "8000", "1", "g728"); - s_knownCodecs.add("18", "G729", "8000", "1", "g729"); - s_knownCodecs.add("31", "H261", "90000", "1", "h261"); - s_knownCodecs.add("32", "MPV", "90000", "1", "mpv"); - s_knownCodecs.add("34", "H263", "90000", "1", "h263"); - s_knownCodecs.add("98", "iLBC", "8000", "1", "ilbc"); - s_knownCodecs.add("98", "iLBC", "8000", "1", "ilbc20"); - s_knownCodecs.add("98", "iLBC", "8000", "1", "ilbc30"); - - // Jabber protocol to use - JBEngine::Protocol proto = Engine::clientMode() ? - JBEngine::Client : JBEngine::Component; - NamedString* p = sect->getParam("protocol"); - if (p) - proto = (JBEngine::Protocol)JBEngine::lookupProto(*p,proto); - - if (proto == JBEngine::Client) - m_installIq = true; - else - m_installIq = sect->getBoolValue("installiq",true); - - // Create Jabber engine and services - s_jabber = new YJBEngine(proto); - s_jingle = new YJGEngine(s_jabber,0); - s_message = new YJBMessage(s_jabber,1); - s_stream = new YJBStreamService(s_jabber,0); - // Create protocol dependent services - // Don't create presence service for client protocol: presence is kept by client streams - // Instantiate event handler for messages related to presence when running in client mode - if (s_jabber->protocol() != JBEngine::Client) - s_presence = new YJBPresence(s_jabber,0); - else - s_clientPresence = new YJBClientPresence(s_jabber,0); - if (m_installIq) - s_iqService = new YJBIqService(s_jabber,100); - - // Attach services to the engine - s_jabber->attachService(s_jingle,JBEngine::ServiceJingle); - s_jabber->attachService(s_jingle,JBEngine::ServiceWriteFail); - s_jabber->attachService(s_jingle,JBEngine::ServiceIq); - s_jabber->attachService(s_jingle,JBEngine::ServiceStream); - s_jabber->attachService(s_message,JBEngine::ServiceMessage); - if (s_presence) { - s_jabber->attachService(s_presence,JBEngine::ServicePresence); - s_jabber->attachService(s_presence,JBEngine::ServiceDisco); - } - else if (s_clientPresence) { - s_jabber->attachService(s_clientPresence,JBEngine::ServicePresence); - s_jabber->attachService(s_clientPresence,JBEngine::ServiceRoster); - } - if (s_stream) - s_jabber->attachService(s_stream,JBEngine::ServiceStream); - if (s_iqService) { - s_jabber->attachService(s_iqService,JBEngine::ServiceIq); - s_jabber->attachService(s_iqService,JBEngine::ServiceCommand); - s_jabber->attachService(s_iqService,JBEngine::ServiceDisco); - s_jabber->attachService(s_iqService,JBEngine::ServiceWriteFail); - } + 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"); + 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"); + s_knownCodecs.add("98", "iLBC", "8000", "ilbc30"); + s_jingle = new YJGEngine; + s_jingle->debugChain(this); // Driver setup - installRelay(Halt); + setup(); + installRelay(Halt); installRelay(Route); installRelay(Update); installRelay(Transfer); installRelay(ImExecute); installRelay(Progress); - installRelay(ChanNotify,"chan.notify",100); - Engine::install(new ResNotifyHandler); - Engine::install(new ResSubscribeHandler); - Engine::install(new XmppGenerateHandler); - if (s_jabber->protocol() == JBEngine::Client) - Engine::install(new UserLoginHandler); - if (m_installIq) - Engine::install(new XmppIqHandler); - setup(); + // Install handlers + for (const TokenDict* d = s_msgHandler; d->token; d++) { + YJGMessageHandler* h = new YJGMessageHandler(d->value); + Engine::install(h); + m_handlers.append(h); + } +// if (s_jabber->protocol() == JBEngine::Client) +// Engine::install(new UserLoginHandler); +// if (m_installIq) +// Engine::install(new XmppIqHandler); + + m_features.add(XMPPNamespace::Jingle); + m_features.add(XMPPNamespace::JingleError); + 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, + "http://yate.null.ro/yate"); + + (new YJGEngineWorker)->startup(); } + else + setDomains(sect->getValue("domains")); - lock(); - - // Initialize Jabber engine and services - s_jabber->initialize(); - s_jingle->initialize(); - s_message->initialize(); - if (s_presence) - s_presence->initialize(); - if (s_stream) - s_stream->initialize(); - - m_singleTone = sect->getBoolValue("singletone",true); - s_localAddress = sect->getValue("localip"); - s_anonymousCaller = sect->getValue("anonymous_caller","unk_caller"); + if (s_serverMode) + s_requestSubscribe = sect->getBoolValue("request_subscribe",true); + else + s_requestSubscribe = false; + s_singleTone = sect->getBoolValue("singletone",true); s_pendingTimeout = sect->getIntValue("pending_timeout",10000); - m_imToChanText = sect->getBoolValue("imtochantext",false); - s_attachPresToCmd = sect->getBoolValue("addpresencetocommand",false); - s_userRoster = sect->getBoolValue("user.roster",false); - s_useCrypto = sect->getBoolValue("secure_rtp",false); - s_cryptoMandatory = s_useCrypto; - s_acceptRelay = sect->getBoolValue("accept_relay",Engine::clientMode()); + 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); + 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); + + m_resources.clear(); + String resList(sect->getValue("resources","yate")); + ObjList* list = resList.split(',',false); + for (ObjList* o = list->skipNull(); o; o = o->skipNext()) { + String* tmp = static_cast(o->get()); + if (!m_resources.find(*tmp)) + m_resources.append(new String(*tmp)); + } + TelEngine::destruct(list); // Init codecs in use. Check each codec in known codecs list against the configuration s_usedCodecs.clear(); @@ -4379,18 +3144,18 @@ void YJGDriver::initialize() } int dbg = DebugInfo; - if (!s_localAddress) + if (!m_localAddress) dbg = DebugNote; if (!s_usedCodecs.count()) dbg = DebugWarn; if (debugAt(dbg)) { String s; - s << " localip=" << s_localAddress ? s_localAddress.c_str() : "MISSING"; + s << " localip=" << m_localAddress ? m_localAddress.c_str() : "MISSING"; s << " jingle_version=" << JGSession::lookupVersion(s_sessVersion); - s << " singletone=" << String::boolText(m_singleTone); + s << " singletone=" << String::boolText(s_singleTone); s << " pending_timeout=" << s_pendingTimeout; - s << " anonymous_caller=" << s_anonymousCaller; + s << " anonymous_caller=" << m_anonymousCaller; String media; if (!s_usedCodecs.createList(media,true)) media = "MISSING"; @@ -4407,139 +3172,197 @@ void YJGDriver::initialize() // Check if we have an existing stream (account) bool YJGDriver::hasLine(const String& line) const { - JBStream* stream = (line && s_jabber) ? s_jabber->findStream(line) : 0; - if (stream) - stream->deref(); - return 0 != stream; + if (!line) + return false; + Message m("jabber.account"); + m.addParam("account",line); + return Engine::dispatch(m); } // Make an outgoing calls // Build peers' JIDs and check if the destination is available bool YJGDriver::msgExecute(Message& msg, String& dest) { - // Construct JIDs + if (!msg.userData()) { + Debug(this,DebugNote,"Jingle call failed. No data channel"); + msg.setParam("error","failure"); + return false; + } JabberID caller; JabberID called; - bool available = true; - String error; - const char* errStr = "failure"; - bool sendSub = false; - while (true) { - if (!msg.userData()) { - error = "No data channel"; - break; + const char* line = msg.getValue("line"); + // 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; + } } - // Component: delay check - // Client: just check if caller/called are full JIDs - if (s_jabber->protocol() == JBEngine::Component) - break; - // Check if a stream exists. Try to get a resource for caller and/or called - JBStream* stream = 0; - NamedString* account = msg.getParam("line"); - if (account) - stream = s_jabber->findStream(*account); - if (stream) - caller.set(stream->local().node(),stream->local().domain(), - stream->local().resource()); - else { + // 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 { + // Check line + if (!TelEngine::null(line)) { + // TODO: request a full JID from stream name (account) + Message m("jabber.account"); + m.addParam("module",name()); + m.addParam("line",line); + if (Engine::dispatch(m)) { + caller.set(m.getValue("jid")); + if (!caller.isFull()) + caller.set(""); + } + if (!caller) + DDebug(this,DebugInfo,"No stream for line=%s",line); + } + if (!caller) caller.set(msg.getValue("caller")); - stream = s_jabber->getStream(&caller,false); - } - if (!(stream && stream->type() == JBEngine::Client)) { - error = "No stream"; - TelEngine::destruct(stream); - break; - } - if (!caller.resource()) { - Debug(this,DebugAll,"Set resource '%s' for caller '%s'", - stream->local().resource().c_str(),caller.c_str()); - caller.resource(stream->local().resource()); - } - called.set(dest); - // Check if it's the same user - if (caller.bare() &= called.bare()) { - if (!called.resource()) { - XMPPUserRoster* roster = (static_cast(stream))->roster(); - roster->ref(); - Lock2 lock(roster,&roster->resources()); - JIDResource* res = roster->resources().getAudio(true); - if (res) - called.resource(res->name()); - lock.drop(); - TelEngine::destruct(roster); - } - if (!called.resource()) { - error = "No resource available for called party"; - errStr = "offline"; - } - else if (caller.resource() == called.resource()) - error = "Can't call the same resource"; - break; - } - // No resource: - // Check if we have it in the roster - // Declare unavailable if the caller is subscribed to called's presence - if (!(called.resource() || - getClientTargetResource(static_cast(stream),called,&sendSub) || - sendSub)) { - error = "No resource available for called party"; - errStr = "offline"; - } - if (sendSub) - available = false; - else if (error.null() && !(caller.isFull() && called.isFull())) - error << "Incomplete caller=" << caller << " or called=" << called; - TelEngine::destruct(stream); - break; + } + if (!caller.isFull()) { + Debug(this,DebugNote,"Jingle call failed. Incomplete caller '%s'", + caller.c_str()); + msg.setParam("error","failure"); + return false; + } + called.set(dest); + if (!called.node()) { + 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; - if (!error) { - String* format = msg.getParam("format"); - if (format && *format == "data") { - // Check file. Remove path if present - file = msg.getValue("file_name"); - int pos = file.rfind('/'); - if (pos == -1) - pos = file.rfind('\\'); - if (pos != -1) - file = file.substr(pos + 1); - if (file.null()) - error << "File transfer request with no file"; + String* format = msg.getParam("format"); + if (format && *format == "data") { + // Check file. Remove path if present + file = msg.getValue("file_name"); + 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; } } - if (error) { - Debug(this,DebugNote,"Jingle call failed. %s",error.c_str()); - msg.setParam("error",errStr ? errStr : "noconn"); - return false; + bool online = !called.resource().null(); + bool local = (caller.domain() == called.domain()); + NamedList caps(""); + 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"); + Debug(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; + } } - // Component: prepare caller/called. check availability // Lock driver to prevent probe response to be processed before the channel // is fully built Lock lock(this); - if (s_jabber->protocol() == JBEngine::Component) - setComponentCall(caller,called,msg.getValue("caller"),dest,available,error); - if (error) { - Debug(this,DebugNote,"Jingle call failed. %s",error.c_str()); - msg.setParam("error",errStr ? errStr : "noconn"); - return false; - } Debug(this,DebugAll, - "msgExecute. caller='%s' called='%s' available=%s filetransfer=%s", - caller.c_str(),called.c_str(),String::boolText(available), + "msgExecute. caller='%s' called='%s' online=%s filetransfer=%s", + caller.c_str(),called.c_str(),String::boolText(online), String::boolText(!file.null())); - // Send subscribe - if (sendSub) { - JBStream* stream = s_jabber->getStream(&caller,false); - if (stream) - stream->sendStanza(JBPresence::createPresence(caller.bare(),called.bare(), - JBPresence::Subscribe)); - TelEngine::destruct(stream); - } - YJGConnection* conn = new YJGConnection(msg,caller,called,available,file); + YJGConnection* conn = new YJGConnection(msg,caller,called,online,caps,file); bool ok = conn->state() != YJGConnection::Terminated; lock.drop(); if (ok) { @@ -4551,7 +3374,7 @@ bool YJGDriver::msgExecute(Message& msg, String& dest) } } else { - Debug(this,DebugNote,"Jingle call failed to initialize. error=%s", + Debug(this,DebugNote,"Jingle call failed to initialize error=%s", conn->reason().c_str()); msg.setParam("error","failure"); } @@ -4559,448 +3382,346 @@ bool YJGDriver::msgExecute(Message& msg, String& dest) return ok; } -// Send IM messages -bool YJGDriver::imExecute(Message& msg, String& dest) -{ - // Construct JIDs - JabberID caller(msg.getValue("caller")); - JabberID called(dest); - String error; - const char* errStr = "failure"; - JBStream* stream = 0; - while (true) { - // Component: prepare/check caller/called - if (s_jabber->protocol() == JBEngine::Component) { - stream = s_jabber->getStream(0,false); - if (!stream) - error = "No stream"; - // Check caller: - // No node: use its domain part as node - if (!caller.node() && caller.domain()) { - String domain; - if (!s_jabber->getServerIdentity(domain,!s_presence->autoRoster())) { - error = "No default server"; - break; - } - String node = caller.domain(); - String res = caller.resource(); - caller.set(node,domain,res); - } - if (!caller.bare()) { - error << "Invalid caller=" << caller; - break; - } - if (!called) { - error = "called is empty"; - break; - } - break; - } - // Check if a stream exists - NamedString* account = msg.getParam("line"); - if (account) - stream = s_jabber->findStream(*account); - if (!stream) - stream = s_jabber->getStream(&caller,false); - if (!(stream && stream->type() == JBEngine::Client)) { - error = "No stream"; - break; - } - // Reset caller - caller.set(""); - // Caller must be at least bare JIDs - if (!(called.node() && called.domain())) - error << "Incomplete called=" << called; - break; - } - // Send the message - if (!error) { - const char* t = msg.getValue("xmpp_type",msg.getValue("type")); - const char* id = msg.getValue("id"); - const char* stanzaId = msg.getValue("xmpp_id",id); - JBMessage::MsgType type = JBMessage::msgType(t); - XMLElement* im = 0; - if (type == JBMessage::None) { - if (!t) - im = JBMessage::createMessage(JBMessage::Chat,caller,called,stanzaId,0); - else - im = JBMessage::createMessage(t,caller,called,stanzaId,0); - } - else - im = JBMessage::createMessage(type,caller,called,stanzaId,0); - const char* subject = msg.getValue("subject"); - if (subject) - im->addChild(new XMLElement(XMLElement::Subject,0,subject)); - NamedString* b = msg.getParam("body"); - XMLElement* body = 0; - if (b) { - body = new XMLElement(XMLElement::Body,0,*b); - NamedPointer* np = static_cast(b->getObject("NamedPointer")); - MimeStringBody* sb = static_cast(np ? np->userObject("MimeStringBody") : 0); - if (sb) { - String name = sb->getType(); - name.startSkip("text/",false); - body->addChild(new XMLElement(name,0,sb->text())); - } - } - im->addChild(body); - JBStream::Error result = stream->sendStanza(im,id); - if (result == JBStream::ErrorContext || result == JBStream::ErrorNoSocket) - error = "Failed to send message"; - } - TelEngine::destruct(stream); - if (!error) - return true; - Debug(this,DebugNote,"Jabber message failed. %s",error.c_str()); - msg.setParam("error",errStr?errStr:"noconn"); - return false; -} - -// Handle command complete requests -bool YJGDriver::commandComplete(Message& msg, const String& partLine, - const String& partWord) -{ - bool status = partLine.startsWith("status"); - bool drop = !status && partLine.startsWith("drop"); - if (!(status || drop)) - return Driver::commandComplete(msg,partLine,partWord); - - // 'status' command - Lock lock(this); - // line='status jingle': add additional commands - if (partLine == m_statusCmd) { - for (unsigned int i = 0; i < StatusCmdCount; i++) - if (!partWord || s_statusCmd[i].startsWith(partWord)) - msg.retValue().append(s_statusCmd[i],"\t"); - return true; - } - - if (partLine != "status" && partLine != "drop") - return false; - - // Empty partial word or name start with it: add name and prefix - if (!partWord || name().startsWith(partWord)) { - msg.retValue().append(name(),"\t"); - if (channels().skipNull()) - msg.retValue().append(prefix(),"\t"); - return false; - } - - // Partial word starts with module prefix: add channels - if (partWord.startsWith(prefix())) { - for (ObjList* o = channels().skipNull(); o; o = o->skipNext()) { - CallEndpoint* c = static_cast(o->get()); - if (c->id().startsWith(partWord)) - msg.retValue().append(c->id(),"\t"); - } - return true; - } - return false; -} - -// Check and build caller and called for Component run mode -// Caller: Set user if missing. Get default server identity for Yate Component -// Try to get an available resource for the called party -bool YJGDriver::setComponentCall(JabberID& caller, JabberID& called, - const char* cr, const char* cd, bool& available, String& error) -{ - // Get identity for default server - String domain; - if (!s_jabber->getServerIdentity(domain,!s_presence->autoRoster())) { - error = "No default server"; - return false; - } - if (!cr) - cr = s_anonymousCaller; - // Validate caller's JID - if (!(cr && JabberID::valid(cr))) { - error << "Invalid caller=" << cr; - return false; - } - JabberID tmp(cr); - if (tmp.node()) - caller.set(tmp.node(),domain,tmp.resource()); - else - caller.set(tmp.domain(),domain,tmp.resource()); - called.set(cd); - - // Get an available resource for the remote user if we keep the roster - // Send subscribe and probe if not - if (s_presence->autoRoster()) { - // Get remote user - bool newPresence = false; - XMPPUser* remote = s_presence->getRemoteUser(caller,called,true,0, - true,&newPresence); - if (!remote) { - error = "Remote user is unavailable"; - return false; - } - // Get a resource for the caller - JIDResource* res = remote->getAudio(true,true); - if (!res) { - s_presence->notifyNewUser(remote); - res = remote->getAudio(true,true); - // This should never happen !!! - if (!res) { - TelEngine::destruct(remote); - error = "Unable to get a resource for the caller"; - return false; - } - } - caller.resource(res->name()); - // Get a resource for the called - res = remote->getAudio(false,true); - available = (res != 0); - if (!(newPresence || available)) { - if (!s_jingle->requestSubscribe()) { - TelEngine::destruct(remote); - error = "Remote peer is unavailable"; - return false; - } - remote->sendSubscribe(JBPresence::Subscribe,0); - } - if (available) - called.resource(res->name()); - else - if (!newPresence) - remote->probe(0); - TelEngine::destruct(remote); - } - else { - available = false; - // Get stream for default component - JBStream* stream = s_jabber->getStream(); - if (!stream) { - error << "No stream for called=" << called; - return false; - } - if (!caller.resource()) - caller.resource(s_jabber->defaultResource()); - // Send subscribe request and probe - XMLElement* xml = 0; - if (s_jingle->requestSubscribe()) { - xml = JBPresence::createPresence(caller.bare(),called.bare(),JBPresence::Subscribe); - stream->sendStanza(xml); - } - xml = JBPresence::createPresence(caller.bare(),called.bare(),JBPresence::Probe); - stream->sendStanza(xml); - TelEngine::destruct(stream); - } - return true; -} - // Message handler: Disconnect channels, destroy streams, clear rosters bool YJGDriver::received(Message& msg, int id) { - // Execute: accept - if (id == Execute) { - String dest; - if (getExecuteDest(msg,dest)) - return msgExecute(msg,dest); - return Driver::received(msg,Execute); - } - - // Send message - if (id == ImExecute) { - String dest; - if (getExecuteDest(msg,dest)) - return imExecute(msg,dest); - return Driver::received(msg,Execute); - } - - if (id == Status) { - String target = msg.getValue("module"); - // Target is the driver or channel - if (!target || target == name() || target.startsWith(prefix())) - return Driver::received(msg,id); - - // Check additional commands - if (!target.startSkip(name(),false)) - return false; - target.trimBlanks(); - int cmd = 0; - for (; cmd < StatusCmdCount; cmd++) - if (s_statusCmd[cmd] == target) - break; - - // Show streams - if (cmd == StatusStreams && s_jabber) { - msg.retValue().clear(); - msg.retValue() << "name=" << name(); - msg.retValue() << ",type=" << type(); - msg.retValue() << ",format=Account|State|Local|Remote"; - s_jabber->lock(); - msg.retValue() << ";count=" << s_jabber->streams().count(); - for (ObjList* o = s_jabber->streams().skipNull(); o; o = o->skipNext()) { - JBStream* stream = static_cast(o->get()); - msg.retValue() << ";" << JBEngine::lookupProto(stream->type()); - msg.retValue() << "=" << stream->name(); - msg.retValue() << "|" << JBStream::lookupState(stream->state()); - msg.retValue() << "|" << stream->local(); - msg.retValue() << "|" << stream->remote(); - } - s_jabber->unlock(); - msg.retValue() << "\r\n"; - return true; + if (id == ImExecute) + return !isModule(msg) && handleImExecute(msg); + if (id == Halt) { + // Uninstall message handlers + for (ObjList* o = m_handlers.skipNull(); o; o = o->skipNext()) { + YJGMessageHandler* h = static_cast(o->get()); + Engine::uninstall(h); } - } - else if (id == ChanNotify) { - String* module = msg.getParam("module"); - if (module && *module == name()) - return false; - String* chan = msg.getParam("notify"); - if (!chan) - return false; - YJGConnection* ch = static_cast(Driver::find(*chan)); - if (!ch) - return false; - ch->processChanNotify(msg); - if (ch->state() == YJGConnection::Terminated) - ch->disconnect(0); - return true; - } - else if (id == Halt) { dropAll(msg); - if (s_presence) - s_presence->cleanup(); - s_jabber->cleanup(); - s_jabber->cancelThreads(); - s_jingle->cancelThreads(); - if (s_presence) - s_presence->cancelThreads(); - s_jabber->detachService(s_presence); - s_jabber->detachService(s_jingle); - s_jabber->detachService(s_message); - s_jabber->detachService(s_stream); - s_jabber->detachService(s_clientPresence); - s_jabber->detachService(s_iqService); } return Driver::received(msg,id); } -bool YJGDriver::getJidFrom(JabberID& jid, Message& msg, bool checkDomain) +// Handle jabber.iq messages +bool YJGDriver::handleJabberIq(Message& msg) { - String username = msg.getValue("username"); - if (username.null()) - return decodeJid(jid,msg,"from",checkDomain); - String domain; - s_jabber->getServerIdentity(domain,true); - const char* res = msg.getValue("resource",s_jabber->defaultResource()); - jid.set(username,domain,res); - return true; -} + JabberID to(msg.getValue("to")); + if (!(to.domain() && handleDomain(to.domain()))) + return false; + if (!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; + } -bool YJGDriver::decodeJid(JabberID& jid, Message& msg, const char* param, - bool checkDomain) -{ - jid.set(msg.getValue(param)); - if (jid.node().null() || jid.domain().null()) { - Debug(this,DebugNote,"'%s'. Parameter '%s'='%s' is an invalid JID", - msg.c_str(),param,jid.c_str()); + // 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,"YJGDriver::handleJabberIq() no xml element"); return false; } - if (checkDomain && !(s_presence && s_presence->validDomain(jid.domain()))) { - Debug(this,DebugNote,"'%s'. Parameter '%s'='%s' has invalid (unknown) domain", - msg.c_str(),param,jid.c_str()); - 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); + const char* node = query ? query->attribute("node") : 0; + if (TelEngine::null(node)) { + if (discoInfo) + rsp = m_features.buildDiscoInfo(0,0,id); + else { + XmlElement* query = XMPPUtils::createElement(XmlTag::Query, + XMPPNamespace::DiscoItems); + rsp = XMPPUtils::createIqResult(0,0,id,query); + } + } + else { + XmlElement* query = XMPPUtils::createElement(XmlTag::Query); + query->setXmlns(xmlns); + query->setAttribute("node",node); + rsp = XMPPUtils::createIqResult(0,0,id,query); + } + TelEngine::destruct(xml); + msg.setParam(new NamedPointer("response",rsp)); + return true; } - 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,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; } -XMLElement* YJGDriver::getPresenceCommand(JabberID& from, JabberID& to, - bool available, XMLElement* presence) +// Handle resource.notify messages +bool YJGDriver::handleResNotify(Message& msg) { - // Used only for debug purposes - static int idCrt = 1; - // Create 'x' child - XMLElement* x = new XMLElement("x"); - x->setAttribute("xmlns","jabber:x:data"); - x->setAttribute("type","submit"); - // Field children of 'x' element - XMLElement* field = new XMLElement("field"); - field->setAttribute("var","jid"); - XMLElement* value = new XMLElement("value",0,from); - field->addChild(value); - x->addChild(field); - field = new XMLElement("field"); - field->setAttribute("var","available"); - value = new XMLElement("value",0,available ? "true" : "false"); - field->addChild(value); - x->addChild(field); - // 'command' stanza - XMLElement* command = XMPPUtils::createElement(XMLElement::Command,XMPPNamespace::Command); - command->setAttribute("node","USER_STATUS"); - command->addChild(x); - // Add other children - if (presence) - command->addChild(presence); - // 'iq' stanza - String id(idCrt++); - String domain; - if (s_jabber->getServerIdentity(domain,false)) - from.domain(domain); - XMLElement* iq = XMPPUtils::createIq(XMPPUtils::IqSet,from,to,id); - iq->addChild(command); - return iq; -} - -void YJGDriver::processPresence(const JabberID& local, const JabberID& remote, - bool available, bool audio) -{ - // Check if it is a brodcast and remote user has a resource - bool broadcast = local.null(); - bool remoteRes = !remote.resource().null(); - DDebug(this,DebugAll,"Presence (%s). Local: '%s'. Remote: '%s'.", - available?"available":"unavailable",local.c_str(),remote.c_str()); - // If a remote user became available notify only pending connections - // that match local bare jid and remote bare jid - // No need to notify if remote user has no resource or no audio capability - if (available) { - if (!remoteRes || !audio) - return; + 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")); + JabberID local; + if (remote) + remote.resource(msg.getValue("instance")); + else { + local.set(msg.getValue("to")); + lock(); + if (!handleDomain(local.domain())) + return false; + unlock(); + 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(); + for (ObjList* o = channels().skipNull(); o; o = o->skipNext()) { + YJGConnection* conn = static_cast(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); + } + } + unlock(); + return false; + } + // Offline + // Remote user is unavailable: notify all connections + // Remote has no resource: match connections by bare jid lock(); - ObjList* obj = channels().skipNull(); - for (; obj; obj = obj->skipNext()) { - YJGConnection* conn = static_cast(obj->get()); - if (conn->state() != YJGConnection::Pending || - (!broadcast && (local.bare() |= conn->local().bare())) || - (remote.bare() |= conn->remote().bare())) - continue; - conn->updateResource(remote.resource()); - if (conn->presenceChanged(true)) - conn->disconnect(0); + for (ObjList* o = channels().skipNull(); o; o = o->skipNext()) { + YJGConnection* conn = static_cast(o->get()); + if (conn->remote().match(remote) && (!local || + local.bare() != conn->local().bare())) { + if (conn->presenceChanged(false)) + conn->disconnect(0); + } } unlock(); - return; + return false; } - // Remote user is unavailable: notify all connections - // Remote has no resource: match connections by bare jid - lock(); - ObjList* obj = channels().skipNull(); - for (; obj; obj = obj->skipNext()) { - YJGConnection* conn = static_cast(obj->get()); - if ((!broadcast && local.bare() != conn->local().bare()) || - !conn->remote().match(remote)) - continue; - if (conn->presenceChanged(false)) - conn->disconnect(0); + 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; } - unlock(); + // probe + if (*oper == "probe") { + // TODO: send presence for users in our domain + } + return false; } -YJGConnection* YJGDriver::find(const JabberID& local, const JabberID& remote, bool anyResource) +// Handle resource.subscribe messages +bool YJGDriver::handleResSubscribe(Message& msg) { - String bareJID = local.bare(); - if (bareJID == local) + String* oper = msg.getParam("operation"); + String* subscriber = msg.getParam("subscriber"); + String* notifier = msg.getParam("notifier"); + if (TelEngine::null(oper) || TelEngine::null(subscriber) || TelEngine::null(notifier)) + return false; + bool sub = (*oper == "subscribe"); + if (sub || *oper == "unsubscribe") { + return false; + } + if (*oper == "query") { + return false; + } + return false; +} + +// Handle chan.notify messages +bool YJGDriver::handleChanNotify(Message& msg) +{ + String* chan = msg.getParam("notify"); + if (!chan) + return false; + YJGConnection* ch = static_cast(find(*chan)); + if (!ch) + return false; + ch->processChanNotify(msg); + if (ch->state() == YJGConnection::Terminated) + ch->disconnect(0); + 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")); + if (local && !local.resource()) + local.resource(msg.getValue("called_instance")); + } + else { + if (!callto->startsWith(prefix())) + return false; + local.set(callto->substr(prefix().length())); + } + if (!local) + return false; + Message* m = 0; + lock(); + // 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"); + } + unlock(); + if (m) { + m->addParam("text",msg.getValue("body")); + Engine::enqueue(m); + } + return m != 0; +} + +// Handle user.(un)unregister messages. Send resource.notify if enabled +bool YJGDriver::handleUserRegister(Message& msg, bool reg) +{ + if (!msg.getParam("driver")) + return false; + const char* user = msg.getValue("number",msg.getValue("username")); + if (TelEngine::null(user)) + return false; + lock(); + String domain; + ObjList* o = m_domains.skipNull(); + if (o) + domain = static_cast(o->get()); + unlock(); + if (!domain) + return false; + String tmp; + defaultResource(tmp); + if (!tmp) + return false; + Message* m = message("resource.notify"); + m->addParam("operation",reg ? "online" : "offline"); + m->addParam("contact",JabberID(user,domain)); + m->addParam("instance",tmp); + if (reg) { + m->addParam("priority",s_priority); + XmlElement* xml = XMPPUtils::createPresence(0,0); + XMPPUtils::setPriority(*xml,s_priority); + xml->addChild(XMPPUtils::createEntityCapsGTalkV1()); + xml->addChild(new XmlElement(*m_entityCaps)); + String data; + xml->toString(data); + m->addParam("data",data); + m->addParam(new NamedPointer("xml",xml)); + } + Engine::enqueue(m); + return false; +} + +// Handle engine.start message +void YJGDriver::handleEngineStart(Message& msg) +{ + setDomains(s_cfg.getValue("general","domains")); +} + +// Dispatch jabber.iq messages +bool YJGDriver::dispatchJabberIq(JGSession* session, XmlElement*& xml) +{ + if (!(session && xml)) { + TelEngine::destruct(xml); + return false; + } + DDebug(this,DebugAll,"dispatchJabberIq() session=(%p,%s) xml=%p", + session,session->sid().c_str(),xml); + Message m("jabber.iq"); + m.addParam("module",name()); + 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()); + m.addParam(new NamedPointer("xml",xml)); + return Engine::dispatch(m); +} + +// 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; - Lock lock(this); ObjList* obj = channels().skipNull(); for (; obj; obj = obj->skipNext()) { YJGConnection* conn = static_cast(obj->get()); if (!conn->remote().match(remote)) continue; if (anyResource) { - if (bareJID &= conn->local().bare()) + if (local.bare() == conn->local().bare()) return conn; } else if (conn->local().match(local)) @@ -5009,148 +3730,6 @@ YJGConnection* YJGDriver::find(const JabberID& local, const JabberID& remote, bo return 0; } -// Build an XML element from a received message -bool YJGDriver::addChildren(NamedList& msg, XMLElement* xml, ObjList* list) -{ - String prefix = msg.getValue("message-prefix"); - if (!(prefix && (xml || list))) - return false; - - prefix << "."; - bool added = false; - for (unsigned int i = 1; i < 0xffffffff; i++) { - String childPrefix(prefix + String(i)); - if (!msg.getValue(childPrefix)) - break; - XMLElement* child = new XMLElement(msg,childPrefix); - if (xml) - xml->addChild(child); - else - list->append(child); - added = true; - } - return added; -} - -// Get the destination from a call/im execute message -bool YJGDriver::getExecuteDest(Message& msg, String& dest) -{ - NamedString* callto = msg.getParam("callto"); - if (!callto) - return false; - int pos = callto->find('/'); - if (pos < 1) - return false; - dest = callto->substr(0,pos); - if (!canHandleProtocol(dest)) - return false; - dest = callto->substr(pos + 1); - return true; -} - -// Process a message received by a stream -void YJGDriver::processImMsg(JBEvent& event) -{ - DDebug(this,DebugInfo,"Message from=%s to=%s '%s'", - event.from().c_str(),event.to().c_str(),event.text().c_str()); - - if (!event.text()) - return; - - //JBMessage::MsgType type = JBMessage::msgType(event.stanzaType()); - - Message* m = 0; - YJGConnection* conn = 0; - while (m_imToChanText) { - conn = find(event.to().c_str(),event.from().c_str()); - if (!conn) - break; - DDebug(this,DebugInfo,"Found conn=%p for message from=%s to=%s", - conn,event.from().c_str(),event.to().c_str()); - m = conn->message("chan.text"); - m->addParam("text",event.text()); - break; - } - if (!m) { - m = new Message("msg.execute"); - m->addParam("caller",event.from()); - m->addParam("called",event.to()); - m->addParam("module",name()); - m->addParam("contact",event.from().bare()); - if (event.from().resource()) - m->addParam("instance",event.from().resource()); - String billid; - billid << Engine::runId() << "-" << Channel::allocId(); - m->addParam("billid",billid); - } - - if (event.stream()) - m->addParam("account",event.stream()->name()); - - // Fill the message - if (event.id()) - m->addParam("id",event.id()); - if (event.stanzaType()) - m->addParam("type",event.stanzaType()); - XMLElement* xml = event.element(); - XMLElement* body = 0; - if (xml) { - XMLElement* e = xml->findFirstChild(XMLElement::Subject); - if (e) - m->addParam("subject",e->getText()); - TelEngine::destruct(e); - body = xml->findFirstChild(XMLElement::Body); - } - // FIXME: the body child may be repeated - NamedPointer* p = new NamedPointer("body"); - if (body) { - *p = body->getText(); - // FIXME: the body may have more then 1 child - XMLElement* tmp = body->findFirstChild(); - if (tmp) - p->userData(new MimeStringBody("text/" + String(tmp->name()),tmp->getText())); - TelEngine::destruct(tmp); - } - m->addParam(p); - TelEngine::destruct(body); - if (conn) - Engine::enqueue(m); - else { - Engine::dispatch(*m); - TelEngine::destruct(m); - } -} - -// 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 YJGDriver::getClientTargetResource(JBClientStream* stream, - JabberID& target, bool* noSub) -{ - if (!stream) - return false; - XMPPUser* user = stream->getRemote(target); - if (noSub) - *noSub = (0 == user); - if (!user) - return false; - user->lock(); - // Get an audio resource if available - if (!target.resource()) { - JIDResource* res = user->getAudio(false); - if (res) - target.resource(res->name()); - } - // No resource: check subscription to - if (!target.resource() && noSub) - *noSub = !user->subscription().to(); - user->unlock(); - TelEngine::destruct(user); - return !target.resource().null(); -} - // Find a channel by its sid YJGConnection* YJGDriver::findBySid(const String& sid) { @@ -5165,6 +3744,43 @@ YJGConnection* YJGDriver::findBySid(const String& sid) return 0; } +// 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(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(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(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(ores->get())); + Engine::enqueue(m); + } + } + TelEngine::destruct(l); +} + }; // anonymous namespace /* vi: set ts=8 sw=4 sts=4 noet: */