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