3 ## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
5 ## This program is free software; you can redistribute it and/or modify
6 ## it under the terms of the GNU General Public License as published by
7 ## the Free Software Foundation; either version 2, or (at your option)
10 ## This program is distributed in the hope that it will be useful,
11 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 ## GNU General Public License for more details.
15 # $Id: client.py,v 1.60 2007/08/28 10:03:33 normanr Exp $
18 Provides PlugIn class functionality to develop extentions for xmpppy.
19 Also provides Client and Component classes implementations as the
20 examples of xmpppy structures usage.
21 These classes can be used for simple applications "AS IS" though.
27 Debug
.DEBUGGING_IS_ON
=1
28 Debug
.Debug
.colors
['socket']=debug
.color_dark_gray
29 Debug
.Debug
.colors
['CONNECTproxy']=debug
.color_dark_gray
30 Debug
.Debug
.colors
['nodebuilder']=debug
.color_brown
31 Debug
.Debug
.colors
['client']=debug
.color_cyan
32 Debug
.Debug
.colors
['component']=debug
.color_cyan
33 Debug
.Debug
.colors
['dispatcher']=debug
.color_green
34 Debug
.Debug
.colors
['browser']=debug
.color_blue
35 Debug
.Debug
.colors
['auth']=debug
.color_yellow
36 Debug
.Debug
.colors
['roster']=debug
.color_magenta
37 Debug
.Debug
.colors
['ibb']=debug
.color_yellow
39 Debug
.Debug
.colors
['down']=debug
.color_brown
40 Debug
.Debug
.colors
['up']=debug
.color_brown
41 Debug
.Debug
.colors
['data']=debug
.color_brown
42 Debug
.Debug
.colors
['ok']=debug
.color_green
43 Debug
.Debug
.colors
['warn']=debug
.color_yellow
44 Debug
.Debug
.colors
['error']=debug
.color_red
45 Debug
.Debug
.colors
['start']=debug
.color_dark_gray
46 Debug
.Debug
.colors
['stop']=debug
.color_dark_gray
47 Debug
.Debug
.colors
['sent']=debug
.color_yellow
48 Debug
.Debug
.colors
['got']=debug
.color_bright_cyan
51 DBG_COMPONENT
='component'
54 """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """
56 self
._exported
_methods
=[]
57 self
.DBG_LINE
=self
.__class
__.__name
__.lower()
59 def PlugIn(self
,owner
):
60 """ Attach to main instance and register ourself and all our staff in it. """
62 if self
.DBG_LINE
not in owner
.debug_flags
:
63 owner
.debug_flags
.append(self
.DBG_LINE
)
64 self
.DEBUG('Plugging %s into %s'%(self
,self
._owner
),'start')
65 if owner
.__dict
__.has_key(self
.__class
__.__name
__):
66 return self
.DEBUG('Plugging ignored: another instance already plugged.','error')
67 self
._old
_owners
_methods
=[]
68 for method
in self
._exported
_methods
:
69 if owner
.__dict
__.has_key(method
.__name
__):
70 self
._old
_owners
_methods
.append(owner
.__dict
__[method
.__name
__])
71 owner
.__dict
__[method
.__name
__]=method
72 owner
.__dict
__[self
.__class
__.__name
__]=self
73 if self
.__class
__.__dict
__.has_key('plugin'): return self
.plugin(owner
)
76 """ Unregister all our staff from main instance and detach from it. """
77 self
.DEBUG('Plugging %s out of %s.'%(self
,self
._owner
),'stop')
79 if self
.__class
__.__dict
__.has_key('plugout'): ret
= self
.plugout()
80 self
._owner
.debug_flags
.remove(self
.DBG_LINE
)
81 for method
in self
._exported
_methods
: del self
._owner
.__dict
__[method
.__name
__]
82 for method
in self
._old
_owners
_methods
: self
._owner
.__dict
__[method
.__name
__]=method
83 del self
._owner
.__dict
__[self
.__class
__.__name
__]
86 def DEBUG(self
,text
,severity
='info'):
87 """ Feed a provided debug line to main instance's debug facility along with our ID string. """
88 self
._owner
.DEBUG(self
.DBG_LINE
,text
,severity
)
90 import transports
,dispatcher
,auth
,roster
92 """ Base for Client and Component classes."""
93 def __init__(self
,server
,port
=5222,debug
=['always', 'nodebuilder']):
94 """ Caches server name and (optionally) port to connect to. "debug" parameter specifies
95 the debug IDs that will go into debug output. You can either specifiy an "include"
96 or "exclude" list. The latter is done via adding "always" pseudo-ID to the list.
97 Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket',
98 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """
99 if self
.__class
__.__name
__=='Client': self
.Namespace
,self
.DBG
='jabber:client',DBG_CLIENT
100 elif self
.__class
__.__name
__=='Component': self
.Namespace
,self
.DBG
=dispatcher
.NS_COMPONENT_ACCEPT
,DBG_COMPONENT
101 self
.defaultNamespace
=self
.Namespace
102 self
.disconnect_handlers
=[]
105 if debug
and type(debug
)<>list: debug
=['always', 'nodebuilder']
106 self
._DEBUG
=Debug
.Debug(debug
)
107 self
.DEBUG
=self
._DEBUG
.Show
108 self
.debug_flags
=self
._DEBUG
.debug_flags
109 self
.debug_flags
.append(self
.DBG
)
111 self
._registered
_name
=None
112 self
.RegisterDisconnectHandler(self
.DisconnectHandler
)
116 def RegisterDisconnectHandler(self
,handler
):
117 """ Register handler that will be called on disconnect."""
118 self
.disconnect_handlers
.append(handler
)
120 def UnregisterDisconnectHandler(self
,handler
):
121 """ Unregister handler that is called on disconnect."""
122 self
.disconnect_handlers
.remove(handler
)
124 def disconnected(self
):
125 """ Called on disconnection. Calls disconnect handlers and cleans things up. """
127 self
.DEBUG(self
.DBG
,'Disconnect detected','stop')
128 self
.disconnect_handlers
.reverse()
129 for i
in self
.disconnect_handlers
: i()
130 self
.disconnect_handlers
.reverse()
131 if self
.__dict
__.has_key('TLS'): self
.TLS
.PlugOut()
133 def DisconnectHandler(self
):
134 """ Default disconnect handler. Just raises an IOError.
135 If you choosed to use this class in your production client,
136 override this method or at least unregister it. """
137 raise IOError('Disconnected from server.')
139 def event(self
,eventName
,args
={}):
140 """ Default event handler. To be overriden. """
141 print "Event: ",(eventName
,args
)
143 def isConnected(self
):
144 """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """
145 return self
.connected
147 def reconnectAndReauth(self
):
148 """ Example of reconnection method. In fact, it can be used to batch connection and auth as well. """
149 handlerssave
=self
.Dispatcher
.dumpHandlers()
150 if self
.__dict
__.has_key('ComponentBind'): self
.ComponentBind
.PlugOut()
151 if self
.__dict
__.has_key('Bind'): self
.Bind
.PlugOut()
153 if self
.__dict
__.has_key('NonSASL'): self
.NonSASL
.PlugOut()
154 if self
.__dict
__.has_key('SASL'): self
.SASL
.PlugOut()
155 if self
.__dict
__.has_key('TLS'): self
.TLS
.PlugOut()
156 self
.Dispatcher
.PlugOut()
157 if self
.__dict
__.has_key('HTTPPROXYsocket'): self
.HTTPPROXYsocket
.PlugOut()
158 if self
.__dict
__.has_key('TCPsocket'): self
.TCPsocket
.PlugOut()
159 if not self
.connect(server
=self
._Server
,proxy
=self
._Proxy
): return
160 if not self
.auth(self
._User
,self
._Password
,self
._Resource
): return
161 self
.Dispatcher
.restoreHandlers(handlerssave
)
162 return self
.connected
164 def connect(self
,server
=None,proxy
=None,ssl
=None,use_srv
=None):
165 """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream.
166 Returns None or 'tcp' or 'tls', depending on the result."""
167 if not server
: server
=(self
.Server
,self
.Port
)
168 if proxy
: sock
=transports
.HTTPPROXYsocket(proxy
,server
,use_srv
)
169 else: sock
=transports
.TCPsocket(server
,use_srv
)
170 connected
=sock
.PlugIn(self
)
174 self
._Server
,self
._Proxy
=server
,proxy
176 if (ssl
is None and self
.Connection
.getPort() in (5223, 443)) or ssl
:
177 try: # FIXME. This should be done in transports.py
178 transports
.TLS().PlugIn(self
,now
=1)
180 except socket
.sslerror
:
182 dispatcher
.Dispatcher().PlugIn(self
)
183 while self
.Dispatcher
.Stream
._document
_attrs
is None:
184 if not self
.Process(1): return
185 if self
.Dispatcher
.Stream
._document
_attrs
.has_key('version') and self
.Dispatcher
.Stream
._document
_attrs
['version']=='1.0':
186 while not self
.Dispatcher
.Stream
.features
and self
.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented
187 return self
.connected
189 class Client(CommonClient
):
190 """ Example client class, based on CommonClient. """
191 def connect(self
,server
=None,proxy
=None,secure
=None,use_srv
=True):
192 """ Connect to jabber server. If you want to specify different ip/port to connect to you can
193 pass it as tuple as first parameter. If there is HTTP proxy between you and server
194 specify it's address and credentials (if needed) in the second argument.
195 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)
196 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.
197 If you want to disable tls/ssl support completely, set it to 0.
198 Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'})
199 Returns '' or 'tcp' or 'tls', depending on the result."""
200 if not CommonClient
.connect(self
,server
,proxy
,secure
,use_srv
) or secure
<>None and not secure
: return self
.connected
201 transports
.TLS().PlugIn(self
)
202 if not self
.Dispatcher
.Stream
._document
_attrs
.has_key('version') or not self
.Dispatcher
.Stream
._document
_attrs
['version']=='1.0': return self
.connected
203 while not self
.Dispatcher
.Stream
.features
and self
.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented
204 if not self
.Dispatcher
.Stream
.features
.getTag('starttls'): return self
.connected
# TLS not supported by server
205 while not self
.TLS
.starttls
and self
.Process(1): pass
206 if not hasattr(self
, 'TLS') or self
.TLS
.starttls
!='success': self
.event('tls_failed'); return self
.connected
208 return self
.connected
210 def auth(self
,user
,password
,resource
='',sasl
=1):
211 """ Authenticate connnection and bind resource. If resource is not provided
212 random one or library name used. """
213 self
._User
,self
._Password
,self
._Resource
=user
,password
,resource
214 while not self
.Dispatcher
.Stream
._document
_attrs
and self
.Process(1): pass
215 if self
.Dispatcher
.Stream
._document
_attrs
.has_key('version') and self
.Dispatcher
.Stream
._document
_attrs
['version']=='1.0':
216 while not self
.Dispatcher
.Stream
.features
and self
.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented
217 if sasl
: auth
.SASL(user
,password
).PlugIn(self
)
218 if not sasl
or self
.SASL
.startsasl
=='not-supported':
219 if not resource
: resource
='xmpppy'
220 if auth
.NonSASL(user
,password
,resource
).PlugIn(self
):
221 self
.connected
+='+old_auth'
225 while self
.SASL
.startsasl
=='in-process' and self
.Process(1): pass
226 if self
.SASL
.startsasl
=='success':
227 auth
.Bind().PlugIn(self
)
228 while self
.Bind
.bound
is None and self
.Process(1): pass
229 if self
.Bind
.Bind(resource
):
230 self
.connected
+='+sasl'
233 if self
.__dict
__.has_key('SASL'): self
.SASL
.PlugOut()
236 """ Return the Roster instance, previously plugging it in and
237 requesting roster from server if needed. """
238 if not self
.__dict
__.has_key('Roster'): roster
.Roster().PlugIn(self
)
239 return self
.Roster
.getRoster()
241 def sendInitPresence(self
,requestRoster
=1):
242 """ Send roster request and initial <presence/>.
243 You can disable the first by setting requestRoster argument to 0. """
244 self
.sendPresence(requestRoster
=requestRoster
)
246 def sendPresence(self
,jid
=None,typ
=None,requestRoster
=0):
247 """ Send some specific presence state.
248 Can also request roster from server if according agrument is set."""
249 if requestRoster
: roster
.Roster().PlugIn(self
)
250 self
.send(dispatcher
.Presence(to
=jid
, typ
=typ
))
252 class Component(CommonClient
):
253 """ Component class. The only difference from CommonClient is ability to perform component authentication. """
254 def __init__(self
,server
,port
=5347,typ
=None,debug
=['always', 'nodebuilder'],domains
=None,sasl
=0,bind
=0,route
=0,xcp
=0):
255 """ Init function for Components.
256 As components use a different auth mechanism which includes the namespace of the component.
257 Jabberd1.4 and Ejabberd use the default namespace then for all client messages.
258 Jabberd2 uses jabber:client.
259 'server' argument is a server name that you are connecting to (f.e. "localhost").
260 'port' can be specified if 'server' resolves to correct IP. If it is not then you'll need to specify IP
261 and port while calling "connect()"."""
262 CommonClient
.__init
__(self
,server
,port
=port
,debug
=debug
)
271 self
.domains
=[server
]
273 def connect(self
,server
=None,proxy
=None):
274 """ This will connect to the server, and if the features tag is found then set
275 the namespace to be jabber:client as that is required for jabberd2.
276 'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """
278 self
.Namespace
=auth
.NS_COMPONENT_1
279 self
.Server
=server
[0]
280 CommonClient
.connect(self
,server
=server
,proxy
=proxy
)
281 if self
.connected
and (self
.typ
=='jabberd2' or not self
.typ
and self
.Dispatcher
.Stream
.features
!= None) and (not self
.xcp
):
282 self
.defaultNamespace
=auth
.NS_CLIENT
283 self
.Dispatcher
.RegisterNamespace(self
.defaultNamespace
)
284 self
.Dispatcher
.RegisterProtocol('iq',dispatcher
.Iq
)
285 self
.Dispatcher
.RegisterProtocol('message',dispatcher
.Message
)
286 self
.Dispatcher
.RegisterProtocol('presence',dispatcher
.Presence
)
287 return self
.connected
289 def dobind(self
, sasl
):
290 # This has to be done before binding, because we can receive a route stanza before binding finishes
291 self
._route
= self
.route
293 for domain
in self
.domains
:
294 auth
.ComponentBind(sasl
).PlugIn(self
)
295 while self
.ComponentBind
.bound
is None: self
.Process(1)
296 if (not self
.ComponentBind
.Bind(domain
)):
297 self
.ComponentBind
.PlugOut()
299 self
.ComponentBind
.PlugOut()
301 def auth(self
,name
,password
,dup
=None):
302 """ Authenticate component "name" with password "password"."""
303 self
._User
,self
._Password
,self
._Resource
=name
,password
,''
305 if self
.sasl
: auth
.SASL(name
,password
).PlugIn(self
)
306 if not self
.sasl
or self
.SASL
.startsasl
=='not-supported':
307 if auth
.NonSASL(name
,password
,'').PlugIn(self
):
308 self
.dobind(sasl
=False)
309 self
.connected
+='+old_auth'
313 while self
.SASL
.startsasl
=='in-process' and self
.Process(1): pass
314 if self
.SASL
.startsasl
=='success':
315 self
.dobind(sasl
=True)
316 self
.connected
+='+sasl'
319 raise auth
.NotAuthorized(self
.SASL
.startsasl
)
321 self
.DEBUG(self
.DBG
,"Failed to authenticate %s"%name
,'error')