use local1.info as it's ridiculously spammy
[uccdoor.git] / xmpp / client.py
1 ##   client.py
2 ##
3 ##   Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
4 ##
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)
8 ##   any later version.
9 ##
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.
14
15 # $Id: client.py,v 1.61 2009/04/07 06:19:42 snakeru Exp $
16
17 """
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.
22 """
23
24 import socket
25 import debug
26 Debug=debug
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
38
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
49
50 DBG_CLIENT='client'
51 DBG_COMPONENT='component'
52
53 class PlugIn:
54     """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """
55     def __init__(self):
56         self._exported_methods=[]
57         self.DBG_LINE=self.__class__.__name__.lower()
58
59     def PlugIn(self,owner):
60         """ Attach to main instance and register ourself and all our staff in it. """
61         self._owner=owner
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)
74  
75     def PlugOut(self):
76         """ Unregister all our staff from main instance and detach from it. """
77         self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop')
78         ret = None
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__]
84         return ret
85
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)
89
90 import transports,dispatcher,auth,roster
91 class CommonClient:
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=[]
103         self.Server=server
104         self.Port=port
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)
110         self._owner=self
111         self._registered_name=None
112         self.RegisterDisconnectHandler(self.DisconnectHandler)
113         self.connected=''
114         self._route=0
115
116     def RegisterDisconnectHandler(self,handler):
117         """ Register handler that will be called on disconnect."""
118         self.disconnect_handlers.append(handler)
119
120     def UnregisterDisconnectHandler(self,handler):
121         """ Unregister handler that is called on disconnect."""
122         self.disconnect_handlers.remove(handler)
123
124     def disconnected(self):
125         """ Called on disconnection. Calls disconnect handlers and cleans things up. """
126         self.connected=''
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()
132
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.')
138
139     def event(self,eventName,args={}):
140         """ Default event handler. To be overriden. """
141         print "Event: ",(eventName,args)
142
143     def isConnected(self):
144         """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """
145         return self.connected
146
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()
152         self._route=0
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
163
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)
171         if not connected: 
172             sock.PlugOut()
173             return
174         self._Server,self._Proxy=server,proxy
175         self.connected='tcp'
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)
179                 self.connected='ssl'
180             except socket.sslerror:
181                 return
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
188
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
207         self.connected='tls'
208         return self.connected
209
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'
222                 return 'old_auth'
223             return
224         self.SASL.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'
231                 return 'sasl'
232         else:
233             if self.__dict__.has_key('SASL'): self.SASL.PlugOut()
234
235     def getRoster(self):
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()
240
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)
245
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))
251
252 class Component(CommonClient):
253     """ Component class. The only difference from CommonClient is ability to perform component authentication. """
254     def __init__(self,transport,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             'transport' argument is a transport name that you are going to serve (f.e. "irc.localhost").
260             'port' can be specified if 'transport' resolves to correct IP. If it is not then you'll have to specify IP
261             and port while calling "connect()".
262             If you are going to serve several different domains with single Component instance - you must list them ALL
263             in the 'domains' argument.
264             For jabberd2 servers you should set typ='jabberd2' argument.
265         """
266         CommonClient.__init__(self,transport,port=port,debug=debug)
267         self.typ=typ
268         self.sasl=sasl
269         self.bind=bind
270         self.route=route
271         self.xcp=xcp
272         if domains:
273             self.domains=domains
274         else:
275             self.domains=[transport]
276     
277     def connect(self,server=None,proxy=None):
278         """ This will connect to the server, and if the features tag is found then set
279             the namespace to be jabber:client as that is required for jabberd2.
280             'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """
281         if self.sasl:
282             self.Namespace=auth.NS_COMPONENT_1
283             self.Server=server[0]
284         CommonClient.connect(self,server=server,proxy=proxy)
285         if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp):
286             self.defaultNamespace=auth.NS_CLIENT
287             self.Dispatcher.RegisterNamespace(self.defaultNamespace)
288             self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq)
289             self.Dispatcher.RegisterProtocol('message',dispatcher.Message)
290             self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence)
291         return self.connected
292
293     def dobind(self, sasl):
294         # This has to be done before binding, because we can receive a route stanza before binding finishes
295         self._route = self.route
296         if self.bind:
297             for domain in self.domains:
298                 auth.ComponentBind(sasl).PlugIn(self)
299                 while self.ComponentBind.bound is None: self.Process(1)
300                 if (not self.ComponentBind.Bind(domain)):
301                     self.ComponentBind.PlugOut()
302                     return
303                 self.ComponentBind.PlugOut()
304
305     def auth(self,name,password,dup=None):
306         """ Authenticate component "name" with password "password"."""
307         self._User,self._Password,self._Resource=name,password,''
308         try:
309             if self.sasl: auth.SASL(name,password).PlugIn(self)
310             if not self.sasl or self.SASL.startsasl=='not-supported':
311                 if auth.NonSASL(name,password,'').PlugIn(self):
312                     self.dobind(sasl=False)
313                     self.connected+='+old_auth'
314                     return 'old_auth'
315                 return
316             self.SASL.auth()
317             while self.SASL.startsasl=='in-process' and self.Process(1): pass
318             if self.SASL.startsasl=='success':
319                 self.dobind(sasl=True)
320                 self.connected+='+sasl'
321                 return 'sasl'
322             else:
323                 raise auth.NotAuthorized(self.SASL.startsasl)
324         except:
325             self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error')

UCC git Repository :: git.ucc.asn.au