From d6966c6347c0781dde4a7a3f49b05916189759a4 Mon Sep 17 00:00:00 2001 From: marian Date: Mon, 2 Nov 2009 16:05:39 +0000 Subject: [PATCH] Implemented jabber server. Changed jingle channel to support the new jabber library. Replaced tinyxml with yate own XML library. Added openssl support for server. Work in progress in jabber client support. git-svn-id: http://yate.null.ro/svn/yate/trunk@2882 acf43c95-373e-0410-b603-e72c3f656dc1 --- Makefile.in | 2 +- conf.d/jabberclient.conf.sample | 54 + conf.d/jabberserver.conf.sample | 106 + conf.d/jbfeatures.conf.sample | 96 + conf.d/openssl.conf.sample | 24 + configure.in | 2 +- libs/{yjingle => yjabber}/Makefile.in | 10 +- libs/yjabber/jbengine.cpp | 2271 ++++++++++++++++ libs/yjabber/jbstream.cpp | 2509 ++++++++++++++++++ libs/yjabber/jgengine.cpp | 323 +++ libs/{yjingle => yjabber}/session.cpp | 1678 ++++++------ libs/yjabber/xmpputils.cpp | 1564 +++++++++++ libs/yjabber/xmpputils.h | 2129 +++++++++++++++ libs/yjabber/yatejabber.h | 2476 +++++++++++++++++ libs/{yjingle => yjabber}/yatejingle.h | 690 +++-- libs/yjingle/.cvsignore | 8 - libs/yjingle/jbengine.cpp | 2519 ------------------ libs/yjingle/jbstream.cpp | 2384 ----------------- libs/yjingle/jgengine.cpp | 377 --- libs/yjingle/xmlparser.cpp | 487 ---- libs/yjingle/xmlparser.h | 704 ----- libs/yjingle/xmpputils.cpp | 886 ------- libs/yjingle/xmpputils.h | 1220 --------- libs/yjingle/yatejabber.h | 2663 ------------------- libs/yxml/Makefile.in | 4 +- libs/yxml/XML.cpp | 2018 ++++++++++++++ libs/yxml/tinystr.cpp | 117 - libs/yxml/tinystr.h | 342 --- libs/yxml/tinyxml.cpp | 1816 ------------- libs/yxml/tinyxml.h | 1596 ----------- libs/yxml/tinyxmlparser.cpp | 1625 ------------ libs/yxml/yatexml.h | 1609 ++++++++++++ modules/Makefile.in | 15 +- modules/client/jabberclient.cpp | 1417 ++++++++++ modules/jabber/jabberserver.cpp | 3107 ++++++++++++++++++++++ modules/jabber/jbfeatures.cpp | 821 ++++++ modules/jingle/jinglefeatures.cpp | 833 ------ modules/openssl.cpp | 314 ++- modules/server/presence.cpp | 910 +++++++ modules/server/subscription.cpp | 2595 ++++++++++++++++++ modules/server/users.cpp | 474 ++++ modules/yjinglechan.cpp | 3360 +++++++----------------- 42 files changed, 27068 insertions(+), 21087 deletions(-) create mode 100644 conf.d/jabberclient.conf.sample create mode 100644 conf.d/jabberserver.conf.sample create mode 100644 conf.d/jbfeatures.conf.sample create mode 100644 conf.d/openssl.conf.sample rename libs/{yjingle => yjabber}/Makefile.in (78%) create mode 100644 libs/yjabber/jbengine.cpp create mode 100644 libs/yjabber/jbstream.cpp create mode 100644 libs/yjabber/jgengine.cpp rename libs/{yjingle => yjabber}/session.cpp (54%) create mode 100644 libs/yjabber/xmpputils.cpp create mode 100644 libs/yjabber/xmpputils.h create mode 100644 libs/yjabber/yatejabber.h rename libs/{yjingle => yjabber}/yatejingle.h (71%) delete mode 100644 libs/yjingle/.cvsignore delete mode 100644 libs/yjingle/jbengine.cpp delete mode 100644 libs/yjingle/jbstream.cpp delete mode 100644 libs/yjingle/jgengine.cpp delete mode 100644 libs/yjingle/xmlparser.cpp delete mode 100644 libs/yjingle/xmlparser.h delete mode 100644 libs/yjingle/xmpputils.cpp delete mode 100644 libs/yjingle/xmpputils.h delete mode 100644 libs/yjingle/yatejabber.h create mode 100644 libs/yxml/XML.cpp delete mode 100644 libs/yxml/tinystr.cpp delete mode 100644 libs/yxml/tinystr.h delete mode 100644 libs/yxml/tinyxml.cpp delete mode 100644 libs/yxml/tinyxml.h delete mode 100644 libs/yxml/tinyxmlparser.cpp create mode 100644 libs/yxml/yatexml.h create mode 100644 modules/client/jabberclient.cpp create mode 100644 modules/jabber/jabberserver.cpp create mode 100644 modules/jabber/jbfeatures.cpp delete mode 100644 modules/jingle/jinglefeatures.cpp create mode 100644 modules/server/presence.cpp create mode 100644 modules/server/subscription.cpp create mode 100644 modules/server/users.cpp 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: */