1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
36 HAVE_DNSPYTHON = False
37 HAVE_PYDNS = False
38 try:
39 import dns.resolver
40 HAVE_DNSPYTHON = True
41 except ImportError:
42 try:
43 import DNS
44 HAVE_PYDNS = True
45 except ImportError:
46 pass
47
48 DATA_RECEIVED='DATA RECEIVED'
49 DATA_SENT='DATA SENT'
50
52 """An exception to be raised in case of low-level errors in methods of 'transports' module."""
54 """Cache the descriptive string"""
55 self._comment=comment
56
58 """Serialise exception into pre-cached descriptive string."""
59 return self._comment
60
61 BUFLEN=1024
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
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
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
93 DNS.DiscoverNameServers()
94 response = DNS.Request().req(query, qtype='SRV')
95
96 answers = sorted(response.answers, key=lambda a: a['data'][0])
97 if len(answers) > 0:
98
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
109 return server
110
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
124 """ Return the 'host' value that is connection is [will be] made to."""
125 return self._server[0]
127 """ Return the 'port' value that is connection is [will be] made to."""
128 return self._server[1]
129
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
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
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):
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
233 if raw_data.strip():
234 self.DEBUG(raw_data,'sent')
235 if hasattr(self._owner, '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
242 """ Returns true if there is a data ready to be read. """
243 return select.select([self._sock],[],[],timeout)[0]
244
246 """ Closes the socket. """
247 self.DEBUG("Closing socket",'stop')
248 self._sock.close()
249
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'
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
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
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
316 """ TLS connection used to encrypts already estabilished tcp connection."""
317 - def PlugIn(self,owner,now=0):
331
338
350
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
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
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