2005-04-26 20:45:54 +02:00
|
|
|
## client.py
|
|
|
|
##
|
|
|
|
## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
|
|
|
|
##
|
|
|
|
## 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.
|
|
|
|
|
2006-01-18 21:46:29 +01:00
|
|
|
# $Id: client.py,v 1.52 2006/01/02 19:40:55 normanr Exp $
|
2005-04-26 20:45:54 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
Provides PlugIn class functionality to develop extentions for xmpppy.
|
|
|
|
Also provides Client and Component classes implementations as the
|
|
|
|
examples of xmpppy structures usage.
|
|
|
|
These classes can be used for simple applications "AS IS" though.
|
|
|
|
"""
|
|
|
|
|
2005-09-01 19:44:57 +02:00
|
|
|
import socket
|
2005-04-26 20:45:54 +02:00
|
|
|
import debug
|
|
|
|
Debug=debug
|
|
|
|
Debug.DEBUGGING_IS_ON=1
|
|
|
|
Debug.Debug.colors['socket']=debug.color_dark_gray
|
|
|
|
Debug.Debug.colors['CONNECTproxy']=debug.color_dark_gray
|
|
|
|
Debug.Debug.colors['nodebuilder']=debug.color_brown
|
|
|
|
Debug.Debug.colors['client']=debug.color_cyan
|
|
|
|
Debug.Debug.colors['component']=debug.color_cyan
|
|
|
|
Debug.Debug.colors['dispatcher']=debug.color_green
|
2006-01-18 21:46:29 +01:00
|
|
|
Debug.Debug.colors['browser']=debug.color_blue
|
2005-04-26 20:45:54 +02:00
|
|
|
Debug.Debug.colors['auth']=debug.color_yellow
|
|
|
|
Debug.Debug.colors['roster']=debug.color_magenta
|
|
|
|
Debug.Debug.colors['ibb']=debug.color_yellow
|
|
|
|
|
|
|
|
Debug.Debug.colors['down']=debug.color_brown
|
|
|
|
Debug.Debug.colors['up']=debug.color_brown
|
|
|
|
Debug.Debug.colors['data']=debug.color_brown
|
|
|
|
Debug.Debug.colors['ok']=debug.color_green
|
|
|
|
Debug.Debug.colors['warn']=debug.color_yellow
|
|
|
|
Debug.Debug.colors['error']=debug.color_red
|
|
|
|
Debug.Debug.colors['start']=debug.color_dark_gray
|
|
|
|
Debug.Debug.colors['stop']=debug.color_dark_gray
|
|
|
|
Debug.Debug.colors['sent']=debug.color_yellow
|
|
|
|
Debug.Debug.colors['got']=debug.color_bright_cyan
|
|
|
|
|
|
|
|
DBG_CLIENT='client'
|
|
|
|
DBG_COMPONENT='component'
|
|
|
|
|
|
|
|
class PlugIn:
|
|
|
|
""" Common xmpppy plugins infrastructure: plugging in/out, debugging. """
|
|
|
|
def __init__(self):
|
|
|
|
self._exported_methods=[]
|
|
|
|
self.DBG_LINE=self.__class__.__name__.lower()
|
|
|
|
|
|
|
|
def PlugIn(self,owner):
|
|
|
|
""" Attach to main instance and register ourself and all our staff in it. """
|
|
|
|
self._owner=owner
|
|
|
|
if self.DBG_LINE not in owner.debug_flags:
|
|
|
|
owner.debug_flags.append(self.DBG_LINE)
|
|
|
|
self.DEBUG('Plugging %s into %s'%(self,self._owner),'start')
|
|
|
|
if owner.__dict__.has_key(self.__class__.__name__):
|
|
|
|
return self.DEBUG('Plugging ignored: another instance already plugged.','error')
|
|
|
|
self._old_owners_methods=[]
|
|
|
|
for method in self._exported_methods:
|
|
|
|
if owner.__dict__.has_key(method.__name__):
|
|
|
|
self._old_owners_methods.append(owner.__dict__[method.__name__])
|
|
|
|
owner.__dict__[method.__name__]=method
|
|
|
|
owner.__dict__[self.__class__.__name__]=self
|
|
|
|
if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner)
|
|
|
|
|
|
|
|
def PlugOut(self):
|
|
|
|
""" Unregister all our staff from main instance and detach from it. """
|
|
|
|
self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop')
|
|
|
|
self._owner.debug_flags.remove(self.DBG_LINE)
|
|
|
|
for method in self._exported_methods: del self._owner.__dict__[method.__name__]
|
|
|
|
for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method
|
|
|
|
del self._owner.__dict__[self.__class__.__name__]
|
|
|
|
if self.__class__.__dict__.has_key('plugout'): return self.plugout()
|
2006-03-16 18:29:30 +01:00
|
|
|
del self._owner
|
2005-04-26 20:45:54 +02:00
|
|
|
|
|
|
|
def DEBUG(self,text,severity='info'):
|
|
|
|
""" Feed a provided debug line to main instance's debug facility along with our ID string. """
|
|
|
|
self._owner.DEBUG(self.DBG_LINE,text,severity)
|
|
|
|
|
|
|
|
import transports,dispatcher,auth,roster
|
|
|
|
class CommonClient:
|
|
|
|
""" Base for Client and Component classes."""
|
2005-11-06 11:12:22 +01:00
|
|
|
def __init__(self,server,port=5222,debug=['always', 'nodebuilder'],caller=None):
|
2005-04-26 20:45:54 +02:00
|
|
|
""" Caches server name and (optionally) port to connect to. "debug" parameter specifies
|
|
|
|
the debug IDs that will go into debug output. You can either specifiy an "include"
|
|
|
|
or "exclude" list. The latter is done via adding "always" pseudo-ID to the list.
|
|
|
|
Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket',
|
|
|
|
'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """
|
|
|
|
if self.__class__.__name__=='Client': self.Namespace,self.DBG='jabber:client',DBG_CLIENT
|
|
|
|
elif self.__class__.__name__=='Component': self.Namespace,self.DBG=dispatcher.NS_COMPONENT_ACCEPT,DBG_COMPONENT
|
|
|
|
self.defaultNamespace=self.Namespace
|
|
|
|
self.disconnect_handlers=[]
|
|
|
|
self.Server=server
|
|
|
|
self.Port=port
|
2005-11-06 11:12:22 +01:00
|
|
|
# Who initiated this client
|
|
|
|
# Used to register the EventDispatcher
|
|
|
|
self._caller=caller
|
2005-04-26 20:45:54 +02:00
|
|
|
if debug and type(debug)<>list: debug=['always', 'nodebuilder']
|
|
|
|
self._DEBUG=Debug.Debug(debug)
|
|
|
|
self.DEBUG=self._DEBUG.Show
|
|
|
|
self.debug_flags=self._DEBUG.debug_flags
|
|
|
|
self.debug_flags.append(self.DBG)
|
|
|
|
self._owner=self
|
|
|
|
self._registered_name=None
|
2005-05-07 12:57:40 +02:00
|
|
|
self.RegisterDisconnectHandler(self.DisconnectHandler)
|
2005-04-26 20:45:54 +02:00
|
|
|
self.connected=''
|
2006-01-18 21:46:29 +01:00
|
|
|
self._component=0
|
2005-04-26 20:45:54 +02:00
|
|
|
|
|
|
|
def RegisterDisconnectHandler(self,handler):
|
|
|
|
""" Register handler that will be called on disconnect."""
|
|
|
|
self.disconnect_handlers.append(handler)
|
|
|
|
|
|
|
|
def UnregisterDisconnectHandler(self,handler):
|
|
|
|
""" Unregister handler that is called on disconnect."""
|
|
|
|
self.disconnect_handlers.remove(handler)
|
|
|
|
|
|
|
|
def disconnected(self):
|
|
|
|
""" Called on disconnection. Calls disconnect handlers and cleans things up. """
|
|
|
|
self.connected=''
|
|
|
|
self.DEBUG(self.DBG,'Disconnect detected','stop')
|
|
|
|
self.disconnect_handlers.reverse()
|
|
|
|
for i in self.disconnect_handlers: i()
|
|
|
|
self.disconnect_handlers.reverse()
|
2005-05-07 12:57:40 +02:00
|
|
|
if self.__dict__.has_key('TLS'): self.TLS.PlugOut()
|
2005-04-26 20:45:54 +02:00
|
|
|
|
|
|
|
def DisconnectHandler(self):
|
|
|
|
""" Default disconnect handler. Just raises an IOError.
|
|
|
|
If you choosed to use this class in your production client,
|
|
|
|
override this method or at least unregister it. """
|
|
|
|
raise IOError('Disconnected from server.')
|
|
|
|
|
|
|
|
def event(self,eventName,args={}):
|
|
|
|
""" Default event handler. To be overriden. """
|
|
|
|
print "Event: ",(eventName,args)
|
|
|
|
|
|
|
|
def isConnected(self):
|
|
|
|
""" Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """
|
|
|
|
return self.connected
|
|
|
|
|
|
|
|
def reconnectAndReauth(self):
|
|
|
|
""" Example of reconnection method. In fact, it can be used to batch connection and auth as well. """
|
|
|
|
handlerssave=self.Dispatcher.dumpHandlers()
|
|
|
|
self.Dispatcher.PlugOut()
|
2006-01-20 14:23:38 +01:00
|
|
|
if self.__dict__.has_key('NonSASL'): self.NonSASL.PlugOut()
|
|
|
|
if self.__dict__.has_key('SASL'): self.SASL.PlugOut()
|
|
|
|
if self.__dict__.has_key('TLS'): self.TLS.PlugOut()
|
|
|
|
if self.__dict__.has_key('HTTPPROXYsocket'): self.HTTPPROXYsocket.PlugOut()
|
|
|
|
if self.__dict__.has_key('TCPsocket'): self.TCPsocket.PlugOut()
|
2005-04-26 20:45:54 +02:00
|
|
|
if not self.connect(server=self._Server,proxy=self._Proxy): return
|
|
|
|
if not self.auth(self._User,self._Password,self._Resource): return
|
|
|
|
self.Dispatcher.restoreHandlers(handlerssave)
|
|
|
|
return self.connected
|
|
|
|
|
2005-08-02 00:43:33 +02:00
|
|
|
def get_peerhost(self):
|
|
|
|
''' get the ip address of the account, from which is made connection
|
|
|
|
to the server , (e.g. me).
|
|
|
|
We will create listening socket on the same ip '''
|
2005-08-02 16:25:21 +02:00
|
|
|
if hasattr(self, 'Connection'):
|
2005-08-02 00:43:33 +02:00
|
|
|
return self.Connection._sock.getsockname()
|
|
|
|
|
2006-01-18 21:46:29 +01:00
|
|
|
def connect(self,server=None,proxy=None,ssl=None,use_srv=None):
|
|
|
|
""" Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream.
|
|
|
|
Returns None or 'tcp' or 'tls', depending on the result."""
|
2005-04-26 20:45:54 +02:00
|
|
|
if not server: server=(self.Server,self.Port)
|
2006-01-18 21:46:29 +01:00
|
|
|
if proxy: socket=transports.HTTPPROXYsocket(proxy,server,use_srv)
|
|
|
|
else: socket=transports.TCPsocket(server,use_srv)
|
|
|
|
connected=socket.PlugIn(self)
|
|
|
|
if not connected:
|
|
|
|
socket.PlugOut()
|
|
|
|
return
|
2005-04-26 20:45:54 +02:00
|
|
|
self._Server,self._Proxy=server,proxy
|
|
|
|
self.connected='tcp'
|
2005-06-18 13:22:19 +02:00
|
|
|
if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl:
|
2006-01-18 21:46:29 +01:00
|
|
|
try: # FIXME. This should be done in transports.py
|
2005-09-01 19:44:57 +02:00
|
|
|
transports.TLS().PlugIn(self,now=1)
|
|
|
|
self.connected='ssl'
|
|
|
|
except socket.sslerror:
|
|
|
|
return
|
2005-04-26 20:45:54 +02:00
|
|
|
dispatcher.Dispatcher().PlugIn(self)
|
2005-06-18 13:33:29 +02:00
|
|
|
while self.Dispatcher.Stream._document_attrs is None:
|
|
|
|
if not self.Process(1): return
|
2005-04-26 20:45:54 +02:00
|
|
|
if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0':
|
|
|
|
while not self.Dispatcher.Stream.features and self.Process(): pass # If we get version 1.0 stream the features tag MUST BE presented
|
|
|
|
return self.connected
|
|
|
|
|
|
|
|
class Client(CommonClient):
|
|
|
|
""" Example client class, based on CommonClient. """
|
2006-01-18 21:46:29 +01:00
|
|
|
def connect(self,server=None,proxy=None,secure=None,use_srv=True):
|
2005-04-26 20:45:54 +02:00
|
|
|
""" Connect to jabber server. If you want to specify different ip/port to connect to you can
|
2006-01-18 21:46:29 +01:00
|
|
|
pass it as tuple as first parameter. If there is HTTP proxy between you and server
|
|
|
|
specify it's address and credentials (if needed) in the second argument.
|
2005-06-18 13:22:19 +02:00
|
|
|
If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443)
|
2006-01-18 21:46:29 +01:00
|
|
|
If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1.
|
|
|
|
If you want to disable tls/ssl support completely, set it to 0.
|
2005-06-18 13:33:29 +02:00
|
|
|
Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'})
|
2006-01-18 21:46:29 +01:00
|
|
|
Returns '' or 'tcp' or 'tls', depending on the result."""
|
|
|
|
if not CommonClient.connect(self,server,proxy,secure,use_srv) or secure<>None and not secure: return self.connected
|
2005-05-07 12:57:40 +02:00
|
|
|
transports.TLS().PlugIn(self)
|
2005-04-26 20:45:54 +02:00
|
|
|
if not self.Dispatcher.Stream._document_attrs.has_key('version') or not self.Dispatcher.Stream._document_attrs['version']=='1.0': return self.connected
|
|
|
|
while not self.Dispatcher.Stream.features and self.Process(): pass # If we get version 1.0 stream the features tag MUST BE presented
|
2005-05-07 12:57:40 +02:00
|
|
|
if not self.Dispatcher.Stream.features.getTag('starttls'): return self.connected # TLS not supported by server
|
|
|
|
while not self.TLS.starttls and self.Process(): pass
|
2005-11-16 22:39:17 +01:00
|
|
|
if not hasattr(self, 'TLS') or self.TLS.starttls!='success': self.event('tls_failed'); return self.connected
|
2005-05-07 12:57:40 +02:00
|
|
|
self.connected='tls'
|
2005-04-26 20:45:54 +02:00
|
|
|
return self.connected
|
|
|
|
|
2005-05-24 20:08:37 +02:00
|
|
|
def auth(self,user,password,resource='',sasl=1):
|
2005-04-26 20:45:54 +02:00
|
|
|
""" Authenticate connnection and bind resource. If resource is not provided
|
|
|
|
random one or library name used. """
|
|
|
|
self._User,self._Password,self._Resource=user,password,resource
|
|
|
|
while not self.Dispatcher.Stream._document_attrs and self.Process(): pass
|
|
|
|
if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0':
|
|
|
|
while not self.Dispatcher.Stream.features and self.Process(): pass # If we get version 1.0 stream the features tag MUST BE presented
|
2005-05-26 08:50:17 +02:00
|
|
|
if sasl: auth.SASL(user,password).PlugIn(self)
|
2005-05-24 20:08:37 +02:00
|
|
|
if not sasl or self.SASL.startsasl=='not-supported':
|
2005-04-26 20:45:54 +02:00
|
|
|
if not resource: resource='xmpppy'
|
|
|
|
if auth.NonSASL(user,password,resource).PlugIn(self):
|
|
|
|
self.connected+='+old_auth'
|
|
|
|
return 'old_auth'
|
2005-05-07 12:57:40 +02:00
|
|
|
return
|
2005-05-26 08:50:17 +02:00
|
|
|
self.SASL.auth()
|
2005-05-07 12:57:40 +02:00
|
|
|
while self.SASL.startsasl=='in-process' and self.Process(): pass
|
|
|
|
if self.SASL.startsasl=='success':
|
2005-04-26 20:45:54 +02:00
|
|
|
auth.Bind().PlugIn(self)
|
2005-06-18 13:33:29 +02:00
|
|
|
while self.Bind.bound is None and self.Process(): pass
|
2005-04-26 20:45:54 +02:00
|
|
|
if self.Bind.Bind(resource):
|
|
|
|
self.connected+='+sasl'
|
|
|
|
return 'sasl'
|
|
|
|
|
2005-05-08 19:00:41 +02:00
|
|
|
def initRoster(self):
|
|
|
|
""" Plug in the roster. """
|
|
|
|
if not self.__dict__.has_key('Roster'): roster.Roster().PlugIn(self)
|
|
|
|
|
2005-04-26 20:45:54 +02:00
|
|
|
def getRoster(self):
|
|
|
|
""" Return the Roster instance, previously plugging it in and
|
|
|
|
requesting roster from server if needed. """
|
2005-05-08 19:00:41 +02:00
|
|
|
self.initRoster()
|
2005-04-26 20:45:54 +02:00
|
|
|
return self.Roster.getRoster()
|
|
|
|
|
|
|
|
def sendInitPresence(self,requestRoster=1):
|
|
|
|
""" Send roster request and initial <presence/>.
|
|
|
|
You can disable the first by setting requestRoster argument to 0. """
|
|
|
|
self.sendPresence(requestRoster=requestRoster)
|
|
|
|
|
|
|
|
def sendPresence(self,jid=None,typ=None,requestRoster=0):
|
|
|
|
""" Send some specific presence state.
|
|
|
|
Can also request roster from server if according agrument is set."""
|
|
|
|
if requestRoster: roster.Roster().PlugIn(self)
|
|
|
|
self.send(dispatcher.Presence(to=jid, typ=typ))
|
|
|
|
|
|
|
|
class Component(CommonClient):
|
|
|
|
""" Component class. The only difference from CommonClient is ability to perform component authentication. """
|
2006-01-18 21:46:29 +01:00
|
|
|
def __init__(self,server,port=5347,typ=None,debug=['always', 'nodebuilder'],domains=None,component=0):
|
2005-04-26 20:45:54 +02:00
|
|
|
""" Init function for Components.
|
|
|
|
As components use a different auth mechanism which includes the namespace of the component.
|
|
|
|
Jabberd1.4 and Ejabberd use the default namespace then for all client messages.
|
2006-01-18 21:46:29 +01:00
|
|
|
Jabberd2 uses jabber:client.
|
|
|
|
'server' argument is a server name that you are connecting to (f.e. "localhost").
|
|
|
|
'port' can be specified if 'server' resolves to correct IP. If it is not then you'll need to specify IP
|
|
|
|
and port while calling "connect()"."""
|
2005-04-26 20:45:54 +02:00
|
|
|
CommonClient.__init__(self,server,port=port,debug=debug)
|
|
|
|
self.typ=typ
|
2006-01-18 21:46:29 +01:00
|
|
|
self.component=component
|
|
|
|
if domains:
|
|
|
|
self.domains=domains
|
|
|
|
else:
|
|
|
|
self.domains=[server]
|
2005-04-26 20:45:54 +02:00
|
|
|
|
|
|
|
def connect(self,server=None,proxy=None):
|
|
|
|
""" This will connect to the server, and if the features tag is found then set
|
2006-01-18 21:46:29 +01:00
|
|
|
the namespace to be jabber:client as that is required for jabberd2.
|
|
|
|
'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """
|
|
|
|
if self.component:
|
|
|
|
self.Namespace=auth.NS_COMPONENT_1
|
|
|
|
self.Server=server[0]
|
2005-04-26 20:45:54 +02:00
|
|
|
CommonClient.connect(self,server=server,proxy=proxy)
|
2008-04-18 02:26:07 +02:00
|
|
|
if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features is not None):
|
2005-04-26 20:45:54 +02:00
|
|
|
self.defaultNamespace=auth.NS_CLIENT
|
|
|
|
self.Dispatcher.RegisterNamespace(self.defaultNamespace)
|
|
|
|
self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq)
|
|
|
|
self.Dispatcher.RegisterProtocol('message',dispatcher.Message)
|
|
|
|
self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence)
|
|
|
|
return self.connected
|
|
|
|
|
2006-01-18 21:46:29 +01:00
|
|
|
def auth(self,name,password,dup=None,sasl=0):
|
2005-04-26 20:45:54 +02:00
|
|
|
""" Authenticate component "name" with password "password"."""
|
|
|
|
self._User,self._Password,self._Resource=name,password,''
|
2006-01-18 21:46:29 +01:00
|
|
|
try:
|
|
|
|
if self.component: sasl=1
|
|
|
|
if sasl: auth.SASL(name,password).PlugIn(self)
|
|
|
|
if not sasl or self.SASL.startsasl=='not-supported':
|
|
|
|
if auth.NonSASL(name,password,'').PlugIn(self):
|
|
|
|
self.connected+='+old_auth'
|
|
|
|
return 'old_auth'
|
|
|
|
return
|
|
|
|
self.SASL.auth()
|
|
|
|
while self.SASL.startsasl=='in-process' and self.Process(): pass
|
|
|
|
if self.SASL.startsasl=='success':
|
|
|
|
if self.component:
|
|
|
|
self._component=self.component
|
|
|
|
for domain in self.domains:
|
|
|
|
auth.ComponentBind().PlugIn(self)
|
|
|
|
while self.ComponentBind.bound is None: self.Process()
|
|
|
|
if (not self.ComponentBind.Bind(domain)):
|
|
|
|
self.ComponentBind.PlugOut()
|
|
|
|
return
|
|
|
|
self.ComponentBind.PlugOut()
|
|
|
|
self.connected+='+sasl'
|
|
|
|
return 'sasl'
|
|
|
|
else:
|
|
|
|
raise auth.NotAuthorized(self.SASL.startsasl)
|
|
|
|
except:
|
|
|
|
self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error')
|
2008-07-29 21:49:31 +02:00
|
|
|
|
|
|
|
# vim: se ts=3:
|