initscript: added sample init.d script
[uccdoor.git] / xmpp / transports.py
1 ##   transports.py
2 ##
3 ##   Copyright (C) 2003-2004 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: transports.py,v 1.35 2009/04/07 08:34:09 snakeru Exp $
16
17 """
18 This module contains the low-level implementations of xmpppy connect methods or
19 (in other words) transports for xmpp-stanzas.
20 Currently here is three transports:
21 direct TCP connect - TCPsocket class
22 proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies)
23 TLS connection - TLS class. Can be used for SSL connections also.
24
25 Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport.
26
27 Also exception 'error' is defined to allow capture of this module specific exceptions.
28 """
29
30 import socket,select,base64,dispatcher,sys
31 from simplexml import ustr
32 from client import PlugIn
33 from protocol import *
34
35 # determine which DNS resolution library is available
36 HAVE_DNSPYTHON = False
37 HAVE_PYDNS = False
38 try:
39     import dns.resolver # http://dnspython.org/
40     HAVE_DNSPYTHON = True
41 except ImportError:
42     try:
43         import DNS # http://pydns.sf.net/
44         HAVE_PYDNS = True
45     except ImportError:
46         pass
47
48 DATA_RECEIVED='DATA RECEIVED'
49 DATA_SENT='DATA SENT'
50
51 class error:
52     """An exception to be raised in case of low-level errors in methods of 'transports' module."""
53     def __init__(self,comment):
54         """Cache the descriptive string"""
55         self._comment=comment
56
57     def __str__(self):
58         """Serialise exception into pre-cached descriptive string."""
59         return self._comment
60
61 BUFLEN=1024
62 class TCPsocket(PlugIn):
63     """ This class defines direct TCP connection method. """
64     def __init__(self, server=None, use_srv=True):
65         """ Cache connection point 'server'. 'server' is the tuple of (host, port)
66             absolutely the same as standard tcp socket uses. However library will lookup for 
67             ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is)
68             server instead
69         """
70         PlugIn.__init__(self)
71         self.DBG_LINE='socket'
72         self._exported_methods=[self.send,self.disconnect]
73         self._server, self.use_srv = server, use_srv
74
75     def srv_lookup(self, server):
76         " SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair "
77         if HAVE_DNSPYTHON or HAVE_PYDNS:
78             host, port = server
79             possible_queries = ['_xmpp-client._tcp.' + host]
80
81             for query in possible_queries:
82                 try:
83                     if HAVE_DNSPYTHON:
84                         answers = [x for x in dns.resolver.query(query, 'SRV')]
85                         if answers:
86                             host = str(answers[0].target)
87                             port = int(answers[0].port)
88                             break
89                     elif HAVE_PYDNS:
90                         # ensure we haven't cached an old configuration
91                         DNS.DiscoverNameServers()
92                         response = DNS.Request().req(query, qtype='SRV')
93                         answers = response.answers
94                         if len(answers) > 0:
95                             # ignore the priority and weight for now
96                             _, _, port, host = answers[0]['data']
97                             del _
98                             port = int(port)
99                             break
100                 except:
101                     self.DEBUG('An error occurred while looking up %s' % query, 'warn')
102             server = (host, port)
103         else:
104             self.DEBUG("Could not load one of the supported DNS libraries (dnspython or pydns). SRV records will not be queried and you may need to set custom hostname/port for some servers to be accessible.\n",'warn')
105         # end of SRV resolver
106         return server
107
108     def plugin(self, owner):
109         """ Fire up connection. Return non-empty string on success.
110             Also registers self.disconnected method in the owner's dispatcher.
111             Called internally. """
112         if not self._server: self._server=(self._owner.Server,5222)
113         if self.use_srv: server=self.srv_lookup(self._server)
114         else: server=self._server
115         if not self.connect(server): return
116         self._owner.Connection=self
117         self._owner.RegisterDisconnectHandler(self.disconnected)
118         return 'ok'
119
120     def getHost(self):
121         """ Return the 'host' value that is connection is [will be] made to."""
122         return self._server[0]
123     def getPort(self):
124         """ Return the 'port' value that is connection is [will be] made to."""
125         return self._server[1]
126
127     def connect(self,server=None):
128         """ Try to connect to the given host/port. Does not lookup for SRV record.
129             Returns non-empty string on success. """
130         try:
131             if not server: server=self._server
132             self._sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
133             self._sock.connect((server[0], int(server[1])))
134             self._send=self._sock.sendall
135             self._recv=self._sock.recv
136             self.DEBUG("Successfully connected to remote host %s"%`server`,'start')
137             return 'ok'
138         except socket.error, (errno, strerror): 
139             self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error')
140         except: pass
141
142     def plugout(self):
143         """ Disconnect from the remote server and unregister self.disconnected method from
144             the owner's dispatcher. """
145         self._sock.close()
146         if self._owner.__dict__.has_key('Connection'):
147             del self._owner.Connection
148             self._owner.UnregisterDisconnectHandler(self.disconnected)
149
150     def receive(self):
151         """ Reads all pending incoming data.
152             In case of disconnection calls owner's disconnected() method and then raises IOError exception."""
153         try: received = self._recv(BUFLEN)
154         except socket.sslerror,e:
155             self._seen_data=0
156             if e[0]==socket.SSL_ERROR_WANT_READ: return ''
157             if e[0]==socket.SSL_ERROR_WANT_WRITE: return ''
158             self.DEBUG('Socket error while receiving data','error')
159             sys.exc_clear()
160             self._owner.disconnected()
161             raise IOError("Disconnected from server")
162         except: received = ''
163
164         while self.pending_data(0):
165             try: add = self._recv(BUFLEN)
166             except: add=''
167             received +=add
168             if not add: break
169
170         if len(received): # length of 0 means disconnect
171             self._seen_data=1
172             self.DEBUG(received,'got')
173             if hasattr(self._owner, 'Dispatcher'):
174                 self._owner.Dispatcher.Event('', DATA_RECEIVED, received)
175         else:
176             self.DEBUG('Socket error while receiving data','error')
177             self._owner.disconnected()
178             raise IOError("Disconnected from server")
179         return received
180
181     def send(self,raw_data):
182         """ Writes raw outgoing data. Blocks until done.
183             If supplied data is unicode string, encodes it to utf-8 before send."""
184         if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8')
185         elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8')
186         try:
187             self._send(raw_data)
188             # Avoid printing messages that are empty keepalive packets.
189             if raw_data.strip():
190                 self.DEBUG(raw_data,'sent')
191                 if hasattr(self._owner, 'Dispatcher'): # HTTPPROXYsocket will send data before we have a Dispatcher
192                     self._owner.Dispatcher.Event('', DATA_SENT, raw_data)
193         except:
194             self.DEBUG("Socket error while sending data",'error')
195             self._owner.disconnected()
196
197     def pending_data(self,timeout=0):
198         """ Returns true if there is a data ready to be read. """
199         return select.select([self._sock],[],[],timeout)[0]
200
201     def disconnect(self):
202         """ Closes the socket. """
203         self.DEBUG("Closing socket",'stop')
204         self._sock.close()
205
206     def disconnected(self):
207         """ Called when a Network Error or disconnection occurs.
208             Designed to be overidden. """
209         self.DEBUG("Socket operation failed",'error')
210
211 DBG_CONNECT_PROXY='CONNECTproxy'
212 class HTTPPROXYsocket(TCPsocket):
213     """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class
214         redefines only connect method. Allows to use HTTP proxies like squid with
215         (optionally) simple authentication (using login and password). """
216     def __init__(self,proxy,server,use_srv=True):
217         """ Caches proxy and target addresses.
218             'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address)
219             and optional keys 'user' and 'password' to use for authentication.
220             'server' argument is a tuple of host and port - just like TCPsocket uses. """
221         TCPsocket.__init__(self,server,use_srv)
222         self.DBG_LINE=DBG_CONNECT_PROXY
223         self._proxy=proxy
224
225     def plugin(self, owner):
226         """ Starts connection. Used interally. Returns non-empty string on success."""
227         owner.debug_flags.append(DBG_CONNECT_PROXY)
228         return TCPsocket.plugin(self,owner)
229
230     def connect(self,dupe=None):
231         """ Starts connection. Connects to proxy, supplies login and password to it
232             (if were specified while creating instance). Instructs proxy to make
233             connection to the target server. Returns non-empty sting on success. """
234         if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return
235         self.DEBUG("Proxy server contacted, performing authentification",'start')
236         connector = ['CONNECT %s:%s HTTP/1.0'%self._server,
237             'Proxy-Connection: Keep-Alive',
238             'Pragma: no-cache',
239             'Host: %s:%s'%self._server,
240             'User-Agent: HTTPPROXYsocket/v0.1']
241         if self._proxy.has_key('user') and self._proxy.has_key('password'):
242             credentials = '%s:%s'%(self._proxy['user'],self._proxy['password'])
243             credentials = base64.encodestring(credentials).strip()
244             connector.append('Proxy-Authorization: Basic '+credentials)
245         connector.append('\r\n')
246         self.send('\r\n'.join(connector))
247         try: reply = self.receive().replace('\r','')
248         except IOError:
249             self.DEBUG('Proxy suddenly disconnected','error')
250             self._owner.disconnected()
251             return
252         try: proto,code,desc=reply.split('\n')[0].split(' ',2)
253         except: raise error('Invalid proxy reply')
254         if code<>'200':
255             self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error')
256             self._owner.disconnected()
257             return
258         while reply.find('\n\n') == -1:
259             try: reply += self.receive().replace('\r','')
260             except IOError:
261                 self.DEBUG('Proxy suddenly disconnected','error')
262                 self._owner.disconnected()
263                 return
264         self.DEBUG("Authentification successfull. Jabber server contacted.",'ok')
265         return 'ok'
266
267     def DEBUG(self,text,severity):
268         """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy"."""
269         return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity)
270
271 class TLS(PlugIn):
272     """ TLS connection used to encrypts already estabilished tcp connection."""
273     def PlugIn(self,owner,now=0):
274         """ If the 'now' argument is true then starts using encryption immidiatedly.
275             If 'now' in false then starts encryption as soon as TLS feature is
276             declared by the server (if it were already declared - it is ok).
277         """
278         if owner.__dict__.has_key('TLS'): return  # Already enabled.
279         PlugIn.PlugIn(self,owner)
280         DBG_LINE='TLS'
281         if now: return self._startSSL()
282         if self._owner.Dispatcher.Stream.features:
283             try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features)
284             except NodeProcessed: pass
285         else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS)
286         self.starttls=None
287
288     def plugout(self,now=0):
289         """ Unregisters TLS handler's from owner's dispatcher. Take note that encription
290             can not be stopped once started. You can only break the connection and start over."""
291         self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
292         self._owner.UnregisterHandler('proceed',self.StartTLSHandler,xmlns=NS_TLS)
293         self._owner.UnregisterHandler('failure',self.StartTLSHandler,xmlns=NS_TLS)
294
295     def FeaturesHandler(self, conn, feats):
296         """ Used to analyse server <features/> tag for TLS support.
297             If TLS is supported starts the encryption negotiation. Used internally"""
298         if not feats.getTag('starttls',namespace=NS_TLS):
299             self.DEBUG("TLS unsupported by remote server.",'warn')
300             return
301         self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok')
302         self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS)
303         self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS)
304         self._owner.Connection.send('<starttls xmlns="%s"/>'%NS_TLS)
305         raise NodeProcessed
306
307     def pending_data(self,timeout=0):
308         """ Returns true if there possible is a data ready to be read. """
309         return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0]
310
311     def _startSSL(self):
312         """ Immidiatedly switch socket to TLS mode. Used internally."""
313         """ Here we should switch pending_data to hint mode."""
314         tcpsock=self._owner.Connection
315         tcpsock._sslObj    = socket.ssl(tcpsock._sock, None, None)
316         tcpsock._sslIssuer = tcpsock._sslObj.issuer()
317         tcpsock._sslServer = tcpsock._sslObj.server()
318         tcpsock._recv = tcpsock._sslObj.read
319         tcpsock._send = tcpsock._sslObj.write
320
321         tcpsock._seen_data=1
322         self._tcpsock=tcpsock
323         tcpsock.pending_data=self.pending_data
324         tcpsock._sock.setblocking(0)
325
326         self.starttls='success'
327
328     def StartTLSHandler(self, conn, starttls):
329         """ Handle server reply if TLS is allowed to process. Behaves accordingly.
330             Used internally."""
331         if starttls.getNamespace()<>NS_TLS: return
332         self.starttls=starttls.getName()
333         if self.starttls=='failure':
334             self.DEBUG("Got starttls response: "+self.starttls,'error')
335             return
336         self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok')
337         self._startSSL()
338         self._owner.Dispatcher.PlugOut()
339         dispatcher.Dispatcher().PlugIn(self._owner)

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