2006-02-03 12:17:34 +00:00
|
|
|
## auth_nb.py
|
|
|
|
## based on auth.py
|
|
|
|
##
|
|
|
|
## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
|
|
|
|
## modified by Dimitur Kirov <dkirov@gmail.com>
|
|
|
|
##
|
|
|
|
## 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, 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.
|
|
|
|
'''
|
|
|
|
Provides library with all Non-SASL and SASL authentication mechanisms.
|
|
|
|
Can be used both for client and transport authentication.
|
|
|
|
'''
|
2006-02-14 18:13:20 +00:00
|
|
|
import sys
|
2006-02-03 12:17:34 +00:00
|
|
|
from protocol import *
|
|
|
|
from auth import *
|
|
|
|
from client import PlugIn
|
|
|
|
import sha,base64,random,dispatcher_nb
|
|
|
|
|
2006-04-30 17:33:10 +00:00
|
|
|
def challenge_splitter(data):
|
|
|
|
''' Helper function that creates a dict from challenge string.
|
|
|
|
Sample chalenge string:
|
|
|
|
username="example.org",realm="somerealm",\
|
|
|
|
nonce="OA6MG9tEQGm2hh",cnonce="OA6MHXh6VqTrRk",\
|
|
|
|
nc=00000001,qop="auth,auth-int,auth-conf",charset=utf-8
|
|
|
|
in the above example:
|
|
|
|
dict['qop'] = ('auth','auth-int','auth-conf')
|
|
|
|
dict['realm'] = 'somerealm'
|
|
|
|
'''
|
|
|
|
X_KEYWORD, X_VALUE, X_END = 0, 1, 2
|
|
|
|
quotes_open = False
|
|
|
|
keyword, value = '', ''
|
|
|
|
dict, arr = {}, None
|
|
|
|
|
|
|
|
expecting = X_KEYWORD
|
|
|
|
for iter in range(len(data) + 1):
|
|
|
|
end = False
|
|
|
|
if iter == len(data):
|
|
|
|
expecting = X_END
|
|
|
|
end = True
|
|
|
|
else:
|
|
|
|
char = data[iter]
|
|
|
|
if expecting == X_KEYWORD:
|
|
|
|
if char == '=':
|
|
|
|
expecting = X_VALUE
|
2007-02-06 08:57:59 +00:00
|
|
|
elif char in (',', ' ', '\t'):
|
2006-04-30 17:33:10 +00:00
|
|
|
pass
|
|
|
|
else:
|
|
|
|
keyword = '%s%c' % (keyword, char)
|
|
|
|
elif expecting == X_VALUE:
|
|
|
|
if char == '"':
|
|
|
|
if quotes_open:
|
|
|
|
end = True
|
|
|
|
else:
|
|
|
|
quotes_open = True
|
2007-02-06 08:57:59 +00:00
|
|
|
elif char in (',', ' ', '\t'):
|
2006-04-30 17:33:10 +00:00
|
|
|
if quotes_open:
|
|
|
|
if not arr:
|
|
|
|
arr = [value]
|
|
|
|
else:
|
|
|
|
arr.append(value)
|
|
|
|
value = ""
|
|
|
|
else:
|
|
|
|
end = True
|
|
|
|
else:
|
|
|
|
value = '%s%c' % (value, char)
|
|
|
|
if end:
|
|
|
|
if arr:
|
|
|
|
arr.append(value)
|
|
|
|
dict[keyword] = arr
|
|
|
|
arr = None
|
|
|
|
else:
|
|
|
|
dict[keyword] = value
|
|
|
|
value, keyword = '', ''
|
|
|
|
expecting = X_KEYWORD
|
|
|
|
quotes_open = False
|
|
|
|
return dict
|
|
|
|
|
2006-02-03 12:17:34 +00:00
|
|
|
class SASL(PlugIn):
|
|
|
|
''' Implements SASL authentication. '''
|
|
|
|
def __init__(self,username,password, on_sasl):
|
|
|
|
PlugIn.__init__(self)
|
|
|
|
self.username=username
|
|
|
|
self.password=password
|
|
|
|
self.on_sasl = on_sasl
|
2006-04-08 15:58:50 +00:00
|
|
|
self.realm = None
|
2006-02-03 12:17:34 +00:00
|
|
|
def plugin(self,owner):
|
|
|
|
if not self._owner.Dispatcher.Stream._document_attrs.has_key('version'):
|
|
|
|
self.startsasl='not-supported'
|
|
|
|
elif self._owner.Dispatcher.Stream.features:
|
|
|
|
try:
|
|
|
|
self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
|
|
|
|
except NodeProcessed:
|
|
|
|
pass
|
|
|
|
else: self.startsasl=None
|
|
|
|
|
|
|
|
def auth(self):
|
|
|
|
''' Start authentication. Result can be obtained via "SASL.startsasl" attribute and will be
|
|
|
|
either "success" or "failure". Note that successfull auth will take at least
|
|
|
|
two Dispatcher.Process() calls. '''
|
|
|
|
if self.startsasl:
|
|
|
|
pass
|
|
|
|
elif self._owner.Dispatcher.Stream.features:
|
|
|
|
try:
|
|
|
|
self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
|
|
|
|
except NodeProcessed:
|
|
|
|
pass
|
|
|
|
else: self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS)
|
|
|
|
|
|
|
|
def plugout(self):
|
|
|
|
''' Remove SASL handlers from owner's dispatcher. Used internally. '''
|
|
|
|
self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS)
|
|
|
|
self._owner.UnregisterHandler('challenge', self.SASLHandler, xmlns=NS_SASL)
|
|
|
|
self._owner.UnregisterHandler('failure', self.SASLHandler, xmlns=NS_SASL)
|
|
|
|
self._owner.UnregisterHandler('success', self.SASLHandler, xmlns=NS_SASL)
|
|
|
|
|
|
|
|
def FeaturesHandler(self, conn, feats):
|
|
|
|
''' Used to determine if server supports SASL auth. Used internally. '''
|
|
|
|
if not feats.getTag('mechanisms', namespace=NS_SASL):
|
|
|
|
self.startsasl='not-supported'
|
2006-02-19 20:50:59 +00:00
|
|
|
self.DEBUG('SASL not supported by server', 'error')
|
2006-02-03 12:17:34 +00:00
|
|
|
return
|
|
|
|
mecs=[]
|
|
|
|
for mec in feats.getTag('mechanisms', namespace=NS_SASL).getTags('mechanism'):
|
|
|
|
mecs.append(mec.getData())
|
|
|
|
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)
|
|
|
|
if "DIGEST-MD5" in mecs:
|
2006-02-19 20:50:59 +00:00
|
|
|
node=Node('auth',attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'})
|
2006-02-03 12:17:34 +00:00
|
|
|
elif "PLAIN" in mecs:
|
|
|
|
sasl_data='%s\x00%s\x00%s' % (self.username+'@' + self._owner.Server,
|
|
|
|
self.username, self.password)
|
|
|
|
node=Node('auth', attrs={'xmlns':NS_SASL,'mechanism':'PLAIN'},
|
2006-03-05 19:43:41 +00:00
|
|
|
payload=[base64.encodestring(sasl_data).replace('\n','')])
|
2006-02-03 12:17:34 +00:00
|
|
|
else:
|
|
|
|
self.startsasl='failure'
|
|
|
|
self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.', 'error')
|
|
|
|
return
|
|
|
|
self.startsasl='in-process'
|
|
|
|
self._owner.send(node.__str__())
|
|
|
|
raise NodeProcessed
|
|
|
|
|
|
|
|
def SASLHandler(self, conn, challenge):
|
|
|
|
''' Perform next SASL auth step. Used internally. '''
|
|
|
|
if challenge.getNamespace() <> NS_SASL:
|
|
|
|
return
|
|
|
|
if challenge.getName() == 'failure':
|
|
|
|
self.startsasl = 'failure'
|
|
|
|
try:
|
|
|
|
reason = challenge.getChildren()[0]
|
|
|
|
except:
|
|
|
|
reason = challenge
|
|
|
|
self.DEBUG('Failed SASL authentification: %s' % reason, 'error')
|
|
|
|
if self.on_sasl :
|
|
|
|
self.on_sasl ()
|
|
|
|
raise NodeProcessed
|
|
|
|
elif challenge.getName() == 'success':
|
|
|
|
self.startsasl='success'
|
|
|
|
self.DEBUG('Successfully authenticated with remote server.', 'ok')
|
|
|
|
handlers=self._owner.Dispatcher.dumpHandlers()
|
|
|
|
self._owner.Dispatcher.PlugOut()
|
|
|
|
dispatcher_nb.Dispatcher().PlugIn(self._owner)
|
|
|
|
self._owner.Dispatcher.restoreHandlers(handlers)
|
|
|
|
self._owner.User = self.username
|
|
|
|
if self.on_sasl :
|
|
|
|
self.on_sasl ()
|
|
|
|
raise NodeProcessed
|
|
|
|
########################################3333
|
|
|
|
incoming_data = challenge.getData()
|
|
|
|
data=base64.decodestring(incoming_data)
|
|
|
|
self.DEBUG('Got challenge:'+data,'ok')
|
2006-04-30 17:33:10 +00:00
|
|
|
chal = challenge_splitter(data)
|
2006-04-08 15:58:50 +00:00
|
|
|
if not self.realm and chal.has_key('realm'):
|
|
|
|
self.realm = chal['realm']
|
2006-04-30 17:33:10 +00:00
|
|
|
if chal.has_key('qop') and ((type(chal['qop']) == str and \
|
|
|
|
chal['qop'] =='auth') or (type(chal['qop']) == list and 'auth' in \
|
|
|
|
chal['qop'])):
|
2006-02-03 12:17:34 +00:00
|
|
|
resp={}
|
2006-04-08 15:58:50 +00:00
|
|
|
resp['username'] = self.username
|
|
|
|
if self.realm:
|
|
|
|
resp['realm'] = self.realm
|
|
|
|
else:
|
|
|
|
resp['realm'] = self._owner.Server
|
2006-02-03 12:17:34 +00:00
|
|
|
resp['nonce']=chal['nonce']
|
|
|
|
cnonce=''
|
|
|
|
for i in range(7):
|
2006-04-08 15:58:50 +00:00
|
|
|
cnonce += hex(int(random.random() * 65536 * 4096))[2:]
|
|
|
|
resp['cnonce'] = cnonce
|
|
|
|
resp['nc'] = ('00000001')
|
|
|
|
resp['qop'] = 'auth'
|
|
|
|
resp['digest-uri'] = 'xmpp/'+self._owner.Server
|
|
|
|
A1=C([H(C([resp['username'], resp['realm'], self.password])),
|
|
|
|
resp['nonce'], resp['cnonce']])
|
2006-02-03 12:17:34 +00:00
|
|
|
A2=C(['AUTHENTICATE',resp['digest-uri']])
|
2006-04-08 15:58:50 +00:00
|
|
|
response= HH(C([HH(A1), resp['nonce'], resp['nc'], resp['cnonce'],
|
|
|
|
resp['qop'], HH(A2)]))
|
|
|
|
resp['response'] = response
|
|
|
|
resp['charset'] = 'utf-8'
|
2006-02-03 12:17:34 +00:00
|
|
|
sasl_data=''
|
|
|
|
for key in ['charset', 'username', 'realm', 'nonce', 'nc', 'cnonce', 'digest-uri', 'response', 'qop']:
|
|
|
|
if key in ['nc','qop','response','charset']:
|
|
|
|
sasl_data += "%s=%s," % (key,resp[key])
|
|
|
|
else:
|
|
|
|
sasl_data += '%s="%s",' % (key,resp[key])
|
|
|
|
########################################3333
|
|
|
|
node=Node('response', attrs={'xmlns':NS_SASL},
|
|
|
|
payload=[base64.encodestring(sasl_data[:-1]).replace('\r','').replace('\n','')])
|
|
|
|
self._owner.send(node.__str__())
|
|
|
|
elif chal.has_key('rspauth'):
|
|
|
|
self._owner.send(Node('response', attrs={'xmlns':NS_SASL}).__str__())
|
|
|
|
else:
|
|
|
|
self.startsasl='failure'
|
|
|
|
self.DEBUG('Failed SASL authentification: unknown challenge', 'error')
|
|
|
|
if self.on_sasl :
|
|
|
|
self.on_sasl ()
|
|
|
|
raise NodeProcessed
|
|
|
|
|
|
|
|
class NonBlockingNonSASL(PlugIn):
|
|
|
|
''' Implements old Non-SASL (JEP-0078) authentication used
|
|
|
|
in jabberd1.4 and transport authentication.
|
|
|
|
'''
|
|
|
|
def __init__(self, user, password, resource, on_auth):
|
|
|
|
''' Caches username, password and resource for auth. '''
|
|
|
|
PlugIn.__init__(self)
|
|
|
|
self.DBG_LINE ='gen_auth'
|
|
|
|
self.user = user
|
|
|
|
self.password= password
|
|
|
|
self.resource = resource
|
|
|
|
self.on_auth = on_auth
|
|
|
|
|
|
|
|
def plugin(self, owner):
|
|
|
|
''' Determine the best auth method (digest/0k/plain) and use it for auth.
|
|
|
|
Returns used method name on success. Used internally. '''
|
|
|
|
if not self.resource:
|
|
|
|
return self.authComponent(owner)
|
|
|
|
self.DEBUG('Querying server about possible auth methods', 'start')
|
|
|
|
self.owner = owner
|
|
|
|
|
|
|
|
resp = owner.Dispatcher.SendAndWaitForResponse(
|
|
|
|
Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]), func=self._on_username
|
|
|
|
)
|
|
|
|
|
|
|
|
def _on_username(self, resp):
|
|
|
|
if not isResultNode(resp):
|
|
|
|
self.DEBUG('No result node arrived! Aborting...','error')
|
|
|
|
return self.on_auth(None)
|
|
|
|
iq=Iq(typ='set',node=resp)
|
|
|
|
query=iq.getTag('query')
|
|
|
|
query.setTagData('username',self.user)
|
|
|
|
query.setTagData('resource',self.resource)
|
|
|
|
|
|
|
|
if query.getTag('digest'):
|
|
|
|
self.DEBUG("Performing digest authentication",'ok')
|
|
|
|
query.setTagData('digest',
|
|
|
|
sha.new(self.owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest())
|
|
|
|
if query.getTag('password'):
|
|
|
|
query.delChild('password')
|
|
|
|
self._method='digest'
|
|
|
|
elif query.getTag('token'):
|
|
|
|
token=query.getTagData('token')
|
|
|
|
seq=query.getTagData('sequence')
|
|
|
|
self.DEBUG("Performing zero-k authentication",'ok')
|
|
|
|
hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest()
|
|
|
|
for foo in xrange(int(seq)):
|
|
|
|
hash = sha.new(hash).hexdigest()
|
|
|
|
query.setTagData('hash',hash)
|
|
|
|
self._method='0k'
|
|
|
|
else:
|
|
|
|
self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn')
|
|
|
|
query.setTagData('password',self.password)
|
|
|
|
self._method='plain'
|
|
|
|
resp=self.owner.Dispatcher.SendAndWaitForResponse(iq, func=self._on_auth)
|
|
|
|
|
|
|
|
def _on_auth(self, resp):
|
|
|
|
if isResultNode(resp):
|
|
|
|
self.DEBUG('Sucessfully authenticated with remove host.','ok')
|
|
|
|
self.owner.User=self.user
|
|
|
|
self.owner.Resource=self.resource
|
|
|
|
self.owner._registered_name=self.owner.User+'@'+self.owner.Server+'/'+self.owner.Resource
|
|
|
|
return self.on_auth(self._method)
|
|
|
|
self.DEBUG('Authentication failed!','error')
|
|
|
|
return self.on_auth(None)
|
|
|
|
|
|
|
|
def authComponent(self,owner):
|
|
|
|
''' Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. '''
|
|
|
|
self.handshake=0
|
|
|
|
owner.send(Node(NS_COMPONENT_ACCEPT+' handshake',
|
|
|
|
payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()]))
|
|
|
|
owner.RegisterHandler('handshake', self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT)
|
|
|
|
self._owner.onreceive(self._on_auth_component)
|
|
|
|
|
|
|
|
def _on_auth_component(self, data):
|
|
|
|
''' called when we receive some response, after we send the handshake '''
|
|
|
|
if data:
|
|
|
|
self.Dispatcher.ProcessNonBlocking(data)
|
|
|
|
if not self.handshake:
|
|
|
|
self.DEBUG('waiting on handshake', 'notify')
|
|
|
|
return
|
|
|
|
self._owner.onreceive(None)
|
|
|
|
owner._registered_name=self.user
|
|
|
|
if self.handshake+1:
|
|
|
|
return self.on_auth('ok')
|
|
|
|
self.on_auth(None)
|
|
|
|
|
|
|
|
def handshakeHandler(self,disp,stanza):
|
|
|
|
''' Handler for registering in dispatcher for accepting transport authentication. '''
|
|
|
|
if stanza.getName() == 'handshake':
|
|
|
|
self.handshake=1
|
|
|
|
else:
|
|
|
|
self.handshake=-1
|
|
|
|
|
|
|
|
class NonBlockingBind(Bind):
|
|
|
|
''' Bind some JID to the current connection to allow router know of our location.'''
|
|
|
|
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)
|
|
|
|
except NodeProcessed:
|
|
|
|
pass
|
|
|
|
else: self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS)
|
|
|
|
|
|
|
|
def plugout(self):
|
|
|
|
''' Remove Bind handler from owner's dispatcher. Used internally. '''
|
|
|
|
self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS)
|
|
|
|
|
|
|
|
def NonBlockingBind(self, resource=None, on_bound=None):
|
|
|
|
''' Perform binding. Use provided resource name or random (if not provided). '''
|
|
|
|
self.on_bound = on_bound
|
|
|
|
self._resource = resource
|
|
|
|
if self._resource:
|
|
|
|
self._resource = [Node('resource', payload=[self._resource])]
|
|
|
|
else:
|
|
|
|
self._resource = []
|
|
|
|
|
|
|
|
self._owner.onreceive(None)
|
|
|
|
resp=self._owner.Dispatcher.SendAndWaitForResponse(
|
|
|
|
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):
|
|
|
|
self.bound.append(resp.getTag('bind').getTagData('jid'))
|
|
|
|
self.DEBUG('Successfully bound %s.'%self.bound[-1],'ok')
|
|
|
|
jid=JID(resp.getTag('bind').getTagData('jid'))
|
|
|
|
self._owner.User=jid.getNode()
|
|
|
|
self._owner.Resource=jid.getResource()
|
|
|
|
self._owner.SendAndWaitForResponse(Protocol('iq', typ='set',
|
|
|
|
payload=[Node('session', attrs={'xmlns':NS_SESSION})]), func=self._on_session)
|
|
|
|
elif resp:
|
|
|
|
self.DEBUG('Binding failed: %s.' % resp.getTag('error'),'error')
|
|
|
|
self.on_bound(None)
|
|
|
|
else:
|
|
|
|
self.DEBUG('Binding failed: timeout expired.', 'error')
|
|
|
|
self.on_bound(None)
|
|
|
|
|
|
|
|
def _on_session(self, resp):
|
|
|
|
self._owner.onreceive(None)
|
|
|
|
if isResultNode(resp):
|
|
|
|
self.DEBUG('Successfully opened session.', 'ok')
|
|
|
|
self.session = 1
|
|
|
|
self.on_bound('ok')
|
|
|
|
else:
|
|
|
|
self.DEBUG('Session open failed.', 'error')
|
|
|
|
self.session = 0
|
|
|
|
self.on_bound(None)
|
|
|
|
self._owner.onreceive(None)
|
|
|
|
if isResultNode(resp):
|
|
|
|
self.DEBUG('Successfully opened session.', 'ok')
|
|
|
|
self.session = 1
|
|
|
|
self.on_bound('ok')
|
|
|
|
else:
|
|
|
|
self.DEBUG('Session open failed.', 'error')
|
|
|
|
self.session = 0
|
|
|
|
self.on_bound(None)
|
|
|
|
|
|
|
|
class NBComponentBind(ComponentBind):
|
|
|
|
''' ComponentBind some JID to the current connection to allow
|
|
|
|
router know of our location.
|
|
|
|
'''
|
|
|
|
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)
|
|
|
|
except NodeProcessed:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS)
|
|
|
|
self.needsUnregister = 1
|
|
|
|
|
|
|
|
def plugout(self):
|
|
|
|
''' Remove ComponentBind handler from owner's dispatcher. Used internally. '''
|
|
|
|
if self.needsUnregister:
|
|
|
|
self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS)
|
|
|
|
|
|
|
|
def Bind(self, domain = None, on_bind = None):
|
|
|
|
''' Perform binding. Use provided domain name (if not provided). '''
|
|
|
|
self._owner.onreceive(self._on_bound)
|
|
|
|
self.on_bind = on_bind
|
|
|
|
|
|
|
|
def _on_bound(self, resp):
|
|
|
|
if data:
|
|
|
|
self.Dispatcher.ProcessNonBlocking(data)
|
|
|
|
if self.bound is None:
|
|
|
|
return
|
|
|
|
self._owner.onreceive(None)
|
|
|
|
self._owner.SendAndWaitForResponse(
|
|
|
|
Protocol('bind', attrs={'name':domain}, xmlns=NS_COMPONENT_1),
|
|
|
|
func=self._on_bind_reponse)
|
|
|
|
|
|
|
|
def _on_bind_reponse(self, res):
|
|
|
|
if resp and resp.getAttr('error'):
|
|
|
|
self.DEBUG('Binding failed: %s.' % resp.getAttr('error'), 'error')
|
|
|
|
elif resp:
|
|
|
|
self.DEBUG('Successfully bound.', 'ok')
|
|
|
|
if self.on_bind:
|
|
|
|
self.on_bind('ok')
|
|
|
|
else:
|
|
|
|
self.DEBUG('Binding failed: timeout expired.', 'error')
|
|
|
|
if self.on_bind:
|
|
|
|
self.on_bind(None)
|