Newer
Older
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
## Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2005-2007 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from errno import EWOULDBLOCK
from errno import ENOBUFS
from errno import EINTR
# after foo seconds without activity label transfer as 'stalled'
STALLED_TIMEOUT = 10
# after foo seconds of waiting to connect, disconnect from
# streamhost and try next one
CONNECT_TIMEOUT = 30
# nothing received for the last foo seconds - stop transfer
# if it is 0, then transfer will wait forever
READ_TIMEOUT = 180
# nothing sent for the last foo seconds - stop transfer
# if it is 0, then transfer will wait forever
SEND_TIMEOUT = 180
"""
Queue for all file requests objects
"""

Yann Leboulanger
committed
def __init__(self, idlequeue, complete_transfer_cb=None,
progress_transfer_cb=None, error_cb=None):
self.connected = 0
self.readers = {}
self.files_props = {}
# handle all io events in the global idle queue, instead of processing
# each foo seconds
self.idlequeue = idlequeue
self.complete_transfer_cb = complete_transfer_cb
self.progress_transfer_cb = progress_transfer_cb

Yann Leboulanger
committed
self.error_cb = error_cb
self.on_success = None
self.on_failure = None
def start_listener(self, port, sha_str, sha_handler, sid):
"""
Start waiting for incomming connections on (host, port) and do a socks5
authentication using sid for generated SHA
"""
if self.listener is None:

