Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYann Leboulanger <asterix@lagaule.org>2011-12-29 14:39:02 +0400
committerYann Leboulanger <asterix@lagaule.org>2011-12-29 14:39:02 +0400
commit49bc20242163746cefe89c9d8107b0308676f7b7 (patch)
tree89878c2055358638550c3e7c40295727abe9cb67 /src/common/xmpp
parentc491bd3f59a1bfa442644b722abe3386cfb8f038 (diff)
parentd621252a0d01f63dbbdcc91a0773ae0bd1532af1 (diff)
merge from trunk
Diffstat (limited to 'src/common/xmpp')
-rw-r--r--src/common/xmpp/__init__.py1
-rw-r--r--src/common/xmpp/auth_nb.py186
-rw-r--r--src/common/xmpp/bosh.py18
-rw-r--r--src/common/xmpp/client_nb.py13
-rw-r--r--src/common/xmpp/dispatcher_nb.py40
-rw-r--r--src/common/xmpp/protocol.py116
-rw-r--r--src/common/xmpp/roster_nb.py16
-rw-r--r--src/common/xmpp/smacks.py130
-rw-r--r--src/common/xmpp/stringprepare.py4
-rw-r--r--src/common/xmpp/tls_nb.py3
10 files changed, 420 insertions, 107 deletions
diff --git a/src/common/xmpp/__init__.py b/src/common/xmpp/__init__.py
index 4ebc4cfa3..46037a1cb 100644
--- a/src/common/xmpp/__init__.py
+++ b/src/common/xmpp/__init__.py
@@ -15,3 +15,4 @@ import simplexml, protocol, auth_nb, transports_nb, roster_nb
import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb, proxy_connectors
from client_nb import NonBlockingClient
from plugin import PlugIn
+from smacks import Smacks
diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py
index 670526bf9..1063ed9a7 100644
--- a/src/common/xmpp/auth_nb.py
+++ b/src/common/xmpp/auth_nb.py
@@ -22,8 +22,10 @@ See client_nb.py
"""
from protocol import NS_SASL, NS_SESSION, NS_STREAMS, NS_BIND, NS_AUTH
+from protocol import NS_STREAM_MGMT
from protocol import Node, NodeProcessed, isResultNode, Iq, Protocol, JID
from plugin import PlugIn
+from smacks import Smacks
import base64
import random
import itertools
@@ -40,13 +42,14 @@ def H(some): return hashlib.md5(some).digest()
def C(some): return ':'.join(some)
try:
- import kerberos
+ kerberos = __import__('kerberos')
have_kerberos = True
except ImportError:
have_kerberos = False
GSS_STATE_STEP = 0
GSS_STATE_WRAP = 1
+SASL_FAILURE_IN_PROGRESS = 'failure-in-process'
SASL_FAILURE = 'failure'
SASL_SUCCESS = 'success'
SASL_UNSUPPORTED = 'not-supported'
@@ -142,7 +145,7 @@ class SASL(PlugIn):
elif self._owner.Dispatcher.Stream.features:
try:
self.FeaturesHandler(self._owner.Dispatcher,
- self._owner.Dispatcher.Stream.features)
+ self._owner.Dispatcher.Stream.features)
except NodeProcessed:
pass
else:
@@ -154,16 +157,16 @@ class SASL(PlugIn):
"""
if 'features' in self._owner.__dict__:
self._owner.UnregisterHandler('features', self.FeaturesHandler,
- xmlns=NS_STREAMS)
+ xmlns=NS_STREAMS)
if 'challenge' in self._owner.__dict__:
self._owner.UnregisterHandler('challenge', self.SASLHandler,
- xmlns=NS_SASL)
+ xmlns=NS_SASL)
if 'failure' in self._owner.__dict__:
self._owner.UnregisterHandler('failure', self.SASLHandler,
- xmlns=NS_SASL)
+ xmlns=NS_SASL)
if 'success' in self._owner.__dict__:
self._owner.UnregisterHandler('success', self.SASLHandler,
- xmlns=NS_SASL)
+ xmlns=NS_SASL)
def auth(self):
"""
@@ -178,12 +181,12 @@ class SASL(PlugIn):
elif self._owner.Dispatcher.Stream.features:
try:
self.FeaturesHandler(self._owner.Dispatcher,
- self._owner.Dispatcher.Stream.features)
+ self._owner.Dispatcher.Stream.features)
except NodeProcessed:
pass
else:
self._owner.RegisterHandler('features',
- self.FeaturesHandler, xmlns=NS_STREAMS)
+ self.FeaturesHandler, xmlns=NS_STREAMS)
def FeaturesHandler(self, conn, feats):
"""
@@ -198,7 +201,8 @@ class SASL(PlugIn):
'mechanism'):
self.mecs.append(mec.getData())
- self._owner.RegisterHandler('challenge', self.SASLHandler, xmlns=NS_SASL)
+ self._owner.RegisterHandler('challenge', self.SASLHandler,
+ xmlns=NS_SASL)
self._owner.RegisterHandler('failure', self.SASLHandler, xmlns=NS_SASL)
self._owner.RegisterHandler('success', self.SASLHandler, xmlns=NS_SASL)
self.MechanismHandler()
@@ -206,7 +210,8 @@ class SASL(PlugIn):
def MechanismHandler(self):
if 'ANONYMOUS' in self.mecs and self.username is None:
self.mecs.remove('ANONYMOUS')
- node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'ANONYMOUS'})
+ node = Node('auth', attrs={'xmlns': NS_SASL,
+ 'mechanism': 'ANONYMOUS'})
self.mechanism = 'ANONYMOUS'
self.startsasl = SASL_IN_PROCESS
self._owner.send(str(node))
@@ -226,11 +231,11 @@ class SASL(PlugIn):
self.mecs.remove('GSSAPI')
try:
self.gss_vc = kerberos.authGSSClientInit('xmpp@' + \
- self._owner.xmpp_hostname)[1]
+ self._owner.xmpp_hostname)[1]
kerberos.authGSSClientStep(self.gss_vc, '')
response = kerberos.authGSSClientResponse(self.gss_vc)
- node=Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'GSSAPI'},
- payload=(response or ''))
+ node=Node('auth', attrs={'xmlns': NS_SASL,
+ 'mechanism': 'GSSAPI'}, payload=(response or ''))
self.mechanism = 'GSSAPI'
self.gss_step = GSS_STATE_STEP
self.startsasl = SASL_IN_PROCESS
@@ -247,7 +252,8 @@ class SASL(PlugIn):
raise NodeProcessed
if 'DIGEST-MD5' in self.mecs:
self.mecs.remove('DIGEST-MD5')
- node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'})
+ node = Node('auth', attrs={'xmlns': NS_SASL,
+ 'mechanism': 'DIGEST-MD5'})
self.mechanism = 'DIGEST-MD5'
self.startsasl = SASL_IN_PROCESS
self._owner.send(str(node))
@@ -258,6 +264,12 @@ class SASL(PlugIn):
self._owner._caller.get_password(self.set_password, self.mechanism)
self.startsasl = SASL_IN_PROCESS
raise NodeProcessed
+ if 'X-MESSENGER-OAUTH2' in self.mecs:
+ self.mecs.remove('X-MESSENGER-OAUTH2')
+ self.mechanism = 'X-MESSENGER-OAUTH2'
+ self._owner._caller.get_password(self.set_password, self.mechanism)
+ self.startsasl = SASL_IN_PROCESS
+ raise NodeProcessed
self.startsasl = SASL_FAILURE
log.info('I can only use EXTERNAL, SCRAM-SHA-1, DIGEST-MD5, GSSAPI and '
'PLAIN mecanisms.')
@@ -271,36 +283,54 @@ class SASL(PlugIn):
"""
if challenge.getNamespace() != NS_SASL:
return
+
+ def scram_base64(s):
+ return ''.join(s.encode('base64').split('\n'))
+
+ incoming_data = challenge.getData()
+ data=base64.decodestring(incoming_data)
### Handle Auth result
- if challenge.getName() == 'failure':
- self.startsasl = SASL_FAILURE
- try:
- reason = challenge.getChildren()[0]
- except Exception:
- reason = challenge
+ def on_auth_fail(reason):
log.info('Failed SASL authentification: %s' % reason)
+ self._owner.send(str(Node('abort', attrs={'xmlns': NS_SASL})))
if len(self.mecs) > 0:
- # There are other mechanisms to test
- self.MechanismHandler()
+ # There are other mechanisms to test, but wait for <failure>
+ # answer from server
+ self.startsasl = SASL_FAILURE_IN_PROGRESS
raise NodeProcessed
if self.on_sasl:
self.on_sasl()
raise NodeProcessed
+
+ if challenge.getName() == 'failure':
+ if self.startsasl == SASL_FAILURE_IN_PROGRESS:
+ self.MechanismHandler()
+ raise NodeProcessed
+ self.startsasl = SASL_FAILURE
+ try:
+ reason = challenge.getChildren()[0]
+ except Exception:
+ reason = challenge
+ on_auth_fail(reason)
elif challenge.getName() == 'success':
- # TODO: Need to validate any data-with-success.
- # TODO: Important for DIGEST-MD5 and SCRAM.
+ if self.mechanism == 'SCRAM-SHA-1':
+ # check data-with-success
+ data = scram_parse(data)
+ if data['v'] != scram_base64(self.scram_ServerSignature):
+ on_auth_fail('ServerSignature is wrong')
+
self.startsasl = SASL_SUCCESS
log.info('Successfully authenticated with remote server.')
handlers = self._owner.Dispatcher.dumpHandlers()
# Bosh specific dispatcher replugging
- # save old features. They will be used in case we won't get response on
- # stream restart after SASL auth (happens with XMPP over BOSH with
- # Openfire)
+ # save old features. They will be used in case we won't get response
+ # on stream restart after SASL auth (happens with XMPP over BOSH
+ # with Openfire)
old_features = self._owner.Dispatcher.Stream.features
self._owner.Dispatcher.PlugOut()
dispatcher_nb.Dispatcher.get_instance().PlugIn(self._owner,
- after_SASL=True, old_features=old_features)
+ after_SASL=True, old_features=old_features)
self._owner.Dispatcher.restoreHandlers(handlers)
self._owner.User = self.username
@@ -309,8 +339,6 @@ class SASL(PlugIn):
raise NodeProcessed
### Perform auth step
- incoming_data = challenge.getData()
- data=base64.decodestring(incoming_data)
log.info('Got challenge:' + data)
if self.mechanism == 'GSSAPI':
@@ -322,12 +350,12 @@ class SASL(PlugIn):
rc = kerberos.authGSSClientUnwrap(self.gss_vc, incoming_data)
response = kerberos.authGSSClientResponse(self.gss_vc)
rc = kerberos.authGSSClientWrap(self.gss_vc, response,
- kerberos.authGSSClientUserName(self.gss_vc))
+ kerberos.authGSSClientUserName(self.gss_vc))
response = kerberos.authGSSClientResponse(self.gss_vc)
if not response:
response = ''
self._owner.send(Node('response', attrs={'xmlns': NS_SASL},
- payload=response).__str__())
+ payload=response).__str__())
raise NodeProcessed
if self.mechanism == 'SCRAM-SHA-1':
hashfn = hashlib.sha1
@@ -353,12 +381,9 @@ class SASL(PlugIn):
ui = XOR(ui, ui_1)
return ui
- def H(s):
+ def scram_H(s):
return hashfn(s).digest()
- def scram_base64(s):
- return ''.join(s.encode('base64').split('\n'))
-
if self.scram_step == 0:
self.scram_step = 1
self.scram_soup += ',' + data + ','
@@ -373,7 +398,7 @@ class SASL(PlugIn):
SaltedPassword = Hi(self.password, salt, iter)
# TODO: Could cache this, along with salt+iter.
ClientKey = HMAC(SaltedPassword, 'Client Key')
- StoredKey = H(ClientKey)
+ StoredKey = scram_H(ClientKey)
ClientSignature = HMAC(StoredKey, self.scram_soup)
ClientProof = XOR(ClientKey, ClientSignature)
r += ',p=' + scram_base64(ClientProof)
@@ -408,8 +433,8 @@ class SASL(PlugIn):
else:
self.resp['realm'] = self._owner.Server
self.resp['nonce'] = chal['nonce']
- self.resp['cnonce'] = ''.join("%x" % randint(0, 2**28) for randint in
- itertools.repeat(random.randint, 7))
+ self.resp['cnonce'] = ''.join("%x" % randint(0, 2**28) for randint \
+ in itertools.repeat(random.randint, 7))
self.resp['nc'] = ('00000001')
self.resp['qop'] = 'auth'
self.resp['digest-uri'] = 'xmpp/' + self._owner.Server
@@ -417,6 +442,9 @@ class SASL(PlugIn):
# Password is now required
self._owner._caller.get_password(self.set_password, self.mechanism)
elif 'rspauth' in chal:
+ # Check rspauth value
+ if chal['rspauth'] != self.digest_rspauth:
+ on_auth_fail('rspauth is wrong')
self._owner.send(str(Node('response', attrs={'xmlns':NS_SASL})))
else:
self.startsasl = SASL_FAILURE
@@ -449,10 +477,14 @@ class SASL(PlugIn):
hash_realm = self._convert_to_iso88591(self.resp['realm'])
hash_password = self._convert_to_iso88591(self.password)
A1 = C([H(C([hash_username, hash_realm, hash_password])),
- self.resp['nonce'], self.resp['cnonce']])
+ self.resp['nonce'], self.resp['cnonce']])
A2 = C(['AUTHENTICATE', self.resp['digest-uri']])
- response= HH(C([HH(A1), self.resp['nonce'], self.resp['nc'],
- self.resp['cnonce'], self.resp['qop'], HH(A2)]))
+ response = HH(C([HH(A1), self.resp['nonce'], self.resp['nc'],
+ self.resp['cnonce'], self.resp['qop'], HH(A2)]))
+ A2 = C(['', self.resp['digest-uri']])
+ self.digest_rspauth = HH(C([HH(A1), self.resp['nonce'],
+ self.resp['nc'], self.resp['cnonce'], self.resp['qop'],
+ HH(A2)]))
self.resp['response'] = response
sasl_data = u''
for key in ('charset', 'username', 'realm', 'nonce', 'nc', 'cnonce',
@@ -462,14 +494,19 @@ class SASL(PlugIn):
else:
sasl_data += u'%s="%s",' % (key, self.resp[key])
sasl_data = sasl_data[:-1].encode('utf-8').encode('base64').replace(
- '\r', '').replace('\n', '')
- node = Node('response', attrs={'xmlns':NS_SASL}, payload=[sasl_data])
+ '\r', '').replace('\n', '')
+ node = Node('response', attrs={'xmlns': NS_SASL},
+ payload=[sasl_data])
elif self.mechanism == 'PLAIN':
sasl_data = u'\x00%s\x00%s' % (self.username, self.password)
sasl_data = sasl_data.encode('utf-8').encode('base64').replace(
- '\n', '')
+ '\n', '')
node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'},
- payload=[sasl_data])
+ payload=[sasl_data])
+ elif self.mechanism == 'X-MESSENGER-OAUTH2':
+ node = Node('auth', attrs={'xmlns': NS_SASL,
+ 'mechanism': 'X-MESSENGER-OAUTH2'})
+ node.addData(password)
self._owner.send(str(node))
@@ -501,8 +538,8 @@ class NonBlockingNonSASL(PlugIn):
self.owner = owner
owner.Dispatcher.SendAndWaitForResponse(
- Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]),
- func=self._on_username)
+ Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]),
+ func=self._on_username)
def _on_username(self, resp):
if not isResultNode(resp):
@@ -517,8 +554,8 @@ class NonBlockingNonSASL(PlugIn):
if query.getTag('digest'):
log.info("Performing digest authentication")
query.setTagData('digest',
- hashlib.sha1(self.owner.Dispatcher.Stream._document_attrs['id']
- + self.password).hexdigest())
+ hashlib.sha1(self.owner.Dispatcher.Stream._document_attrs['id']
+ + self.password).hexdigest())
if query.getTag('password'):
query.delChild('password')
self._method = 'digest'
@@ -533,23 +570,25 @@ class NonBlockingNonSASL(PlugIn):
def hash_n_times(s, count):
return count and hasher(hash_n_times(s, count-1)) or s
- hash_ = hash_n_times(hasher(hasher(self.password) + token), int(seq))
+ hash_ = hash_n_times(hasher(hasher(self.password) + token),
+ int(seq))
query.setTagData('hash', hash_)
self._method='0k'
else:
log.warn("Secure methods unsupported, performing plain text \
- authentication")
+ authentication")
query.setTagData('password', self.password)
self._method = 'plain'
- resp = self.owner.Dispatcher.SendAndWaitForResponse(iq, func=self._on_auth)
+ resp = self.owner.Dispatcher.SendAndWaitForResponse(iq,
+ func=self._on_auth)
def _on_auth(self, resp):
if isResultNode(resp):
log.info('Sucessfully authenticated with remote host.')
self.owner.User = self.user
self.owner.Resource = self.resource
- self.owner._registered_name = self.owner.User+'@'+self.owner.Server+\
- '/'+self.owner.Resource
+ self.owner._registered_name = self.owner.User + '@' + \
+ self.owner.Server+ '/' + self.owner.Resource
return self.on_auth(self._method)
log.info('Authentication failed!')
return self.on_auth(None)
@@ -564,24 +603,34 @@ class NonBlockingBind(PlugIn):
def __init__(self):
PlugIn.__init__(self)
self.bound = None
+ self.supports_sm = False
+ self.resuming = False
def plugin(self, owner):
''' Start resource binding, if allowed at this time. Used internally. '''
if self._owner.Dispatcher.Stream.features:
try:
self.FeaturesHandler(self._owner.Dispatcher,
- self._owner.Dispatcher.Stream.features)
+ self._owner.Dispatcher.Stream.features)
except NodeProcessed:
pass
else:
self._owner.RegisterHandler('features', self.FeaturesHandler,
- xmlns=NS_STREAMS)
+ xmlns=NS_STREAMS)
def FeaturesHandler(self, conn, feats):
"""
Determine if server supports resource binding and set some internal
- attributes accordingly
+ attributes accordingly.
+
+ It also checks if server supports stream management
"""
+
+ if feats.getTag('sm', namespace=NS_STREAM_MGMT):
+ self.supports_sm = True # server supports stream management
+ if self.resuming:
+ self._owner._caller.sm.resume_request()
+
if not feats.getTag('bind', namespace=NS_BIND):
log.info('Server does not requested binding.')
# we try to bind resource anyway
@@ -599,12 +648,14 @@ class NonBlockingBind(PlugIn):
Remove Bind handler from owner's dispatcher. Used internally
"""
self._owner.UnregisterHandler('features', self.FeaturesHandler,
- xmlns=NS_STREAMS)
+ xmlns=NS_STREAMS)
def NonBlockingBind(self, resource=None, on_bound=None):
"""
Perform binding. Use provided resource name or random (if not provided).
"""
+ if self.resuming: # We don't bind if we resume the stream
+ return
self.on_bound = on_bound
self._resource = resource
if self._resource:
@@ -614,8 +665,9 @@ class NonBlockingBind(PlugIn):
self._owner.onreceive(None)
self._owner.Dispatcher.SendAndWaitForResponse(
- Protocol('iq', typ='set', payload=[Node('bind', attrs={'xmlns':NS_BIND},
- payload=self._resource)]), func=self._on_bound)
+ Protocol('iq', typ='set', payload=[Node('bind',
+ attrs={'xmlns': NS_BIND}, payload=self._resource)]),
+ func=self._on_bound)
def _on_bound(self, resp):
if isResultNode(resp):
@@ -625,14 +677,22 @@ class NonBlockingBind(PlugIn):
jid = JID(resp.getTag('bind').getTagData('jid'))
self._owner.User = jid.getNode()
self._owner.Resource = jid.getResource()
+ # Only negociate stream management after bounded
+ sm = self._owner._caller.sm
+ if self.supports_sm:
+ # starts negociation
+ sm.set_owner(self._owner)
+ sm.negociate()
+ self._owner.Dispatcher.sm = sm
+
if hasattr(self, 'session') and self.session == -1:
# Server don't want us to initialize a session
log.info('No session required.')
self.on_bound('ok')
else:
self._owner.SendAndWaitForResponse(Protocol('iq', typ='set',
- payload=[Node('session', attrs={'xmlns':NS_SESSION})]),
- func=self._on_session)
+ payload=[Node('session', attrs={'xmlns':NS_SESSION})]),
+ func=self._on_session)
return
if resp:
log.info('Binding failed: %s.' % resp.getTag('error'))
diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py
index 353e79826..9bf5be7f3 100644
--- a/src/common/xmpp/bosh.py
+++ b/src/common/xmpp/bosh.py
@@ -413,15 +413,15 @@ class NonBlockingBOSH(NonBlockingTransport):
'xmlns:xmpp': 'urn:xmpp:xbosh'})
else:
t = BOSHBody(
- attrs={ 'content': self.bosh_content,
- 'hold': str(self.bosh_hold),
- 'route': '%s:%s' % (self.route_host, self.route_port),
- 'to': self.bosh_to,
- 'wait': str(self.bosh_wait),
- 'xml:lang': self.bosh_xml_lang,
- 'xmpp:version': '1.0',
- 'ver': '1.6',
- 'xmlns:xmpp': 'urn:xmpp:xbosh'})
+ attrs={ 'content': self.bosh_content,
+ 'hold': str(self.bosh_hold),
+ 'route': 'xmpp:%s:%s' % (self.route_host, self.route_port),
+ 'to': self.bosh_to,
+ 'wait': str(self.bosh_wait),
+ 'xml:lang': self.bosh_xml_lang,
+ 'xmpp:version': '1.0',
+ 'ver': '1.6',
+ 'xmlns:xmpp': 'urn:xmpp:xbosh'})
self.send_BOSH((t, True))
def start_disconnect(self):
diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py
index dc4c4476b..608be0fa6 100644
--- a/src/common/xmpp/client_nb.py
+++ b/src/common/xmpp/client_nb.py
@@ -493,6 +493,8 @@ class NonBlockingClient:
if self._sasl:
auth_nb.SASL.get_instance(self._User, self._Password,
self._on_start_sasl).PlugIn(self)
+ if not hasattr(self, 'SASL'):
+ return
if not self._sasl or self.SASL.startsasl == 'not-supported':
if not self._Resource:
self._Resource = 'xmpppy'
@@ -521,7 +523,16 @@ class NonBlockingClient:
self.connected = None # FIXME: is this intended? We use ''elsewhere
self._on_sasl_auth(None)
elif self.SASL.startsasl == 'success':
- auth_nb.NonBlockingBind.get_instance().PlugIn(self)
+ nb_bind = auth_nb.NonBlockingBind.get_instance()
+ sm = self._caller.sm
+ if sm._owner and sm.resumption:
+ nb_bind.resuming = True
+ sm.set_owner(self)
+ self.Dispatcher.sm = sm
+ nb_bind.PlugIn(self)
+ return
+
+ nb_bind.PlugIn(self)
self.onreceive(self._on_auth_bind)
return True
diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py
index cca56f33a..37020e5c6 100644
--- a/src/common/xmpp/dispatcher_nb.py
+++ b/src/common/xmpp/dispatcher_nb.py
@@ -21,6 +21,7 @@ different handlers to different XMPP stanzas and namespaces
"""
import simplexml, sys, locale
+import re
from xml.parsers.expat import ExpatError
from plugin import PlugIn
from protocol import (NS_STREAMS, NS_XMPP_STREAMS, NS_HTTP_BIND, Iq, Presence,
@@ -90,6 +91,27 @@ class XMPPDispatcher(PlugIn):
self.SendAndWaitForResponse, self.SendAndCallForResponse,
self.getAnID, self.Event, self.send]
+ # Let the dispatcher know if there is support for stream management
+ self.sm = None
+
+ # \ufddo -> \ufdef range
+ c = u'\ufdd0'
+ r = c.encode('utf8')
+ while (c < u'\ufdef'):
+ c = unichr(ord(c) + 1)
+ r += '|' + c.encode('utf8')
+
+ # \ufffe-\uffff, \u1fffe-\u1ffff, ..., \u10fffe-\u10ffff
+ c = u'\ufffe'
+ r += '|' + c.encode('utf8')
+ r += '|' + unichr(ord(c) + 1).encode('utf8')
+ while (c < u'\U0010fffe'):
+ c = unichr(ord(c) + 0x10000)
+ r += '|' + c.encode('utf8')
+ r += '|' + unichr(ord(c) + 1).encode('utf8')
+
+ self.invalid_chars_re = re.compile(r)
+
def getAnID(self):
global outgoingID
outgoingID += 1
@@ -175,6 +197,9 @@ class XMPPDispatcher(PlugIn):
raise ValueError('Incorrect stream start: (%s,%s). Terminating.'
% (tag, ns))
+ def replace_non_character(self, data):
+ return re.sub(self.invalid_chars_re, u'\ufffd'.encode('utf-8'), data)
+
def ProcessNonBlocking(self, data):
"""
Check incoming stream for data waiting
@@ -190,6 +215,7 @@ class XMPPDispatcher(PlugIn):
# disconnect method will never be called.
# Is this intended?
# also look at transports start_disconnect()
+ data = self.replace_non_character(data)
for handler in self._cycleHandlers:
handler(self)
if len(self._pendingExceptions) > 0:
@@ -417,6 +443,12 @@ class XMPPDispatcher(PlugIn):
stanza.props = stanza.getProperties()
ID = stanza.getID()
+ # If server supports stream management
+ if self.sm and self.sm.enabled and (stanza.getName() != 'r' and
+ stanza.getName() != 'a' and stanza.getName() != 'enabled' and
+ stanza.getName() != 'resumed'):
+ # increments the number of stanzas that has been handled
+ self.sm.in_h = self.sm.in_h + 1
list_ = ['default'] # we will use all handlers:
if typ in self.handlers[xmlns][name]:
list_.append(typ) # from very common...
@@ -525,6 +557,14 @@ class XMPPDispatcher(PlugIn):
ID = stanza.getID()
if self._owner._registered_name and not stanza.getAttr('from'):
stanza.setAttr('from', self._owner._registered_name)
+
+ # If no ID then it is a whitespace
+ if self.sm and self.sm.enabled and ID:
+ self.sm.uqueue.append(stanza)
+ self.sm.out_h = self.sm.out_h + 1
+ if len(self.sm.uqueue) > self.sm.max_queue:
+ self.sm.request_ack()
+
self._owner.Connection.send(stanza, now)
return ID
diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py
index f32863674..87bc44ec6 100644
--- a/src/common/xmpp/protocol.py
+++ b/src/common/xmpp/protocol.py
@@ -47,12 +47,14 @@ NS_BOB = 'urn:xmpp:bob' # XEP-0231
NS_BOOKMARKS = 'storage:bookmarks' # XEP-0048
NS_BROWSE = 'jabber:iq:browse'
NS_BROWSING = 'http://jabber.org/protocol/browsing' # XEP-0195
-NS_BYTESTREAM = 'http://jabber.org/protocol/bytestreams' # JEP-0065
-NS_CAPS = 'http://jabber.org/protocol/caps' # JEP-0115
+NS_BYTESTREAM = 'http://jabber.org/protocol/bytestreams' # XEP-0065
+NS_CAPS = 'http://jabber.org/protocol/caps' # XEP-0115
NS_CAPTCHA = 'urn:xmpp:captcha' # XEP-0158
-NS_CHATSTATES = 'http://jabber.org/protocol/chatstates' # JEP-0085
+NS_CARBONS = 'urn:xmpp:carbons:1' # XEP-0280
+NS_CHATSTATES = 'http://jabber.org/protocol/chatstates' # XEP-0085
NS_CHATTING = 'http://jabber.org/protocol/chatting' # XEP-0194
NS_CLIENT = 'jabber:client'
+NS_CONDITIONS = 'urn:xmpp:muc:conditions:0' # XEP-0306
NS_COMMANDS = 'http://jabber.org/protocol/commands'
NS_COMPONENT_ACCEPT = 'jabber:component:accept'
NS_COMPONENT_1 = 'http://jabberd.jabberstudio.org/ns/component/1.0'
@@ -69,9 +71,10 @@ NS_DISCO_ITEMS = NS_DISCO + '#items'
NS_ENCRYPTED = 'jabber:x:encrypted' # XEP-0027
NS_ESESSION = 'http://www.xmpp.org/extensions/xep-0116.html#ns'
NS_ESESSION_INIT = 'http://www.xmpp.org/extensions/xep-0116.html#ns-init' # XEP-0116
-NS_EVENT = 'jabber:x:event' # XEP-0022
+NS_EVENT = 'jabber:x:event' # XEP-0022
NS_FEATURE = 'http://jabber.org/protocol/feature-neg'
-NS_FILE = 'http://jabber.org/protocol/si/profile/file-transfer' # JEP-0096
+NS_FILE = 'http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096
+NS_FORWARD = 'urn:xmpp:forward:0' # XEP-0297
NS_GAMING = 'http://jabber.org/protocol/gaming' # XEP-0196
NS_GATEWAY = 'jabber:iq:gateway' # XEP-0100
NS_GEOLOC = 'http://jabber.org/protocol/geoloc' # XEP-0080
@@ -105,7 +108,7 @@ NS_MUC_CONFIG = NS_MUC + '#roomconfig'
NS_NICK = 'http://jabber.org/protocol/nick' # XEP-0172
NS_OFFLINE = 'http://www.jabber.org/jeps/jep-0030.html' # XEP-0013
NS_PHYSLOC = 'http://jabber.org/protocol/physloc' # XEP-0112
-NS_PING = 'urn:xmpp:ping' # SEP-0199
+NS_PING = 'urn:xmpp:ping' # XEP-0199
NS_PRESENCE = 'presence' # Jabberd2
NS_PRIVACY = 'jabber:iq:privacy'
NS_PRIVATE = 'jabber:iq:private'
@@ -113,7 +116,7 @@ NS_PROFILE = 'http://jabber.org/protocol/profile' # XEP-0154
NS_PUBSUB = 'http://jabber.org/protocol/pubsub' # XEP-0060
NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event'
NS_PUBSUB_PUBLISH_OPTIONS = NS_PUBSUB + '#publish-options' # XEP-0060
-NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' # JEP-0060
+NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' # XEP-0060
NS_REGISTER = 'jabber:iq:register'
NS_ROSTER = 'jabber:iq:roster'
NS_ROSTERNOTES = 'storage:rosternotes'
@@ -123,7 +126,7 @@ NS_RPC = 'jabber:iq:rpc' # XEP-0009
NS_RSM = 'http://jabber.org/protocol/rsm'
NS_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
NS_SECLABEL = 'urn:xmpp:sec-label:0'
-NS_SECLABEL_CATALOG = 'urn:xmpp:sec-label:catalog:0'
+NS_SECLABEL_CATALOG = 'urn:xmpp:sec-label:catalog:2'
NS_SEARCH = 'jabber:iq:search'
NS_SERVER = 'jabber:server'
NS_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'
@@ -153,9 +156,10 @@ NS_DATA_LAYOUT = 'http://jabber.org/protocol/xdata-layout' # XEP-0141
NS_DATA_VALIDATE = 'http://jabber.org/protocol/xdata-validate' # XEP-0122
NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams'
NS_RECEIPTS = 'urn:xmpp:receipts'
-NS_PUBKEY_PUBKEY='urn:xmpp:pubkey:2' # XEP-0189
-NS_PUBKEY_REVOKE='urn:xmpp:revoke:2'
-NS_PUBKEY_ATTEST='urn:xmpp:attest:2'
+NS_PUBKEY_PUBKEY = 'urn:xmpp:pubkey:2' # XEP-0189
+NS_PUBKEY_REVOKE = 'urn:xmpp:revoke:2'
+NS_PUBKEY_ATTEST = 'urn:xmpp:attest:2'
+NS_STREAM_MGMT = 'urn:xmpp:sm:2' # XEP-198
xmpp_stream_error_conditions = '''
bad-format -- -- -- The entity has sent XML that cannot be processed.
@@ -631,6 +635,17 @@ class Protocol(Node):
"""
return self.getTagAttr('error', 'code')
+ def getStatusConditions(self):
+ """
+ Return the status conditions list as defined in XEP-0306.
+ """
+ conds = []
+ condtag = self.getTag('conditions', namespace=NS_CONDITIONS)
+ if condtag:
+ for tag in condtag.getChildren():
+ conds.append(tag.getName())
+ return conds
+
def setError(self, error, code=None):
"""
Set the error code. Obsolete. Use error-conditions instead
@@ -773,10 +788,11 @@ class Message(Protocol):
def buildReply(self, text=None):
"""
Builds and returns another message object with specified text. The to,
- from and thread properties of new message are pre-set as reply to this
- message
+ from, thread and type properties of new message are pre-set as reply to
+ this message
"""
- m = Message(to=self.getFrom(), frm=self.getTo(), body=text)
+ m = Message(to=self.getFrom(), frm=self.getTo(), body=text,
+ typ=self.getType())
th = self.getThread()
if th:
m.setThread(th)
@@ -933,11 +949,20 @@ class Iq(Protocol):
if queryNS:
self.setQueryNS(queryNS)
+ def getQuery(self):
+ """
+ Return the IQ's child element if it exists, None otherwise.
+ """
+ children = self.getChildren()
+ if children and self.getType() != 'error' and \
+ children[0].getName() != 'error':
+ return children[0]
+
def getQueryNS(self):
"""
Return the namespace of the 'query' child element
"""
- tag = self.getTag('query')
+ tag = self.getQuery()
if tag:
return tag.getNamespace()
@@ -945,13 +970,15 @@ class Iq(Protocol):
"""
Return the 'node' attribute value of the 'query' child element
"""
- return self.getTagAttr('query', 'node')
+ tag = self.getQuery()
+ if tag:
+ return tag.getAttr('node')
def getQueryPayload(self):
"""
Return the 'query' child element payload
"""
- tag = self.getTag('query')
+ tag = self.getQuery()
if tag:
return tag.getPayload()
@@ -959,38 +986,79 @@ class Iq(Protocol):
"""
Return the 'query' child element child nodes
"""
- tag = self.getTag('query')
+ tag = self.getQuery()
if tag:
return tag.getChildren()
+ def setQuery(self, name=None):
+ """
+ Change the name of the query node, creating it if needed. Keep the
+ existing name if none is given (use 'query' if it's a creation).
+ Return the query node.
+ """
+ query = self.getQuery()
+ if query is None:
+ query = self.addChild('query')
+ if name is not None:
+ query.setName(name)
+ return query
+
def setQueryNS(self, namespace):
"""
Set the namespace of the 'query' child element
"""
- self.setTag('query').setNamespace(namespace)
+ self.setQuery().setNamespace(namespace)
def setQueryPayload(self, payload):
"""
Set the 'query' child element payload
"""
- self.setTag('query').setPayload(payload)
+ self.setQuery().setPayload(payload)
def setQuerynode(self, node):
"""
Set the 'node' attribute value of the 'query' child element
"""
- self.setTagAttr('query', 'node', node)
+ self.setQuery().setAttr('node', node)
def buildReply(self, typ):
"""
Build and return another Iq object of specified type. The to, from and
query child node of new Iq are pre-set as reply to this Iq.
"""
- iq = Iq(typ, to=self.getFrom(), frm=self.getTo(), attrs={'id': self.getID()})
- if self.getTag('query'):
- iq.setQueryNS(self.getQueryNS())
+ iq = Iq(typ, to=self.getFrom(), frm=self.getTo(),
+ attrs={'id': self.getID()})
+ iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
return iq
+class Acks(Node):
+ """
+ Acknowledgement elements for Stream Management
+ """
+ def __init__(self, nsp=NS_STREAM_MGMT):
+ Node.__init__(self, None, {}, [], None, None,False, None)
+ self.setNamespace(nsp)
+
+ def buildAnswer(self, handled):
+ """
+ handled is the number of stanzas handled
+ """
+ self.setName('a')
+ self.setAttr('h', handled)
+
+ def buildRequest(self):
+ self.setName('r')
+
+ def buildEnable(self, resume=False):
+ self.setName('enable')
+ if resume:
+ self.setAttr('resume', 'true')
+
+ def buildResume(self, handled, previd):
+ self.setName('resume')
+ self.setAttr('h', handled)
+ self.setAttr('previd', previd)
+
class ErrorNode(Node):
"""
XMPP-style error element
diff --git a/src/common/xmpp/roster_nb.py b/src/common/xmpp/roster_nb.py
index c5a7bb463..9f520e3d3 100644
--- a/src/common/xmpp/roster_nb.py
+++ b/src/common/xmpp/roster_nb.py
@@ -45,7 +45,7 @@ class NonBlockingRoster(PlugIn):
PlugIn.__init__(self)
self.version = version
self._data = {}
- self.set=None
+ self._set=None
self._exported_methods=[self.getRoster]
self.received_from_server = False
@@ -54,8 +54,8 @@ class NonBlockingRoster(PlugIn):
Request roster from server if it were not yet requested (or if the
'force' argument is set)
"""
- if self.set is None:
- self.set = 0
+ if self._set is None:
+ self._set = 0
elif not force:
return
@@ -100,7 +100,7 @@ class NonBlockingRoster(PlugIn):
if group.getData() not in self._data[jid]['groups']:
self._data[jid]['groups'].append(group.getData())
self._data[self._owner.User+'@'+self._owner.Server]={'resources': {}, 'name': None, 'ask': None, 'subscription': None, 'groups': None,}
- self.set=1
+ self._set=1
# Looks like we have a workaround
# raise NodeProcessed # a MUST. Otherwise you'll get back an <iq type='error'/>
@@ -323,7 +323,7 @@ class NonBlockingRoster(PlugIn):
'subscription': None,
'groups': None
}
- self.set = 1
+ self._set = 1
def plugin(self, owner, request=1):
"""
@@ -340,9 +340,9 @@ class NonBlockingRoster(PlugIn):
def _on_roster_set(self, data):
if data:
self._owner.Dispatcher.ProcessNonBlocking(data)
- if not self.set:
+ if not self._set:
return
- if not self._owner:
+ if not hasattr(self, '_owner') or not self._owner:
# Connection has been closed by receiving a <stream:error> for ex,
return
self._owner.onreceive(None)
@@ -356,7 +356,7 @@ class NonBlockingRoster(PlugIn):
Request roster from server if neccessary and returns self
"""
return_self = True
- if not self.set:
+ if not self._set:
self.on_ready = on_ready
self._owner.onreceive(self._on_roster_set)
return_self = False
diff --git a/src/common/xmpp/smacks.py b/src/common/xmpp/smacks.py
new file mode 100644
index 000000000..e59b5d1b8
--- /dev/null
+++ b/src/common/xmpp/smacks.py
@@ -0,0 +1,130 @@
+from protocol import Acks
+from protocol import NS_STREAM_MGMT
+import logging
+log = logging.getLogger('gajim.c.x.smacks')
+
+class Smacks():
+ '''
+ This is Smacks is the Stream Management class. It takes care of requesting
+ and sending acks. Also, it keeps track of the unhandled outgoing stanzas.
+
+ The dispatcher has to be able to access this class to increment the
+ number of handled stanzas
+ '''
+
+ def __init__(self, con):
+ self.con = con # Connection object
+ self.out_h = 0 # Outgoing stanzas handled
+ self.in_h = 0 # Incoming stanzas handled
+ self.uqueue = [] # Unhandled stanzas queue
+ self.session_id = None
+ self.resumption = False # If server supports resume
+ # Max number of stanzas in queue before making a request
+ self.max_queue = 5
+ self._owner = None
+ self.resuming = False
+ self.enabled = False # If SM is enabled
+ self.location = None
+
+ def set_owner(self, owner):
+ self._owner = owner
+
+ # Register handlers
+ owner.Dispatcher.RegisterNamespace(NS_STREAM_MGMT)
+ owner.Dispatcher.RegisterHandler('enabled', self._neg_response,
+ xmlns=NS_STREAM_MGMT)
+ owner.Dispatcher.RegisterHandler('r', self.send_ack,
+ xmlns=NS_STREAM_MGMT)
+ owner.Dispatcher.RegisterHandler('a', self.check_ack,
+ xmlns=NS_STREAM_MGMT)
+ owner.Dispatcher.RegisterHandler('resumed', self.check_ack,
+ xmlns=NS_STREAM_MGMT)
+ owner.Dispatcher.RegisterHandler('failed', self.error_handling,
+ xmlns=NS_STREAM_MGMT)
+
+ def _neg_response(self, disp, stanza):
+ r = stanza.getAttr('resume')
+ if r == 'true' or r == 'True' or r == '1':
+ self.resumption = True
+ self.session_id = stanza.getAttr('id')
+
+ if r == 'false' or r == 'False' or r == '0':
+ self.negociate(False)
+
+ l = stanza.getAttr('location')
+ if l:
+ self.location = l
+
+ def negociate(self, resume=True):
+ # Every time we attempt to negociate, we must erase all previous info
+ # about any previous session
+ self.uqueue = []
+ self.in_h = 0
+ self.out_h = 0
+ self.session_id = None
+ self.enabled = True
+
+ stanza = Acks()
+ stanza.buildEnable(resume)
+ self._owner.Connection.send(stanza, now=True)
+
+ def resume_request(self):
+ if not self.session_id:
+ self.resuming = False
+ log.error('Attempted to resume without a valid session id ')
+ return
+ resume = Acks()
+ resume.buildResume(self.in_h, self.session_id)
+ self._owner.Connection.send(resume, False)
+
+ def send_ack(self, disp, stanza):
+ ack = Acks()
+ ack.buildAnswer(self.in_h)
+ self._owner.Connection.send(ack, False)
+
+ def request_ack(self):
+ r = Acks()
+ r.buildRequest()
+ self._owner.Connection.send(r, False)
+
+ def check_ack(self, disp, stanza):
+ '''
+ Checks if the number of stanzas sent are the same as the
+ number of stanzas received by the server. Pops stanzas that were
+ handled by the server from the queue.
+ '''
+ h = int(stanza.getAttr('h'))
+ diff = self.out_h - h
+
+ if len(self.uqueue) < diff or diff < 0:
+ log.error('Server and client number of stanzas handled mismatch ')
+ else:
+ while (len(self.uqueue) > diff):
+ self.uqueue.pop(0)
+
+ if stanza.getName() == 'resumed':
+ self.resuming = True
+ self.con.set_oldst()
+ if self.uqueue != []:
+ for i in self.uqueue:
+ self._owner.Connection.send(i, False)
+
+ def error_handling(self, disp, stanza):
+ # If the server doesn't recognize previd, forget about resuming
+ # Ask for service discovery, etc..
+ if stanza.getTag('item-not-found'):
+ self.resuming = False
+ # we need to bind a resource
+ self._owner.NonBlockingBind.resuming = False
+ self._owner._on_auth_bind(None)
+ return
+
+ # Doesn't support resumption
+ if stanza.getTag('feature-not-implemented'):
+ self.negociate(False)
+ return
+
+ if stanza.getTag('unexpected-request'):
+ self.enabled = False
+ log.error('Gajim failed to negociate Stream Management')
+ return
diff --git a/src/common/xmpp/stringprepare.py b/src/common/xmpp/stringprepare.py
index bae16fcec..657895c84 100644
--- a/src/common/xmpp/stringprepare.py
+++ b/src/common/xmpp/stringprepare.py
@@ -2,7 +2,7 @@
## src/common/xmpp/stringprepare.py
##
## Copyright (C) 2001-2005 Twisted Matrix Laboratories
-## Copyright (C) 2005-2010 Yann Leboulanger <asterix AT lagaule.org>
+## Copyright (C) 2005-2011 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
## Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
##
@@ -202,6 +202,8 @@ class NamePrep:
def nameprep(self, label):
label = idna.nameprep(label)
self.check_prohibiteds(label)
+ if len(label) == 0:
+ raise UnicodeError, "Invalid empty name"
if label[0] == '-':
raise UnicodeError, "Invalid leading hyphen-minus"
if label[-1] == '-':
diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py
index 7a9c80f98..cc8393d52 100644
--- a/src/common/xmpp/tls_nb.py
+++ b/src/common/xmpp/tls_nb.py
@@ -359,7 +359,8 @@ class NonBlockingTLS(PlugIn):
tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
log.debug('Using client cert and key from %s' % conn.client_cert)
try:
- p12 = OpenSSL.crypto.load_pkcs12(open(conn.client_cert).read())
+ p12 = OpenSSL.crypto.load_pkcs12(open(conn.client_cert).read(),
+ conn.client_cert_passphrase)
except OpenSSL.crypto.Error, exception_obj:
log.warning('Unable to load client pkcs12 certificate from '
'file %s: %s ... Is it a valid PKCS12 cert?' % \