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

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