Yann Leboulanger
committed
# We cannot bind port, call error callback and fail
self.error_cb(_('Unable to bind to port %s.') % port,
_('Maybe you have another running instance of Gajim. File '
'Transfer will be cancelled.'))
def send_success_reply(self, file_props, streamhost):
if 'streamhost-used' in file_props and \
file_props['streamhost-used'] is True:
if 'proxyhosts' in file_props:
for proxy in file_props['proxyhosts']:
if proxy == streamhost:
self.on_success(streamhost)
return 2
return 0
if 'streamhosts' in file_props:
streamhost['state'] = 1
self.on_success(streamhost)
def connect_to_hosts(self, account, sid, on_success=None, on_failure=None):
self.on_success = on_success
self.on_failure = on_failure
file_props = self.files_props[account][sid]
file_props['failure_cb'] = on_failure
for streamhost in file_props['streamhosts']:
receiver = Socks5Receiver(self.idlequeue, streamhost, sid, file_props)
self.add_receiver(account, receiver)
streamhost['idx'] = receiver.queue_idx
def _socket_connected(self, streamhost, file_props):
"""
Called when there is a host connected to one of the senders's
streamhosts. Stop othere attempts for connections
"""
for host in file_props['streamhosts']:
if host != streamhost and 'idx' in host:
self.remove_receiver(streamhost['idx'])
return
# set state -2, meaning that this streamhost is stopped,
# but it may be connectected later
self.remove_receiver(host['idx'])
host['idx'] = -1
host['state'] = -2
def reconnect_receiver(self, receiver, streamhost):
"""
Check the state of all streamhosts and if all has failed, then emit
connection failure cb. If there are some which are still not connected
try to establish connection to one of them
"""
self.idlequeue.remove_timeout(receiver.fd)
self.idlequeue.unplug_idle(receiver.fd)
file_props = receiver.file_props
streamhost['state'] = -1
# boolean, indicates that there are hosts, which are not tested yet
unused_hosts = False
for host in file_props['streamhosts']:
if 'idx' in host:
if host['state'] >= 0:
return
elif host['state'] == -2:
unused_hosts = True
if unused_hosts:
for host in file_props['streamhosts']:
if host['state'] == -2:
host['state'] = 0
receiver = Socks5Receiver(self.idlequeue, host, host['sid'],
file_props)
self.add_receiver(receiver.account, receiver)
host['idx'] = receiver.queue_idx
# we still have chances to connect
return
if 'received-len' not in file_props or file_props['received-len'] == 0:
# there are no other streamhosts and transfer hasn't started
self._connection_refused(streamhost, file_props, receiver.queue_idx)
else:
# transfer stopped, it is most likely stopped from sender
receiver.disconnect()
file_props['error'] = -1
self.process_result(-1, receiver)
def _connection_refused(self, streamhost, file_props, idx):
"""
Called when we loose connection during transfer
"""
if file_props is None:
return
streamhost['state'] = -1
if 'streamhosts' in file_props:
for host in file_props['streamhosts']:
if host['state'] != -1:
return
# failure_cb exists - this means that it has never been called
if 'failure_cb' in file_props and file_props['failure_cb']:
file_props['failure_cb'](streamhost['initiator'], streamhost['id'],
def add_receiver(self, account, sock5_receiver):
"""
Add new file request
"""
self.readers[self.idx] = sock5_receiver
sock5_receiver.queue_idx = self.idx
sock5_receiver.queue = self
if result is not None:
result = sock5_receiver.main()
self.process_result(result, sock5_receiver)
return 1
return None
def get_file_from_sender(self, file_props, account):
if 'hash' in file_props and file_props['hash'] in self.senders:
sender = self.senders[file_props['hash']]
sender.account = account
result = self.get_file_contents(0)
self.process_result(result, sender)
def result_sha(self, sha_str, idx):
if sha_str in self.sha_handlers:
props = self.sha_handlers[sha_str]
props[0](props[1], idx)
if idx not in self.readers:
return
reader = self.readers[idx]
if reader.file_props['type'] != 's':
return
if reader.state != 5:
return
reader.state = 6
if reader.connected:
reader.file_props['error'] = 0
reader.file_props['disconnect_cb'] = reader.disconnect
reader.file_props['started'] = True
reader.file_props['completed'] = False
reader.file_props['paused'] = False
reader.file_props['stalled'] = False
reader.file_props['last-time'] = self.idlequeue.current_time()
reader.file_props['received-len'] = 0
reader.pauses = 0
self.idlequeue.set_read_timeout(reader.fd, STALLED_TIMEOUT)
self.idlequeue.plug_idle(reader, True, False)
result = reader.write_next()
self.process_result(result, reader)
def send_file(self, file_props, account):
if 'hash' in file_props and file_props['hash'] in self.senders:
sender = self.senders[file_props['hash']]
sender.account = account
if file_props['type'] == 's':
result = sender.send_file()
self.process_result(result, sender)
else:
file_props['last-time'] = self.idlequeue.current_time()
file_props['received-len'] = 0
sender.file_props = file_props
def add_file_props(self, account, file_props):
"""
File_prop to the dict of current file_props. It is identified by account
name and sid
"""
if file_props is None or ('sid' in file_props) is False:
if account not in self.files_props:
self.files_props[account][_id] = file_props
if account in self.files_props:
if sid in fl_props:
if len(self.files_props) == 0:
self.connected = 0
def get_file_props(self, account, sid):
"""
Get fil_prop by account name and session id
"""
if account in self.files_props:
fl_props = self.files_props[account]
if sid in fl_props:
if sock_hash not in self.senders:
self.senders[sock_hash] = Socks5Sender(self.idlequeue, sock_hash, self,
sock[0], sock[1][0], sock[1][1])
"""
Take appropriate actions upon the result:
[ 0, - 1 ] complete/end transfer
[ > 0 ] send progress message
[ None ] do nothing
"""
if account is None and 'tt_account' in actor.file_props:
def remove_receiver(self, idx, do_disconnect=True):
"""
Remove reciver from the list and decrease the number of active
connections with 1
"""
if idx in self.readers:
reader = self.readers[idx]
self.idlequeue.unplug_idle(reader.fd)
self.idlequeue.remove_timeout(reader.fd)
def remove_sender(self, idx, do_disconnect=True):
"""
Remove sender from the list of senders and decrease the number of active
connections with 1
"""
if idx in self.senders:
if do_disconnect:
self.senders[idx].disconnect()
else:
del(self.senders[idx])
if self.connected > 0:
self.connected -= 1
if len(self.senders) == 0 and self.listener is not None:
self.listener.disconnect()
self.listener = None
self.connected -= 1
def __init__(self, idlequeue, host, port, initiator, target, sid):
self.ais = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
self.idlequeue = idlequeue
self.fd = -1
self.port = port
self.initiator = initiator
self.target = target
self.sid = sid
self._sock = None
self.pauses = 0
self.size = 0
self.remaining_buff = ''
if self.file is None:
self.file = open(self.file_props['file-name'],'rb')
if 'offset' in self.file_props and self.file_props['offset']:
self.size = self.file_props['offset']
self.file_props['received-len'] = self.size
except IOError, e:
self.close_file()
raise IOError, e
if self.file:
if not self.file.closed:
try:
self.file.close()
"""
Test if file is already open and return its fd, or just open the file and
return the fd
"""
if 'fd' in self.file_props:
fd = self.file_props['fd']
else:
offset = 0
opt = 'wb'
if 'offset' in self.file_props and self.file_props['offset']:
offset = self.file_props['offset']
opt = 'ab'
fd = open(self.file_props['file-name'], opt)
self.file_props['last-time'] = self.idlequeue.current_time()
self.file_props['received-len'] = offset
if 'fd' in self.file_props:
"""
Read small chunks of data. Call owner's disconnected() method if
appropriate
"""
"""
Write raw outgoing data
"""
self._send(raw_data)
except Exception:
if self.remaining_buff != '':
buff = self.remaining_buff
self.remaining_buff = ''
else:
try:
self.open_file_for_reading()
except IOError, e:
self.state = 8 # end connection
self.disconnect()
self.file_props['error'] = -7 # unable to read from file
return -1
if len(buff) > 0:
lenn = 0
try:
lenn = self._send(buff)
except Exception, e:
if e.args[0] not in (EINTR, ENOBUFS, EWOULDBLOCK):
self.disconnect()
self.file_props['error'] = -1
return -1
self.size += lenn
current_time = self.idlequeue.current_time()
self.file_props['elapsed-time'] += current_time - \
self.file_props['last-time']
self.file_props['last-time'] = current_time
self.file_props['received-len'] = self.size
if self.size >= int(self.file_props['size']):
self.file_props['error'] = 0
self.disconnect()
return -1
if lenn != len(buff):
self.remaining_buff = buff[lenn:]
else:
self.remaining_buff = ''
self.state = 7 # continue to write in the socket
def get_file_contents(self, timeout):
"""
Read file contents from socket and write them to file
"""
if self.file_props is None or ('file-name' in self.file_props) is False:
self.file_props['error'] = -2
return None
fd = None
if self.remaining_buff != '':
try:
fd = self.get_fd()
except IOError, e:
self.disconnect(False)
self.file_props['error'] = -6 # file system error
return 0
fd.write(self.remaining_buff)
lenn = len(self.remaining_buff)
current_time = self.idlequeue.current_time()
self.file_props['elapsed-time'] += current_time - \
self.file_props['last-time']
self.file_props['last-time'] = current_time
self.file_props['received-len'] += lenn
self.remaining_buff = ''
if self.file_props['received-len'] == int(self.file_props['size']):
self.rem_fd(fd)
self.disconnect()
self.file_props['error'] = 0
self.file_props['completed'] = True
return 0
else:
try:
fd = self.get_fd()
except IOError, e:
self.disconnect(False)
self.file_props['error'] = -6 # file system error
return 0
current_time = self.idlequeue.current_time()
self.file_props['elapsed-time'] += current_time - \
self.file_props['last-time']
self.file_props['last-time'] = current_time
self.file_props['received-len'] += len(buff)
if len(buff) == 0:
# Transfer stopped somehow:
# reset, paused or network error
self.rem_fd(fd)
self.disconnect(False)
self.file_props['error'] = -1
return 0
try:
fd.write(buff)
except IOError, e:
self.rem_fd(fd)
self.disconnect(False)
self.file_props['error'] = -6 # file system error
return 0
if self.file_props['received-len'] >= int(self.file_props['size']):
# transfer completed
self.rem_fd(fd)
self.disconnect()
self.file_props['error'] = 0
self.file_props['completed'] = True
return 0
# return number of read bytes. It can be used in progressbar
if fd is not None:
self.file_props['stalled'] = False
if fd is None and self.file_props['stalled'] is False:
if 'received-len' in self.file_props:
if self.file_props['received-len'] != 0:
return self.file_props['received-len']
return None
"""
Close open descriptors and remover socket descr. from idleque
"""
# be sure that we don't leave open file
self.close_file()
self.idlequeue.remove_timeout(self.fd)
self.idlequeue.unplug_idle(self.fd)
# socket is already closed
pass
self.connected = False
self.fd = -1
"""
Message, that we support 1 one auth mechanism: the 'no auth' mechanism
"""
"""
Parse the initial message and create a list of auth mechanisms
"""
num_auth = struct.unpack('!xB', buff[:2])[0]
for i in xrange(num_auth):
mechanism, = struct.unpack('!B', buff[1 + i])
auth_mechanisms.append(mechanism)
"""
Socks version(5), number of extra auth methods (we send 0x00 - no auth)
"""
buff = struct.pack('!BBBBB%dsBB' % len(self.host),
0x05, 0x01, 0x00, 0x03, len(self.host), self.host,
"""
Connect request by domain name, sid sha, instead of domain name (jep
0096)
"""
try: # don't trust on what comes from the outside
req_type, host_type, = struct.unpack('!xBxB', buff[:4])
if host_type == 0x01:
host_arr = struct.unpack('!iiii', buff[4:8])
host, = '.'.join(str(s) for s in host_arr)
host_len = len(host)
elif host_type == 0x03:
host_len, = struct.unpack('!B' , buff[4])
host, = struct.unpack('!%ds' % host_len, buff[5:5 + host_len])
portlen = len(buff[host_len + 5:])
port, = struct.unpack('!B', buff[host_len + 5])
port, = struct.unpack('!H', buff[host_len + 5:])
# file data, comes with auth message (Gaim bug)
port, = struct.unpack('!H', buff[host_len + 5: host_len + 7])
self.remaining_buff = buff[host_len + 7:]
"""
Connect response: version, auth method
"""
try:
version, method = struct.unpack('!BB', buff)
def continue_paused_transfer(self):
if self.state < 5:
return
if self.file_props['type'] == 'r':
self.idlequeue.plug_idle(self, False, True)
else:
self.idlequeue.plug_idle(self, True, False)
"""
Get sha of sid + Initiator jid + Target jid
"""
if 'is_a_proxy' in self.file_props:
return hashlib.sha1('%s%s%s' % (self.sid,
self.file_props['proxy_sender'],
self.file_props['proxy_receiver'])).hexdigest()
return hashlib.sha1('%s%s%s' % (self.sid, self.initiator, self.target)).\
"""
Class for sending file to socket over socks5
"""
def __init__(self, idlequeue, sock_hash, parent, _sock, host=None,
Socks5.__init__(self, idlequeue, host, port, None, None, None)
self._recv = _sock.recv
self._send = _sock.send
self.connected = True
self.state = 1 # waiting for first bytes
self.file_props = None
# start waiting for data
self.idlequeue.plug_idle(self, False, True)
def read_timeout(self):
self.idlequeue.remove_timeout(self.fd)
if self.state > 5:
# no activity for foo seconds
if self.file_props['stalled'] == False:
self.file_props['stalled'] = True
self.queue.process_result(-1, self)
if SEND_TIMEOUT > 0:
self.idlequeue.set_read_timeout(self.fd, SEND_TIMEOUT)
else:
def pollout(self):
if not self.connected:
if self.state == 2: # send reply with desired auth type
self.send_raw(self._get_auth_response())
elif self.state == 4: # send positive response to the 'connect'
self.send_raw(self._get_request_buff(self.sha_msg, 0x00))
elif self.state == 7:
if self.file_props['paused']:
self.file_props['continue_cb'] = self.continue_paused_transfer
self.idlequeue.plug_idle(self, False, False)
result = self.write_next()
self.queue.process_result(result, self)
if result is None or result <= 0:
return
self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT)
return
else:
self.disconnect()
if self.state < 5:
self.state += 1
# unplug and plug this time for reading
self.idlequeue.plug_idle(self, False, True)
def pollend(self):
self.state = 8 # end connection
self.file_props['error'] = -1
self.queue.process_result(-1, self)
def pollin(self):
if self.connected:
if self.state < 5:
result = self.main()
if self.state == 4:
self.queue.result_sha(self.sha_msg, self.queue_idx)
if result == -1:
self.disconnect()
if self.file_props is not None and self.file_props['type'] == 'r':
result = self.get_file_contents(0)
self.queue.process_result(result, self)
else:
"""
Start sending the file over verified connection
"""
self.file_props['error'] = 0
self.file_props['disconnect_cb'] = self.disconnect
self.file_props['started'] = True
self.file_props['completed'] = False
self.file_props['paused'] = False
self.file_props['continue_cb'] = self.continue_paused_transfer
self.file_props['stalled'] = False
self.file_props['connected'] = True
self.file_props['last-time'] = self.idlequeue.current_time()
self.file_props['received-len'] = 0
# plug for writing
self.idlequeue.plug_idle(self, True, False)
return self.write_next() # initial for nl byte
"""
Initial requests for verifying the connection
"""
buff = self.receive()
if not self.connected:
return -1
mechs = self._parse_auth_buff(buff)
if mechs is None:
return -1 # invalid auth methods received
elif self.state == 3: # get next request
req_type, self.sha_msg = self._parse_request_buff(buff)[:2]
if req_type != 0x01:
return -1 # request is not of type 'connect'
self.state += 1 # go to the next step
# unplug & plug for writing
self.idlequeue.plug_idle(self, True, False)
"""
Close the socket
"""
self.file_props['connected'] = False
self.file_props['disconnect_cb'] = None
"""
Handle all incomming connections on (0.0.0.0, port)
This class implements IdleObject, but we will expect
only pollin events though
self.ais = socket.getaddrinfo(None, port, socket.AF_UNSPEC,
socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_PASSIVE)
self.ais.sort(reverse=True) # Try IPv6 first

Alex V. Myltsev
committed
# try the different possibilities (ipv6, ipv4, etc.)
try:
self._serv = socket.socket(*ai[:3])
except socket.error, e:
if e.errno == EAFNOSUPPORT:
self.ai = None
continue
raise
self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self._serv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# Under windows Vista, we need that to listen on ipv6 AND ipv4
# Doesn't work under windows XP
if os.name == 'nt':
ver = os.sys.getwindowsversion()
if (ver[3], ver[0], ver[1]) == (2, 6, 0):
# 27 is socket.IPV6_V6ONLY under windows, but not defined ...
self._serv.setsockopt(socket.IPPROTO_IPV6, 27, 1)
# will fail when port as busy, or we don't have rights to bind
try:
self._serv.bind(ai[4])
self.ai = ai
break
self.ai = None
continue
if not self.ai:
# unable to bind, show error dialog
self._serv.listen(socket.SOMAXCONN)
self._serv.setblocking(False)
self.fd = self._serv.fileno()
self.idlequeue.plug_idle(self, False, True)
"""
Called when we stop listening on (host, port)
"""
"""
Accept a new incomming connection and notify queue
"""
sock = self.accept_conn()
self.queue.on_connection_accepted(sock)
"""
Free all resources, we are not listening anymore
"""
self.idlequeue.unplug_idle(self.fd)
self.fd = -1
"""
Accept a new incomming connection
"""
class Socks5Receiver(Socks5, IdleObject):
def __init__(self, idlequeue, streamhost, sid, file_props = None):
self.connected = False
self.pauses = 0
if not self.file_props:
self.file_props = {}
self.file_props['disconnect_cb'] = self.disconnect
self.file_props['error'] = 0
self.file_props['started'] = True
self.file_props['completed'] = False
self.file_props['paused'] = False
self.file_props['continue_cb'] = self.continue_paused_transfer
Socks5.__init__(self, idlequeue, streamhost['host'],
int(streamhost['port']), streamhost['initiator'], streamhost['target'],
sid)
def read_timeout(self):
self.idlequeue.remove_timeout(self.fd)
if self.state > 5:
# no activity for foo seconds
if self.file_props['stalled'] == False:
self.file_props['stalled'] = True
if 'received-len' not in self.file_props:
self.file_props['received-len'] = 0
self.queue.process_result(-1, self)
if READ_TIMEOUT > 0:
self.idlequeue.set_read_timeout(self.fd, READ_TIMEOUT)
else:
self.queue.reconnect_receiver(self, self.streamhost)
"""
Create the socket and plug it to the idlequeue
"""
if self.ais is None:
for ai in self.ais:
try:
# this will not block the GUI
self._sock.setblocking(False)
except socket.error, e:
if not isinstance(e, basestring) and e[0] == EINPROGRESS:
# for all other errors, we try other addresses
self.state = 0 # about to be connected
self.idlequeue.plug_idle(self, True, False)
self.do_connect()
self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT)
def _is_connected(self):
if self.state < 5:
return False