root/branches/gajim_0.11.1/src/common/connection_handlers.py

Revision 8740, 69.4 kB (checked in by asterix, 15 months ago)

fix indentation

Line 
1##
2## Copyright (C) 2006 Gajim Team
3##
4## Contributors for this file:
5##      - Yann Le Boulanger <asterix@lagaule.org>
6##      - Nikos Kouremenos <kourem@gmail.com>
7##      - Dimitur Kirov <dkirov@gmail.com>
8##      - Travis Shirk <travis@pobox.com>
9##
10## This program is free software; you can redistribute it and/or modify
11## it under the terms of the GNU General Public License as published
12## by the Free Software Foundation; version 2 only.
13##
14## This program is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17## GNU General Public License for more details.
18##
19
20import os
21import base64
22import sha
23import socket
24import sys
25
26from time import (altzone, daylight, gmtime, localtime, mktime, strftime,
27        time as time_time, timezone, tzname)
28from calendar import timegm
29
30import socks5
31import common.xmpp
32
33from common import GnuPG
34from common import helpers
35from common import gajim
36from common import atom
37from common import exceptions
38from common.commands import ConnectionCommands
39from common.pubsub import ConnectionPubSub
40
41STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
42        'invisible']
43# kind of events we can wait for an answer
44VCARD_PUBLISHED = 'vcard_published'
45VCARD_ARRIVED = 'vcard_arrived'
46AGENT_REMOVED = 'agent_removed'
47METACONTACTS_ARRIVED = 'metacontacts_arrived'
48PRIVACY_ARRIVED = 'privacy_arrived'
49HAS_IDLE = True
50try:
51        import idle
52except:
53        gajim.log.debug(_('Unable to load idle module'))
54        HAS_IDLE = False
55
56class ConnectionBytestream:
57        def __init__(self):
58                self.files_props = {}
59       
60        def is_transfer_stoped(self, file_props):
61                if file_props.has_key('error') and file_props['error'] != 0:
62                        return True
63                if file_props.has_key('completed') and file_props['completed']:
64                        return True
65                if file_props.has_key('connected') and file_props['connected'] == False:
66                        return True
67                if not file_props.has_key('stopped') or not file_props['stopped']:
68                        return False
69                return True
70       
71        def send_success_connect_reply(self, streamhost):
72                ''' send reply to the initiator of FT that we
73                made a connection
74                '''
75                if streamhost is None:
76                        return None
77                iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result',
78                        frm = streamhost['target'])
79                iq.setAttr('id', streamhost['id'])
80                query = iq.setTag('query')
81                query.setNamespace(common.xmpp.NS_BYTESTREAM)
82                stream_tag = query.setTag('streamhost-used')
83                stream_tag.setAttr('jid', streamhost['jid'])
84                self.connection.send(iq)
85       
86        def remove_transfers_for_contact(self, contact):
87                ''' stop all active transfer for contact '''
88                for file_props in self.files_props.values():
89                        if self.is_transfer_stoped(file_props):
90                                continue
91                        receiver_jid = unicode(file_props['receiver']).split('/')[0]
92                        if contact.jid == receiver_jid:
93                                file_props['error'] = -5
94                                self.remove_transfer(file_props)
95                                self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props, ''))
96                        sender_jid = unicode(file_props['sender']).split('/')[0]
97                        if contact.jid == sender_jid:
98                                file_props['error'] = -3
99                                self.remove_transfer(file_props)
100       
101        def remove_all_transfers(self):
102                ''' stops and removes all active connections from the socks5 pool '''
103                for file_props in self.files_props.values():
104                        self.remove_transfer(file_props, remove_from_list = False)
105                del(self.files_props)
106                self.files_props = {}
107       
108        def remove_transfer(self, file_props, remove_from_list = True):
109                if file_props is None:
110                        return
111                self.disconnect_transfer(file_props)
112                sid = file_props['sid']
113                gajim.socks5queue.remove_file_props(self.name, sid)
114
115                if remove_from_list:
116                        if self.files_props.has_key('sid'):
117                                del(self.files_props['sid'])
118       
119        def disconnect_transfer(self, file_props):
120                if file_props is None:
121                        return
122                if file_props.has_key('hash'):
123                        gajim.socks5queue.remove_sender(file_props['hash'])
124
125                if file_props.has_key('streamhosts'):
126                        for host in file_props['streamhosts']:
127                                if host.has_key('idx') and host['idx'] > 0:
128                                        gajim.socks5queue.remove_receiver(host['idx'])
129                                        gajim.socks5queue.remove_sender(host['idx'])
130       
131        def send_socks5_info(self, file_props, fast = True, receiver = None,
132                sender = None):
133                ''' send iq for the present streamhosts and proxies '''
134                if type(self.peerhost) != tuple:
135                        return
136                port = gajim.config.get('file_transfers_port')
137                ft_override_host_to_send = gajim.config.get('ft_override_host_to_send')
138                cfg_proxies = gajim.config.get_per('accounts', self.name,
139                        'file_transfer_proxies')
140                if receiver is None:
141                        receiver = file_props['receiver']
142                if sender is None:
143                        sender = file_props['sender']
144                proxyhosts = []
145                if fast and cfg_proxies:
146                        proxies = map(lambda e:e.strip(), cfg_proxies.split(','))
147                        default = gajim.proxy65_manager.get_default_for_name(self.name)
148                        if default:
149                                # add/move default proxy at top of the others
150                                if proxies.__contains__(default):
151                                        proxies.remove(default)
152                                proxies.insert(0, default)
153                       
154                        for proxy in proxies:
155                                (host, _port, jid) = gajim.proxy65_manager.get_proxy(proxy, self.name)
156                                if host is None:
157                                        continue
158                                host_dict={
159                                        'state': 0,
160                                        'target': unicode(receiver),
161                                        'id': file_props['sid'],
162                                        'sid': file_props['sid'],
163                                        'initiator': proxy,
164                                        'host': host,
165                                        'port': unicode(_port),
166                                        'jid': jid
167                                }
168                                proxyhosts.append(host_dict)
169                sha_str = helpers.get_auth_sha(file_props['sid'], sender,
170                        receiver)
171                file_props['sha_str'] = sha_str
172                if not ft_override_host_to_send:
173                        ft_override_host_to_send = self.peerhost[0]
174                try:
175                        ft_override_host_to_send = socket.gethostbyname(
176                                ft_override_host_to_send)
177                except socket.gaierror:
178                        self.dispatch('ERROR', (_('Wrong host'), _('The host you configured as the ft_override_host_to_send advanced option is not valid, so ignored.')))
179                        ft_override_host_to_send = self.peerhost[0]
180                listener = gajim.socks5queue.start_listener(port,
181                        sha_str, self._result_socks5_sid, file_props['sid'])
182                if listener == None:
183                        file_props['error'] = -5
184                        self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props,
185                                ''))
186                        self._connect_error(unicode(receiver), file_props['sid'],
187                                file_props['sid'], code = 406)
188                        return
189
190                iq = common.xmpp.Protocol(name = 'iq', to = unicode(receiver),
191                        typ = 'set')
192                file_props['request-id'] = 'id_' + file_props['sid']
193                iq.setID(file_props['request-id'])
194                query = iq.setTag('query')
195                query.setNamespace(common.xmpp.NS_BYTESTREAM)
196                query.setAttr('mode', 'tcp')
197                query.setAttr('sid', file_props['sid'])
198                streamhost = query.setTag('streamhost')
199                streamhost.setAttr('port', unicode(port))
200                streamhost.setAttr('host', ft_override_host_to_send)
201                streamhost.setAttr('jid', sender)
202                if fast and proxyhosts != [] and gajim.config.get_per('accounts',
203                self.name, 'use_ft_proxies'):
204                        file_props['proxy_receiver'] = unicode(receiver)
205                        file_props['proxy_sender'] = unicode(sender)
206                        file_props['proxyhosts'] = proxyhosts
207                        for proxyhost in proxyhosts:
208                                streamhost = common.xmpp.Node(tag = 'streamhost')
209                                query.addChild(node=streamhost)
210                                streamhost.setAttr('port', proxyhost['port'])
211                                streamhost.setAttr('host', proxyhost['host'])
212                                streamhost.setAttr('jid', proxyhost['jid'])
213
214                                # don't add the proxy child tag for streamhosts, which are proxies
215                                # proxy = streamhost.setTag('proxy')
216                                # proxy.setNamespace(common.xmpp.NS_STREAM)
217                self.connection.send(iq)
218
219        def send_file_rejection(self, file_props):
220                ''' informs sender that we refuse to download the file '''
221                # user response to ConfirmationDialog may come after we've disconneted
222                if not self.connection or self.connected < 2:
223                        return
224                iq = common.xmpp.Protocol(name = 'iq',
225                        to = unicode(file_props['sender']), typ = 'error')
226                iq.setAttr('id', file_props['request-id'])
227                err = common.xmpp.ErrorNode(code = '403', typ = 'cancel', name =
228                        'forbidden', text = 'Offer Declined')
229                iq.addChild(node=err)
230                self.connection.send(iq)
231
232        def send_file_approval(self, file_props):
233                ''' send iq, confirming that we want to download the file '''
234                # user response to ConfirmationDialog may come after we've disconneted
235                if not self.connection or self.connected < 2:
236                        return
237                iq = common.xmpp.Protocol(name = 'iq',
238                        to = unicode(file_props['sender']), typ = 'result')
239                iq.setAttr('id', file_props['request-id'])
240                si = iq.setTag('si')
241                si.setNamespace(common.xmpp.NS_SI)
242                if file_props.has_key('offset') and file_props['offset']:
243                        file_tag = si.setTag('file')
244                        file_tag.setNamespace(common.xmpp.NS_FILE)
245                        range_tag = file_tag.setTag('range')
246                        range_tag.setAttr('offset', file_props['offset'])
247                feature = si.setTag('feature')
248                feature.setNamespace(common.xmpp.NS_FEATURE)
249                _feature = common.xmpp.DataForm(typ='submit')
250                feature.addChild(node=_feature)
251                field = _feature.setField('stream-method')
252                field.delAttr('type')
253                field.setValue(common.xmpp.NS_BYTESTREAM)
254                self.connection.send(iq)
255
256        def send_file_request(self, file_props):
257                ''' send iq for new FT request '''
258                if not self.connection or self.connected < 2:
259                        return
260                our_jid = gajim.get_jid_from_account(self.name)
261                resource = self.server_resource
262                frm = our_jid + '/' + resource
263                file_props['sender'] = frm
264                fjid = file_props['receiver'].jid + '/' + file_props['receiver'].resource
265                iq = common.xmpp.Protocol(name = 'iq', to = fjid,
266                        typ = 'set')
267                iq.setID(file_props['sid'])
268                self.files_props[file_props['sid']] = file_props
269                si = iq.setTag('si')
270                si.setNamespace(common.xmpp.NS_SI)
271                si.setAttr('profile', common.xmpp.NS_FILE)
272                si.setAttr('id', file_props['sid'])
273                file_tag = si.setTag('file')
274                file_tag.setNamespace(common.xmpp.NS_FILE)
275                file_tag.setAttr('name', file_props['name'])
276                file_tag.setAttr('size', file_props['size'])
277                desc = file_tag.setTag('desc')
278                if file_props.has_key('desc'):
279                        desc.setData(file_props['desc'])
280                file_tag.setTag('range')
281                feature = si.setTag('feature')
282                feature.setNamespace(common.xmpp.NS_FEATURE)
283                _feature = common.xmpp.DataForm(typ='form')
284                feature.addChild(node=_feature)
285                field = _feature.setField('stream-method')
286                field.setAttr('type', 'list-single')
287                field.addOption(common.xmpp.NS_BYTESTREAM)
288                self.connection.send(iq)
289       
290        def _result_socks5_sid(self, sid, hash_id):
291                ''' store the result of sha message from auth. '''
292                if not self.files_props.has_key(sid):
293                        return
294                file_props = self.files_props[sid]
295                file_props['hash'] = hash_id
296                return
297       
298        def _connect_error(self, to, _id, sid, code = 404):
299                ''' cb, when there is an error establishing BS connection, or
300                when connection is rejected'''
301                msg_dict = {
302                        404: 'Could not connect to given hosts',
303                        405: 'Cancel',
304                        406: 'Not acceptable',
305                }
306                msg = msg_dict[code]
307                iq = None
308                iq = common.xmpp.Protocol(name = 'iq', to = to,
309                        typ = 'error')
310                iq.setAttr('id', _id)
311                err = iq.setTag('error')
312                err.setAttr('code', unicode(code))
313                err.setData(msg)
314                self.connection.send(iq)
315                if code == 404:
316                        file_props = gajim.socks5queue.get_file_props(self.name, sid)
317                        if file_props is not None:
318                                self.disconnect_transfer(file_props)
319                                file_props['error'] = -3
320                                self.dispatch('FILE_REQUEST_ERROR', (to, file_props, msg))
321
322        def _proxy_auth_ok(self, proxy):
323                '''cb, called after authentication to proxy server '''
324                file_props = self.files_props[proxy['sid']]
325                iq = common.xmpp.Protocol(name = 'iq', to = proxy['initiator'],
326                typ = 'set')
327                auth_id = "au_" + proxy['sid']
328                iq.setID(auth_id)
329                query = iq.setTag('query')
330                query.setNamespace(common.xmpp.NS_BYTESTREAM)
331                query.setAttr('sid', proxy['sid'])
332                activate = query.setTag('activate')
333                activate.setData(file_props['proxy_receiver'])
334                iq.setID(auth_id)
335                self.connection.send(iq)
336       
337        # register xmpppy handlers for bytestream and FT stanzas
338        def _bytestreamErrorCB(self, con, iq_obj):
339                gajim.log.debug('_bytestreamErrorCB')
340                id = unicode(iq_obj.getAttr('id'))
341                frm = helpers.get_full_jid_from_iq(iq_obj)
342                query = iq_obj.getTag('query')
343                gajim.proxy65_manager.error_cb(frm, query)
344                jid = helpers.get_jid_from_iq(iq_obj)
345                id = id[3:]
346                if not self.files_props.has_key(id):
347                        return
348                file_props = self.files_props[id]
349                file_props['error'] = -4
350                self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, ''))
351                raise common.xmpp.NodeProcessed
352       
353        def _bytestreamSetCB(self, con, iq_obj):
354                gajim.log.debug('_bytestreamSetCB')
355                target = unicode(iq_obj.getAttr('to'))
356                id = unicode(iq_obj.getAttr('id'))
357                query = iq_obj.getTag('query')
358                sid = unicode(query.getAttr('sid'))
359                file_props = gajim.socks5queue.get_file_props(
360                        self.name, sid)
361                streamhosts=[]
362                for item in query.getChildren():
363                        if item.getName() == 'streamhost':
364                                host_dict={
365                                        'state': 0,
366                                        'target': target,
367                                        'id': id,
368                                        'sid': sid,
369                                        'initiator': helpers.get_full_jid_from_iq(iq_obj)
370                                }
371                                for attr in item.getAttrs():
372                                        host_dict[attr] = item.getAttr(attr)
373                                streamhosts.append(host_dict)
374                if file_props is None:
375                        if self.files_props.has_key(sid):
376                                file_props = self.files_props[sid]
377                                file_props['fast'] = streamhosts
378                                if file_props['type'] == 's': # FIXME: remove fast xmlns
379                                        # only psi do this
380
381                                        if file_props.has_key('streamhosts'):
382                                                file_props['streamhosts'].extend(streamhosts)
383                                        else:
384                                                file_props['streamhosts'] = streamhosts
385                                        if not gajim.socks5queue.get_file_props(self.name, sid):
386                                                gajim.socks5queue.add_file_props(self.name, file_props)
387                                        gajim.socks5queue.connect_to_hosts(self.name, sid,
388                                                self.send_success_connect_reply, None)
389                                raise common.xmpp.NodeProcessed
390
391                file_props['streamhosts'] = streamhosts
392                if file_props['type'] == 'r':
393                        gajim.socks5queue.connect_to_hosts(self.name, sid,
394                                self.send_success_connect_reply, self._connect_error)
395                raise common.xmpp.NodeProcessed
396
397        def _ResultCB(self, con, iq_obj):
398                gajim.log.debug('_ResultCB')
399                # if we want to respect xep-0065 we have to check for proxy
400                # activation result in any result iq
401                real_id = unicode(iq_obj.getAttr('id'))
402                if real_id[:3] != 'au_':
403                        return
404                frm = helpers.get_full_jid_from_iq(iq_obj)
405                id = real_id[3:]
406                if self.files_props.has_key(id):
407                        file_props = self.files_props[id]
408                        if file_props['streamhost-used']:
409                                for host in file_props['proxyhosts']:
410                                        if host['initiator'] == frm and host.has_key('idx'):
411                                                gajim.socks5queue.activate_proxy(host['idx'])
412                                                raise common.xmpp.NodeProcessed
413       
414        def _bytestreamResultCB(self, con, iq_obj):
415                gajim.log.debug('_bytestreamResultCB')
416                frm = helpers.get_full_jid_from_iq(iq_obj)
417                real_id = unicode(iq_obj.getAttr('id'))
418                query = iq_obj.getTag('query')
419                gajim.proxy65_manager.resolve_result(frm, query)
420               
421                try:
422                        streamhost = query.getTag('streamhost-used')
423                except: # this bytestream result is not what we need
424                        pass
425                id = real_id[3:]
426                if self.files_props.has_key(id):
427                        file_props = self.files_props[id]
428                else:
429                        raise common.xmpp.NodeProcessed
430                if streamhost is None:
431                        # proxy approves the activate query
432                        if real_id[:3] == 'au_':
433                                id = real_id[3:]
434                                if not file_props.has_key('streamhost-used') or \
435                                        file_props['streamhost-used'] is False:
436                                        raise common.xmpp.NodeProcessed
437                                if not file_props.has_key('proxyhosts'):
438                                        raise common.xmpp.NodeProcessed
439                                for host in file_props['proxyhosts']:
440                                        if host['initiator'] == frm and \
441                                        unicode(query.getAttr('sid')) == file_props['sid']:
442                                                gajim.socks5queue.activate_proxy(host['idx'])
443                                                break
444                        raise common.xmpp.NodeProcessed
445                jid = streamhost.getAttr('jid')
446                if file_props.has_key('streamhost-used') and \
447                        file_props['streamhost-used'] is True:
448                        raise common.xmpp.NodeProcessed
449
450                if real_id[:3] == 'au_':
451                        gajim.socks5queue.send_file(file_props, self.name)
452                        raise common.xmpp.NodeProcessed
453
454                proxy = None
455                if file_props.has_key('proxyhosts'):
456                        for proxyhost in file_props['proxyhosts']:
457                                if proxyhost['jid'] == jid:
458                                        proxy = proxyhost
459
460                if proxy != None:
461                        file_props['streamhost-used'] = True
462                        if not file_props.has_key('streamhosts'):
463                                file_props['streamhosts'] = []
464                        file_props['streamhosts'].append(proxy)
465                        file_props['is_a_proxy'] = True
466                        receiver = socks5.Socks5Receiver(gajim.idlequeue, proxy, file_props['sid'], file_props)
467                        gajim.socks5queue.add_receiver(self.name, receiver)
468                        proxy['idx'] = receiver.queue_idx
469                        gajim.socks5queue.on_success = self._proxy_auth_ok
470                        raise common.xmpp.NodeProcessed
471
472                else:
473                        gajim.socks5queue.send_file(file_props, self.name)
474                        if file_props.has_key('fast'):
475                                fasts = file_props['fast']
476                                if len(fasts) > 0:
477                                        self._connect_error(frm, fasts[0]['id'], file_props['sid'],
478                                                code = 406)
479               
480                raise common.xmpp.NodeProcessed
481       
482        def _siResultCB(self, con, iq_obj):
483                gajim.log.debug('_siResultCB')
484                id = iq_obj.getAttr('id')
485                if not self.files_props.has_key(id):
486                        # no such jid
487                        return
488                file_props = self.files_props[id]
489                if file_props is None:
490                        # file properties for jid is none
491                        return
492                if file_props.has_key('request-id'):
493                        # we have already sent streamhosts info
494                        return
495                file_props['receiver'] = helpers.get_full_jid_from_iq(iq_obj)
496                si = iq_obj.getTag('si')
497                file_tag = si.getTag('file')
498                range_tag = None
499                if file_tag:
500                        range_tag = file_tag.getTag('range')
501                if range_tag:
502                        offset = range_tag.getAttr('offset')
503                        if offset:
504                                file_props['offset'] = int(offset)
505                        length = range_tag.getAttr('length')
506                        if length:
507                                file_props['length'] = int(length)
508                feature = si.setTag('feature')
509                if feature.getNamespace() != common.xmpp.NS_FEATURE:
510                        return
511                form_tag = feature.getTag('x')
512                form = common.xmpp.DataForm(node=form_tag)
513                field = form.getField('stream-method')
514                if field.getValue() != common.xmpp.NS_BYTESTREAM:
515                        return
516                self.send_socks5_info(file_props, fast = True)
517                raise common.xmpp.NodeProcessed
518       
519        def _siSetCB(self, con, iq_obj):
520                gajim.log.debug('_siSetCB')
521                jid = helpers.get_jid_from_iq(iq_obj)
522                si = iq_obj.getTag('si')
523                profile = si.getAttr('profile')
524                mime_type = si.getAttr('mime-type')
525                if profile != common.xmpp.NS_FILE:
526                        return
527                file_tag = si.getTag('file')
528                file_props = {'type': 'r'}
529                for attribute in file_tag.getAttrs():
530                        if attribute in ('name', 'size', 'hash', 'date'):
531                                val = file_tag.getAttr(attribute)
532                                if val is None:
533                                        continue
534                                file_props[attribute] = val
535                file_desc_tag = file_tag.getTag('desc')
536                if file_desc_tag is not None:
537                        file_props['desc'] = file_desc_tag.getData()
538
539                if mime_type is not None:
540                        file_props['mime-type'] = mime_type
541                our_jid = gajim.get_jid_from_account(self.name)
542                resource = self.server_resource
543                file_props['receiver'] = our_jid + '/' + resource
544                file_props['sender'] = helpers.get_full_jid_from_iq(iq_obj)
545                file_props['request-id'] = unicode(iq_obj.getAttr('id'))
546                file_props['sid'] = unicode(si.getAttr('id'))
547                gajim.socks5queue.add_file_props(self.name, file_props)
548                self.dispatch('FILE_REQUEST', (jid, file_props))
549                raise common.xmpp.NodeProcessed
550
551        def _siErrorCB(self, con, iq_obj):
552                gajim.log.debug('_siErrorCB')
553                si = iq_obj.getTag('si')
554                profile = si