Package xmpp :: Module transports
[hide private]
[frames] | no frames]

Source Code for Module xmpp.transports

  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$ 
 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 # Sort by priority, according to RFC 2782. 86 answers.sort(key=lambda a: a.priority) 87 if answers: 88 host = str(answers[0].target) 89 port = int(answers[0].port) 90 break 91 elif HAVE_PYDNS: 92 # ensure we haven't cached an old configuration 93 DNS.DiscoverNameServers() 94 response = DNS.Request().req(query, qtype='SRV') 95 # Sort by priority, according to RFC 2782. 96 answers = sorted(response.answers, key=lambda a: a['data'][0]) 97 if len(answers) > 0: 98 # ignore the priority and weight for now 99 _, _, port, host = answers[0]['data'] 100 del _ 101 port = int(port) 102 break 103 except: 104 self.DEBUG('An error occurred while looking up %s' % query, 'warn') 105 server = (host, port) 106 else: 107 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') 108 # end of SRV resolver 109 return server
110
111 - def plugin(self, owner):
112 """ Fire up connection. Return non-empty string on success. 113 Also registers self.disconnected method in the owner's dispatcher. 114 Called internally. """ 115 if not self._server: self._server=(self._owner.Server,5222) 116 if self.use_srv: server=self.srv_lookup(self._server) 117 else: server=self._server 118 if not self.connect(server): return 119 self._owner.Connection=self 120 self._owner.RegisterDisconnectHandler(self.disconnected) 121 return 'ok'
122
123 - def getHost(self):
124 """ Return the 'host' value that is connection is [will be] made to.""" 125 return self._server[0]
126 - def getPort(self):
127 """ Return the 'port' value that is connection is [will be] made to.""" 128 return self._server[1]
129
130 - def connect(self,server=None):
131 """ Try to connect to the given host/port. Does not lookup for SRV record. 132 Returns non-empty string on success. """ 133 if not server: server=self._server 134 try: 135 for res in socket.getaddrinfo(server[0], int(server[1]), 0, socket.SOCK_STREAM): 136 af, socktype, proto, canonname, sa = res 137 try: 138 self._sock = socket.socket(af, socktype, proto) 139 self._sock.connect(sa) 140 self._send=self._sock.sendall 141 self._recv=self._sock.recv 142 self.DEBUG("Successfully connected to remote host %s"%`server`,'start') 143 return 'ok' 144 except socket.error, (errno, strerror): 145 if self._sock is not None: self._sock.close() 146 self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error') 147 except socket.gaierror, (errno, strerror): 148 self.DEBUG("Failed to lookup remote host %s: %s (%s)"%(`server`, strerror, errno),'error')
149
150 - def plugout(self):
151 """ Disconnect from the remote server and unregister self.disconnected method from 152 the owner's dispatcher. """ 153 self._sock.close() 154 if self._owner.__dict__.has_key('Connection'): 155 del self._owner.Connection 156 self._owner.UnregisterDisconnectHandler(self.disconnected)
157
158 - def receive(self):
159 """ Reads all pending incoming data. 160 In case of disconnection calls owner's disconnected() method and then raises IOError exception.""" 161 try: received = self._recv(BUFLEN) 162 except socket.sslerror,e: 163 self._seen_data=0 164 if e[0]==socket.SSL_ERROR_WANT_READ: 165 sys.exc_clear() 166 self.DEBUG("SSL_WANT_READ while receiving data, asking for a retry",'warn') 167 return '' 168 if e[0]==socket.SSL_ERROR_WANT_WRITE: 169 sys.exc_clear() 170 self.DEBUG("SSL_WANT_WRITE while receiving data, asking for a retry",'warn') 171 return '' 172 self.DEBUG('Socket error while receiving data','error') 173 sys.exc_clear() 174 self._owner.disconnected() 175 raise IOError("Disconnected from server") 176 except: received = '' 177 178 while self.pending_data(0): 179 try: add = self._recv(BUFLEN) 180 except socket.sslerror,e: 181 self._seen_data=0 182 if e[0]==socket.SSL_ERROR_WANT_READ: 183 sys.exc_clear() 184 self.DEBUG("SSL_WANT_READ while receiving data, ignoring",'warn') 185 break 186 if e[0]==socket.SSL_ERROR_WANT_WRITE: 187 sys.exc_clear() 188 self.DEBUG("SSL_WANT_WRITE while receiving data, ignoring",'warn') 189 break 190 self.DEBUG('Socket error while receiving data','error') 191 sys.exc_clear() 192 self._owner.disconnected() 193 raise IOError("Disconnected from server") 194 except: add='' 195 received +=add 196 if not add: break 197 198 if len(received): # length of 0 means disconnect 199 self._seen_data=1 200 self.DEBUG(received,'got') 201 if hasattr(self._owner, 'Dispatcher'): 202 self._owner.Dispatcher.Event('', DATA_RECEIVED, received) 203 else: 204 self.DEBUG('Socket error while receiving data','error') 205 self._owner.disconnected() 206 raise IOError("Disconnected from server") 207 return received
208
209 - def send(self,raw_data,retry_timeout=1):
210 """ Writes raw outgoing data. Blocks until done. 211 If supplied data is unicode string, encodes it to utf-8 before send.""" 212 if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8') 213 elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8') 214 try: 215 sent = 0 216 while not sent: 217 try: 218 self._send(raw_data) 219 sent = 1 220 except socket.sslerror, e: 221 if e[0]==socket.SSL_ERROR_WANT_READ: 222 sys.exc_clear() 223 self.DEBUG("SSL_WANT_READ while sending data, wating to retry",'warn') 224 select.select([self._sock],[],[],retry_timeout) 225 continue 226 if e[0]==socket.SSL_ERROR_WANT_WRITE: 227 sys.exc_clear() 228 self.DEBUG("SSL_WANT_WRITE while sending data, waiting to retry",'warn') 229 select.select([],[self._sock],[],retry_timeout) 230 continue 231 raise 232 # Avoid printing messages that are empty keepalive packets. 233 if raw_data.strip(): 234 self.DEBUG(raw_data,'sent') 235 if hasattr(self._owner, 'Dispatcher'): # HTTPPROXYsocket will send data before we have a Dispatcher 236 self._owner.Dispatcher.Event('', DATA_SENT, raw_data) 237 except: 238 self.DEBUG("Socket error while sending data",'error') 239 self._owner.disconnected()
240
241 - def pending_data(self,timeout=0):
242 """ Returns true if there is a data ready to be read. """ 243 return select.select([self._sock],[],[],timeout)[0]
244
245 - def disconnect(self):
246 """ Closes the socket. """ 247 self.DEBUG("Closing socket",'stop') 248 self._sock.close()
249
250 - def disconnected(self):
251 """ Called when a Network Error or disconnection occurs. 252 Designed to be overidden. """ 253 self.DEBUG("Socket operation failed",'error')
254 255 DBG_CONNECT_PROXY='CONNECTproxy'
256 -class HTTPPROXYsocket(TCPsocket):
257 """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class 258 redefines only connect method. Allows to use HTTP proxies like squid with 259 (optionally) simple authentication (using login and password). """
260 - def __init__(self,proxy,server,use_srv=True):
261 """ Caches proxy and target addresses. 262 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) 263 and optional keys 'user' and 'password' to use for authentication. 264 'server' argument is a tuple of host and port - just like TCPsocket uses. """ 265 TCPsocket.__init__(self,server,use_srv) 266 self.DBG_LINE=DBG_CONNECT_PROXY 267 self._proxy=proxy
268
269 - def plugin(self, owner):
270 """ Starts connection. Used interally. Returns non-empty string on success.""" 271 owner.debug_flags.append(DBG_CONNECT_PROXY) 272 return TCPsocket.plugin(self,owner)
273
274 - def connect(self,dupe=None):
275 """ Starts connection. Connects to proxy, supplies login and password to it 276 (if were specified while creating instance). Instructs proxy to make 277 connection to the target server. Returns non-empty sting on success. """ 278 if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return 279 self.DEBUG("Proxy server contacted, performing authentification",'start') 280 connector = ['CONNECT %s:%s HTTP/1.0'%self._server, 281 'Proxy-Connection: Keep-Alive', 282 'Pragma: no-cache', 283 'Host: %s:%s'%self._server, 284 'User-Agent: HTTPPROXYsocket/v0.1'] 285 if self._proxy.has_key('user') and self._proxy.has_key('password'): 286 credentials = '%s:%s'%(self._proxy['user'],self._proxy['password']) 287 credentials = base64.encodestring(credentials).strip() 288 connector.append('Proxy-Authorization: Basic '+credentials) 289 connector.append('\r\n') 290 self.send('\r\n'.join(connector)) 291 try: reply = self.receive().replace('\r','') 292 except IOError: 293 self.DEBUG('Proxy suddenly disconnected','error') 294 self._owner.disconnected() 295 return 296 try: proto,code,desc=reply.split('\n')[0].split(' ',2) 297 except: raise error('Invalid proxy reply') 298 if code<>'200': 299 self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error') 300 self._owner.disconnected() 301 return 302 while reply.find('\n\n') == -1: 303 try: reply += self.receive().replace('\r','') 304 except IOError: 305 self.DEBUG('Proxy suddenly disconnected','error') 306 self._owner.disconnected() 307 return 308 self.DEBUG("Authentification successfull. Jabber server contacted.",'ok') 309 return 'ok'
310
311 - def DEBUG(self,text,severity):
312 """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".""" 313 return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity)
314
315 -class TLS(PlugIn):
316 """ TLS connection used to encrypts already estabilished tcp connection."""
317 - def PlugIn(self,owner,now=0):
318 """ If the 'now' argument is true then starts using encryption immidiatedly. 319 If 'now' in false then starts encryption as soon as TLS feature is 320 declared by the server (if it were already declared - it is ok). 321 """ 322 if owner.__dict__.has_key('TLS'): return # Already enabled. 323 PlugIn.PlugIn(self,owner) 324 DBG_LINE='TLS' 325 if now: return self._startSSL() 326 if self._owner.Dispatcher.Stream.features: 327 try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) 328 except NodeProcessed: pass 329 else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS) 330 self.starttls=None
331
332 - def plugout(self,now=0):
333 """ Unregisters TLS handler's from owner's dispatcher. Take note that encription 334 can not be stopped once started. You can only break the connection and start over.""" 335 self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) 336 self._owner.UnregisterHandler('proceed',self.StartTLSHandler,xmlns=NS_TLS) 337 self._owner.UnregisterHandler('failure',self.StartTLSHandler,xmlns=NS_TLS)
338
339 - def FeaturesHandler(self, conn, feats):
340 """ Used to analyse server <features/> tag for TLS support. 341 If TLS is supported starts the encryption negotiation. Used internally""" 342 if not feats.getTag('starttls',namespace=NS_TLS): 343 self.DEBUG("TLS unsupported by remote server.",'warn') 344 return 345 self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok') 346 self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS) 347 self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS) 348 self._owner.Connection.send('<starttls xmlns="%s"/>'%NS_TLS) 349 raise NodeProcessed
350
351 - def pending_data(self,timeout=0):
352 """ Returns true if there possible is a data ready to be read. """ 353 return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0]
354
355 - def _startSSL(self):
356 """ Immidiatedly switch socket to TLS mode. Used internally.""" 357 """ Here we should switch pending_data to hint mode.""" 358 tcpsock=self._owner.Connection 359 tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) 360 tcpsock._sslIssuer = tcpsock._sslObj.issuer() 361 tcpsock._sslServer = tcpsock._sslObj.server() 362 tcpsock._recv = tcpsock._sslObj.read 363 tcpsock._send = tcpsock._sslObj.write 364 365 tcpsock._seen_data=1 366 self._tcpsock=tcpsock 367 tcpsock.pending_data=self.pending_data 368 tcpsock._sock.setblocking(0) 369 370 self.starttls='success'
371
372 - def StartTLSHandler(self, conn, starttls):
373 """ Handle server reply if TLS is allowed to process. Behaves accordingly. 374 Used internally.""" 375 if starttls.getNamespace()<>NS_TLS: return 376 self.starttls=starttls.getName() 377 if self.starttls=='failure': 378 self.DEBUG("Got starttls response: "+self.starttls,'error') 379 return 380 self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok') 381 self._startSSL() 382 self._owner.Dispatcher.PlugOut() 383 dispatcher.Dispatcher().PlugIn(self._owner)
384