3 ## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
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)
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.
15 # $Id: transports.py,v 1.35 2009/04/07 08:34:09 snakeru Exp $
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.
25 Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport.
27 Also exception 'error' is defined to allow capture of this module specific exceptions.
30 import socket,select,base64,dispatcher,sys
31 from simplexml import ustr
32 from client import PlugIn
33 from protocol import *
35 # determine which DNS resolution library is available
36 HAVE_DNSPYTHON = False
39 import dns.resolver # http://dnspython.org/
43 import DNS # http://pydns.sf.net/
48 DATA_RECEIVED='DATA RECEIVED'
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"""
58 """Serialise exception into pre-cached descriptive string."""
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)
71 self.DBG_LINE='socket'
72 self._exported_methods=[self.send,self.disconnect]
73 self._server, self.use_srv = server, use_srv
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:
79 possible_queries = ['_xmpp-client._tcp.' + host]
81 for query in possible_queries:
84 answers = [x for x in dns.resolver.query(query, 'SRV')]
86 host = str(answers[0].target)
87 port = int(answers[0].port)
90 # ensure we haven't cached an old configuration
91 DNS.DiscoverNameServers()
92 response = DNS.Request().req(query, qtype='SRV')
93 answers = response.answers
95 # ignore the priority and weight for now
96 _, _, port, host = answers[0]['data']
101 self.DEBUG('An error occurred while looking up %s' % query, 'warn')
102 server = (host, port)
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
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)
121 """ Return the 'host' value that is connection is [will be] made to."""
122 return self._server[0]
124 """ Return the 'port' value that is connection is [will be] made to."""
125 return self._server[1]
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. """
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')
138 except socket.error, (errno, strerror):
139 self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error')
143 """ Disconnect from the remote server and unregister self.disconnected method from
144 the owner's dispatcher. """
146 if self._owner.__dict__.has_key('Connection'):
147 del self._owner.Connection
148 self._owner.UnregisterDisconnectHandler(self.disconnected)
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:
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')
160 self._owner.disconnected()
161 raise IOError("Disconnected from server")
162 except: received = ''
164 while self.pending_data(0):
165 try: add = self._recv(BUFLEN)
170 if len(received): # length of 0 means disconnect
172 self.DEBUG(received,'got')
173 if hasattr(self._owner, 'Dispatcher'):
174 self._owner.Dispatcher.Event('', DATA_RECEIVED, received)
176 self.DEBUG('Socket error while receiving data','error')
177 self._owner.disconnected()
178 raise IOError("Disconnected from server")
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')
188 # Avoid printing messages that are empty keepalive packets.
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)
194 self.DEBUG("Socket error while sending data",'error')
195 self._owner.disconnected()
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]
201 def disconnect(self):
202 """ Closes the socket. """
203 self.DEBUG("Closing socket",'stop')
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')
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
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)
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',
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','')
249 self.DEBUG('Proxy suddenly disconnected','error')
250 self._owner.disconnected()
252 try: proto,code,desc=reply.split('\n')[0].split(' ',2)
253 except: raise error('Invalid proxy reply')
255 self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error')
256 self._owner.disconnected()
258 while reply.find('\n\n') == -1:
259 try: reply += self.receive().replace('\r','')
261 self.DEBUG('Proxy suddenly disconnected','error')
262 self._owner.disconnected()
264 self.DEBUG("Authentification successfull. Jabber server contacted.",'ok')
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)
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).
278 if owner.__dict__.has_key('TLS'): return # Already enabled.
279 PlugIn.PlugIn(self,owner)
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)
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)
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')
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)
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]
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
322 self._tcpsock=tcpsock
323 tcpsock.pending_data=self.pending_data
324 tcpsock._sock.setblocking(0)
326 self.starttls='success'
328 def StartTLSHandler(self, conn, starttls):
329 """ Handle server reply if TLS is allowed to process. Behaves accordingly.
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')
336 self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok')
338 self._owner.Dispatcher.PlugOut()
339 dispatcher.Dispatcher().PlugIn(self._owner)