Skip to content
Snippets Groups Projects
socks5.py 52.8 KiB
Newer Older
roidelapluie's avatar
roidelapluie committed
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
## src/common/socks5.py
dkirov's avatar
dkirov committed
##
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
##                         Nikos Kouremenos <kourem AT gmail.com>
Yann Leboulanger's avatar
Yann Leboulanger committed
## Copyright (C) 2005-2012 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
dkirov's avatar
dkirov committed
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
dkirov's avatar
dkirov committed
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
dkirov's avatar
dkirov committed
##
## Gajim is distributed in the hope that it will be useful,
dkirov's avatar
dkirov committed
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
dkirov's avatar
dkirov committed
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
dkirov's avatar
dkirov committed

import socket
import struct
import hashlib
Yann Leboulanger's avatar
Yann Leboulanger committed
import os
from errno import EWOULDBLOCK
from errno import ENOBUFS
from errno import EINTR
from errno import EISCONN
from errno import EINPROGRESS
from errno import EAFNOSUPPORT
from xmpp.idlequeue import IdleObject
zimio's avatar
zimio committed
from file_props import FilesProp
from common import gajim
import jingle_xtls
if jingle_xtls.PYOPENSSL_PRESENT:
    import OpenSSL
import logging
log = logging.getLogger('gajim.c.socks5')
MAX_BUFF_LEN = 65536
# 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
zimio's avatar
zimio committed

dkirov's avatar
dkirov committed
class SocksQueue:
    """
    Queue for all file requests objects
    """

    def __init__(self, idlequeue, complete_transfer_cb=None,
Yann Leboulanger's avatar
Yann Leboulanger committed
    progress_transfer_cb=None, error_cb=None):
        self.connected = 0
        self.readers = {}
        self.senders = {}
        self.idx = 1
        self.listener = None
        self.sha_handlers = {}
        # 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
        self.error_cb = error_cb
        self.on_success = {} # {id: cb}
        self.on_failure = {} # {id: cb}
Yann Leboulanger's avatar
Yann Leboulanger committed
    def start_listener(self, port, sha_str, sha_handler, file_props,
    fingerprint=None, typ='sender'):
        """
        Start waiting for incomming connections on (host, port) and do a socks5
        authentication using sid for generated SHA
        """
zimio's avatar
zimio committed
        sid = file_props.sid
Yann Leboulanger's avatar
Yann Leboulanger committed
        self.type_ = typ # It says whether we are sending or receiving
        self.sha_handlers[sha_str] = (sha_handler, sid)
        if self.listener is None or self.listener.connections == []:
zimio's avatar
zimio committed
            self.listener = Socks5Listener(self.idlequeue, port, file_props,
Yann Leboulanger's avatar
Yann Leboulanger committed
                fingerprint=fingerprint)
            self.listener.queue = self
            self.listener.bind()
        else:
            # There is already a listener, we update the file's information
            # on the new connection.
zimio's avatar
zimio committed
            self.listener.file_props = file_props
zimio's avatar
zimio committed
        self.connected += 1
        return self.listener

    def send_success_reply(self, file_props, streamhost):
zimio's avatar
zimio committed
        if file_props.streamhost_used == True:
            for proxy in file_props.proxyhosts:
                if proxy['host'] == streamhost['host']:
                    self.on_success[file_props.sid](proxy)
                    return 1
zimio's avatar
zimio committed
        for host in file_props.streamhosts:
            if streamhost['state'] == 1:
                return 0
        streamhost['state'] = 1
        self.on_success[file_props.sid](streamhost)
Yann Leboulanger's avatar
Yann Leboulanger committed
        return 1
    def connect_to_hosts(self, account, sid, on_success=None, on_failure=None,
    fingerprint=None, receiving=True):
        self.on_success[sid] = on_success
        self.on_failure[sid] = on_failure
Yann Leboulanger's avatar
Yann Leboulanger committed
        file_props = FilesProp.getFileProp(account, sid)
zimio's avatar
zimio committed
        file_props.failure_cb = on_failure
        con = gajim.connections[account]
zimio's avatar
zimio committed
        if not file_props.streamhosts:
            on_failure(file_props.sid)
        # add streamhosts to the queue
zimio's avatar
zimio committed
        for streamhost in file_props.streamhosts:
            if streamhost['host'] == '127.0.0.1' or \
                    streamhost['host'] == '::1' or \
                    streamhost['host'] == con.peerhost[0]:
                continue
            if 'type' in streamhost and streamhost['type'] == 'proxy':
                fp = None
            else:
Yann Leboulanger's avatar
Yann Leboulanger committed
                fp = fingerprint
            if receiving:
Yann Leboulanger's avatar
Yann Leboulanger committed
                self.type_ = 'receiver'
                socks5obj = Socks5ReceiverClient(self.idlequeue, streamhost,
                    sid, file_props, fingerprint=fp)
                self.add_sockobj(account, socks5obj)
Yann Leboulanger's avatar
Yann Leboulanger committed
            else:
zimio's avatar
zimio committed
                if file_props.sha_str:
                    idx = file_props.sha_str
zimio's avatar
zimio committed
                else:
                    idx = self.idx
                    self.idx = self.idx + 1
Yann Leboulanger's avatar
Yann Leboulanger committed
                self.type_ = 'sender'
                if 'type' in streamhost and streamhost['type'] == 'proxy':
zimio's avatar
zimio committed
                    file_props.is_a_proxy = True
                    file_props.proxy_sender = streamhost['target']
                    file_props.proxy_receiver = streamhost['initiator']
zimio's avatar
zimio committed
                socks5obj = Socks5SenderClient(self.idlequeue, idx,
Yann Leboulanger's avatar
Yann Leboulanger committed
                    self, _sock=None,host=str(streamhost['host']),
                    port=int(streamhost['port']),fingerprint=fp,
zimio's avatar
zimio committed
                    connected=False, file_props=file_props)
                socks5obj.streamhost = streamhost
Yann Leboulanger's avatar
Yann Leboulanger committed
                self.add_sockobj(account, socks5obj, type_='sender')
Yann Leboulanger's avatar
Yann Leboulanger committed

            streamhost['idx'] = socks5obj.queue_idx

    def _socket_connected(self, streamhost, file_props):
        """
        Called when there is a host connected to one of the senders's
        streamhosts. Stop other attempts for connections
zimio's avatar
zimio committed
        for host in file_props.streamhosts:
            if host != streamhost and 'idx' in host:
                if host['state'] == 1:
                    # remove current
Yann Leboulanger's avatar
Yann Leboulanger committed
                    if self.type_ == 'sender':
                        self.remove_sender(streamhost['idx'], False)
                    else:
                        self.remove_receiver(streamhost['idx'])
                    return
                # set state -2, meaning that this streamhost is stopped,
                # but it may be connectected later
                if host['state'] >= 0:
Yann Leboulanger's avatar
Yann Leboulanger committed
                    if self.type_ == 'sender':
                        self.remove_sender(host['idx'], False)
                    else:
                        self.remove_receiver(host['idx'])
                    host['idx'] = -1
                    host['state'] = -2

    def reconnect_client(self, client, 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(client.fd)
        self.idlequeue.unplug_idle(client.fd)
        file_props = client.file_props
        streamhost['state'] = -1
        # boolean, indicates that there are hosts, which are not tested yet
        unused_hosts = False
zimio's avatar
zimio committed
        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:
zimio's avatar
zimio committed
            for host in file_props.streamhosts:
                if host['state'] == -2:
                    host['state'] = 0
                    # FIXME: make the sender reconnect also
Yann Leboulanger's avatar
Yann Leboulanger committed
                    client = Socks5ReceiverClient(self.idlequeue, host,
                        host['sid'], file_props)
                    self.add_sockobj(client.account, client)
                    host['idx'] = client.queue_idx
            # we still have chances to connect
            return
zimio's avatar
zimio committed
        if file_props.received_len == 0:
            # there are no other streamhosts and transfer hasn't started
            self._connection_refused(streamhost, file_props, client.queue_idx)
        else:
            # transfer stopped, it is most likely stopped from sender
            client.disconnect()
zimio's avatar
zimio committed
            file_props.error = -1
            self.process_result(-1, client)

    def _connection_refused(self, streamhost, file_props, idx):
        """
        Called when we loose connection during transfer
        """
        if file_props is None:
            return
        streamhost['state'] = -1
zimio's avatar
zimio committed
        # FIXME: should only the receiver be remove? what if we are sending?
        self.remove_receiver(idx, False)
zimio's avatar
zimio committed
        for host in file_props.streamhosts:
            if host['state'] != -1:
                return
zimio's avatar
zimio committed
        self.readers = {}
        # failure_cb exists - this means that it has never been called
zimio's avatar
zimio committed
        if file_props.failure_cb:
            file_props.failure_cb(file_props.sid)
            file_props.failure_cb = None
Yann Leboulanger's avatar
Yann Leboulanger committed
    def add_sockobj(self, account, sockobj, type_='receiver'):
        Add new file a sockobj type receiver or sender, and use it to connect
        to server
Yann Leboulanger's avatar
Yann Leboulanger committed
        if type_ == 'receiver':
            self._add(sockobj, self.readers, sockobj.file_props, self.idx)
        else:
            self._add(sockobj, self.senders, sockobj.file_props, self.idx)
        sockobj.queue_idx = self.idx
        sockobj.queue = self
        sockobj.account = account
        result = sockobj.connect()
        self.connected += 1
        if result is not None:
            result = sockobj.main()
            self.process_result(result, sockobj)
zimio's avatar
zimio committed
    def _add(self, sockobj, sockobjects, file_props, hash_):
        '''
        Adds the sockobj to the current list of sockobjects
        '''
zimio's avatar
zimio committed
        keys = (file_props.sid, file_props.name, hash_)
Yann Leboulanger's avatar
Yann Leboulanger committed
        sockobjects[keys] = sockobj

    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)

    def activate_proxy(self, idx):
        if not self.isHashInSockObjs(self.senders, idx):
        for key in self.senders.keys():
            if idx in key:
                sender = self.senders[key]
                if sender.file_props.type_ != 's':
                sender.state = 6
                if sender.connected:
                    sender.file_props.error = 0
                    sender.file_props.disconnect_cb = sender.disconnect
                    sender.file_props.started = True
                    sender.file_props.completed = False
                    sender.file_props.paused = False
                    sender.file_props.stalled = False
                    sender.file_props.elapsed_time = 0
                    sender.file_props.last_time = self.idlequeue.current_time()
                    sender.file_props.received_len = 0
                    sender.pauses = 0
                    # start sending file to proxy
                    self.idlequeue.set_read_timeout(sender.fd, STALLED_TIMEOUT)
                    self.idlequeue.plug_idle(sender, True, False)
                    result = sender.write_next()
                    self.process_result(result, sender)
zimio's avatar
zimio committed
    def send_file(self, file_props, account, mode):
        for key in self.senders.keys():
Yann Leboulanger's avatar
Yann Leboulanger committed
            if self.senders == {}:
                # Python acts very weird with this. When there is no keys
                # in the dictionary It says that it has a key.
                # Maybe it is my machine. Without this there is a KeyError
                # traceback.
                return
            if file_props.name in key and file_props.sid in key \
zimio's avatar
zimio committed
            and self.senders[key].mode == mode:
Yann Leboulanger's avatar
Yann Leboulanger committed
                log.info('socks5: sending file')
                sender = self.senders[key]
zimio's avatar
zimio committed
                file_props.streamhost_used = True
                sender.account = account
zimio's avatar
zimio committed
                sender.file_props = file_props
                result = sender.send_file()
                self.process_result(result, sender)
    def isHashInSockObjs(self, sockobjs, hash):
        '''
        It tells wether there is a particular hash in sockobjs or not
        '''
        for key in sockobjs:
            if hash in key:
                return True
        return False
    def on_connection_accepted(self, sock, listener):
        sock_hash = sock.__hash__()
Yann Leboulanger's avatar
Yann Leboulanger committed
        if self.type_ == 'sender' and \
Yann Leboulanger's avatar
Yann Leboulanger committed
        not self.isHashInSockObjs(self.senders, sock_hash):
zimio's avatar
zimio committed
            sockobj =  Socks5SenderServer(self.idlequeue, sock_hash, self,
Yann Leboulanger's avatar
Yann Leboulanger committed
                sock[0],  sock[1][0], sock[1][1], fingerprint='server',
                file_props=listener.file_props)
            self._add(sockobj, self.senders, listener.file_props, sock_hash)
zimio's avatar
zimio committed
            # Start waiting for data
            self.idlequeue.plug_idle(sockobj, False, True)
Yann Leboulanger's avatar
Yann Leboulanger committed
        if self.type_ == 'receiver' and \
        not self.isHashInSockObjs(self.readers, sock_hash):
            sh = {}
            sh['host'] = sock[1][0]
            sh['port'] = sock[1][1]
            sh['initiator'] = None
            sh['target'] = None
zimio's avatar
zimio committed
            sockobj =  Socks5ReceiverServer(idlequeue=self.idlequeue,
Yann Leboulanger's avatar
Yann Leboulanger committed
                streamhost=sh,sid=None, file_props=listener.file_props,
zimio's avatar
zimio committed
                fingerprint='server')
Yann Leboulanger's avatar
Yann Leboulanger committed

            self._add(sockobj, self.readers, listener.file_props, sock_hash)
            sockobj.set_sock(sock[0])
            sockobj.queue = self
            self.connected += 1
Yann Leboulanger's avatar
Yann Leboulanger committed

    def process_result(self, result, actor):
        """
        Take appropriate actions upon the result:
                [ 0, - 1 ] complete/end transfer
                [ > 0 ] send progress message
                [ None ] do nothing
        """
        if result is None:
            return
        if result in (0, -1) and self.complete_transfer_cb is not None:
            account = actor.account
zimio's avatar
zimio committed
            if account is None and actor.file_props.tt_account:
                account = actor.file_props.tt_account
            self.complete_transfer_cb(account, actor.file_props)
        elif self.progress_transfer_cb is not None:
            self.progress_transfer_cb(actor.account, actor.file_props)

zimio's avatar
zimio committed
    def remove_receiver(self, idx, do_disconnect=True, remove_all=False):
        """
        Remove reciver from the list and decrease the number of active
        connections with 1
        """
        if idx != -1:
            for key in self.readers.keys():
                if idx in key:
                    reader = self.readers[key]
                    self.idlequeue.unplug_idle(reader.fd)
                    self.idlequeue.remove_timeout(reader.fd)
                    if do_disconnect:
                        reader.disconnect()
zimio's avatar
zimio committed
                        if not remove_all:
                            break
                    else:
                        if reader.streamhost is not None:
                            reader.streamhost['state'] = -1
                        del(self.readers[key])
zimio's avatar
zimio committed
                        if not remove_all:
                            break
Yann Leboulanger's avatar
Yann Leboulanger committed

zimio's avatar
zimio committed
    def remove_sender(self, idx, do_disconnect=True, remove_all=False):
        """
        Remove sender from the list of senders and decrease the number of active
        connections with 1
        """
        if idx != -1:
            for key in self.senders.keys():
                if idx in key:
                    sender = self.senders[key]
                    if do_disconnect:
                        sender.disconnect()
zimio's avatar
zimio committed
                        if not remove_all:
                            break
                    else:
                        self.idlequeue.unplug_idle(sender.fd)
                        self.idlequeue.remove_timeout(sender.fd)
                        del(self.senders[key])
                        if self.connected > 0:
                            self.connected -= 1
zimio's avatar
zimio committed
                        if not remove_all:
                            break
            if len(self.senders) == 0 and self.listener is not None:
                self.listener.disconnect()
                self.listener = None
                self.connected -= 1
Yann Leboulanger's avatar
Yann Leboulanger committed

zimio's avatar
zimio committed

dkirov's avatar
dkirov committed
class Socks5:
    def __init__(self, idlequeue, host, port, initiator, target, sid):
        if host is not None:
            try:
                self.host = host
                self.ais = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
Yann Leboulanger's avatar
Yann Leboulanger committed
                    socket.SOCK_STREAM)
            except socket.gaierror:
                self.ais = None
        self.idlequeue = idlequeue
        self.fd = -1
        self.port = port
        self.initiator = initiator
        self.target = target
        self.sid = sid
        self._sock = None
        self.account = None
        self.state = 0 # not connected
        self.pauses = 0
        self.size = 0
        self.remaining_buff = ''
        self.file = None
        self.connected = False
Yann Leboulanger's avatar
Yann Leboulanger committed
        self.type_ = ''
zimio's avatar
zimio committed
        self.mode = ''
    def _is_connected(self):
        if self.state < 5:
            return False
        return True
Yann Leboulanger's avatar
Yann Leboulanger committed

    def connect(self):
        """
        Create the socket and plug it to the idlequeue
        """
        if self.ais is None:
            return None
        for ai in self.ais:
            try:
                self._sock = socket.socket(*ai[:3])
                if not self.fingerprint is None:
                    self._sock = OpenSSL.SSL.Connection(
                        jingle_xtls.get_context('client'), self._sock)
                # this will not block the GUI
                self._sock.setblocking(False)
                self._server = ai[4]
                break
            except socket.error, e:
                if not isinstance(e, basestring) and e[0] == EINPROGRESS:
                    break
                # for all other errors, we try other addresses
                continue
        self.fd = self._sock.fileno()
        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)
        return None

    def do_connect(self):
            self._sock.connect(self._server)
            self._sock.setblocking(False)
            self._send=self._sock.send
            self._recv=self._sock.recv
        except Exception, ee:
            errnum = ee[0]
            self.connect_timeout += 1
            if errnum == 111 or self.connect_timeout > 1000:
Yann Leboulanger's avatar
Yann Leboulanger committed
                self.queue._connection_refused(self.streamhost, self.file_props,
                    self.queue_idx)
                self.connected = False
                return None
            # win32 needs this
            elif errnum not in  (10056, EISCONN) or self.state != 0:
                return None
            else: # socket is already connected
                self._sock.setblocking(False)
                self._send=self._sock.send
                self._recv=self._sock.recv
        self.buff = ''
        self.connected = True
zimio's avatar
zimio committed
        self.file_props.connected = True
        self.file_props.disconnect_cb = self.disconnect
        self.file_props.paused = False
        self.state = 1 # connected
        # stop all others connections to sender's streamhosts
        self.queue._socket_connected(self.streamhost, self.file_props)
        self.idlequeue.plug_idle(self, True, False)
        return 1 # we are connected
Yann Leboulanger's avatar
Yann Leboulanger committed

    def read_timeout(self):
        self.idlequeue.remove_timeout(self.fd)
        if self.state > 5:
            # no activity for foo seconds
zimio's avatar
zimio committed
            if self.file_props.stalled == False:
                self.file_props.stalled = True
                self.queue.process_result(-1, self)
zimio's avatar
zimio committed
                if not self.file_props.received_len:
                    self.file_props.received_len = 0
                if SEND_TIMEOUT > 0:
                    self.idlequeue.set_read_timeout(self.fd, SEND_TIMEOUT)
            else:
                # stop transfer, there is no error code for this
                self.pollend()
        else:
zimio's avatar
zimio committed
            if self.mode == 'client':
                self.queue.reconnect_client(self, self.streamhost)
Yann Leboulanger's avatar
Yann Leboulanger committed

    def open_file_for_reading(self):
        if self.file is None:
            try:
zimio's avatar
zimio committed
                self.file = open(self.file_props.file_name, 'rb')
                if self.file_props.offset:
                    self.size = self.file_props.offset
                    self.file.seek(self.size)
zimio's avatar
zimio committed
                    self.file_props.received_len = self.size
            except IOError, e:
                self.close_file()
                raise IOError, e

    def close_file(self):
        if self.file:
            if not self.file.closed:
                try:
                    self.file.close()
                except Exception:
                    pass
            self.file = None

    def get_fd(self):
        """
Yann Leboulanger's avatar
Yann Leboulanger committed
        Test if file is already open and return its fd, or just open the file
        and return the fd
zimio's avatar
zimio committed
        if self.file_props.fd:
            fd = self.file_props.fd
        else:
            offset = 0
            opt = 'wb'
zimio's avatar
zimio committed
            if self.file_props.offset:
                offset = self.file_props.offset
                opt = 'ab'
zimio's avatar
zimio committed
            fd = open(self.file_props.file_name, opt)
            self.file_props.fd = fd
            self.file_props.elapsed_time = 0
            self.file_props.last_time = self.idlequeue.current_time()
            self.file_props.received_len = offset
        return fd

    def rem_fd(self, fd):
zimio's avatar
zimio committed
        if self.file_props.fd:
            self.file_props.fd = None
        try:
            fd.close()
        except Exception:
            pass

    def receive(self):
        """
        Read small chunks of data. Call owner's disconnected() method if
        appropriate
        """
        received = ''
        try:
            add = self._recv(64)
        except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError,
Yann Leboulanger's avatar
Yann Leboulanger committed
        OpenSSL.SSL.WantX509LookupError), e:
            log.info('SSL rehandshake request : ' + repr(e))
            raise e
        except Exception:
            add = ''
        received += add
        if len(add) == 0:
            self.disconnect()
        return add

    def send_raw(self, raw_data):
        """
        Write raw outgoing data
        """
        try:
            self._send(raw_data)
        except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError,
Yann Leboulanger's avatar
Yann Leboulanger committed
        OpenSSL.SSL.WantX509LookupError), e:
            log.info('SSL rehandshake request :' + repr(e))
            raise e
        except Exception, e:
            self.disconnect()
        return len(raw_data)

    def write_next(self):
        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()
zimio's avatar
zimio committed
                self.file_props.error = -7 # unable to read from file
                return -1
            buff = self.file.read(MAX_BUFF_LEN)
        if len(buff) > 0:
            lenn = 0
            try:
                lenn = self._send(buff)
            except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError,
Yann Leboulanger's avatar
Yann Leboulanger committed
            OpenSSL.SSL.WantX509LookupError), e:
                log.info('SSL rehandshake request :' + repr(e))
                raise e
            except Exception, e:
                if e.args[0] not in (EINTR, ENOBUFS, EWOULDBLOCK):
                    # peer stopped reading
                    self.state = 8 # end connection
                    self.disconnect()
zimio's avatar
zimio committed
                    self.file_props.error = -1
                    return -1
            self.size += lenn
            current_time = self.idlequeue.current_time()
zimio's avatar
zimio committed
            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.state = 8 # end connection
zimio's avatar
zimio committed
                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
            if lenn == 0:
                return None
zimio's avatar
zimio committed
            self.file_props.stalled = False
            return lenn
        else:
            self.state = 8 # end connection
            self.disconnect()
            return -1

    def get_file_contents(self, timeout):
        """
        Read file contents from socket and write them to file
        """
zimio's avatar
zimio committed
        if self.file_props is None or not self.file_props.file_name:
            self.file_props.error = -2
            return None
        fd = None
        if self.remaining_buff != '':
            try:
                fd = self.get_fd()
            except IOError, e:
                self.disconnect(False)
zimio's avatar
zimio committed
                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()
zimio's avatar
zimio committed
            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
zimio's avatar
zimio committed
            if self.file_props.received_len == int(self.file_props.size):
                self.rem_fd(fd)
                self.disconnect()
zimio's avatar
zimio committed
                self.file_props.error = 0
                self.file_props.completed = True
                return 0
        else:
            try:
                fd = self.get_fd()
            except IOError, e:
                self.disconnect(False)
zimio's avatar
zimio committed
                self.file_props.error = -6 # file system error
                return 0
            try:
                buff = self._recv(MAX_BUFF_LEN)
            except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError,
Yann Leboulanger's avatar
Yann Leboulanger committed
            OpenSSL.SSL.WantX509LookupError), e:
                log.info('SSL rehandshake request :' + repr(e))
                raise e
            except Exception:
                buff = ''
            current_time = self.idlequeue.current_time()
zimio's avatar
zimio committed
            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()
zimio's avatar
zimio committed
                self.file_props.error = -1
                return 0
            try:
                fd.write(buff)
            except IOError, e:
                self.rem_fd(fd)
                self.disconnect()
zimio's avatar
zimio committed
                self.file_props.error = -6 # file system error
zimio's avatar
zimio committed
            if self.file_props.received_len >= int(self.file_props.size):
                # transfer completed
                self.rem_fd(fd)
                self.disconnect()
zimio's avatar
zimio committed
                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:
zimio's avatar
zimio committed
            self.file_props.stalled = False
        if fd is None and self.file_props.stalled is False:
zimio's avatar
zimio committed
        if self.file_props.received_len:
            if self.file_props.received_len != 0:
                return self.file_props.received_len
        return None

    def disconnect(self):
        """
        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)
zimio's avatar
zimio committed
        if self.mode == 'server':
            try:
                self.queue.listener.connections.remove(self._sock)
            except ValueError:
                pass # Not in list
            if self.queue.listener.connections == []:
                self.queue.listener.disconnect()
            if isinstance(self._sock, OpenSSL.SSL.Connection):
                self._sock.shutdown()
            else:
                self._sock.shutdown(socket.SHUT_RDWR)
            self._sock.close()
        except Exception:
            # socket is already closed
            pass
        self.connected = False
        self.fd = -1
        self.state = -1

    def _get_auth_buff(self):
        """
        Message, that we support 1 one auth mechanism: the 'no auth' mechanism
        """
        return struct.pack('!BBB', 0x05, 0x01, 0x00)

    def _parse_auth_buff(self, buff):
        """
        Parse the initial message and create a list of auth mechanisms
        """
        auth_mechanisms = []
        try:
            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)
        except Exception:
            return None
        return auth_mechanisms

    def _get_auth_response(self):
        """
        Socks version(5), number of extra auth methods (we send 0x00 - no auth)
        """
        return struct.pack('!BB', 0x05, 0x00)

    def _get_connect_buff(self):
Yann Leboulanger's avatar
Yann Leboulanger committed
        """
        Connect request by domain name
        """
        buff = struct.pack('!BBBBB%dsBB' % len(self.host),
Yann Leboulanger's avatar
Yann Leboulanger committed
            0x05, 0x01, 0x00, 0x03, len(self.host), self.host, self.port >> 8,
            self.port & 0xff)
        return buff

    def _get_request_buff(self, msg, command = 0x01):
        """
        Connect request by domain name, sid sha, instead of domain name (jep
        0096)
        """
        buff = struct.pack('!BBBBB%dsBB' % len(msg),
                0x05, command, 0x00, 0x03, len(msg), msg, 0, 0)
        return buff

    def _parse_request_buff(self, buff):
        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:])
            if portlen == 1:
                port, = struct.unpack('!B', buff[host_len + 5])
            elif portlen == 2:
                port, = struct.unpack('!H', buff[host_len + 5:])
            # file data, comes with auth message (Gaim bug)
            else:
                port, = struct.unpack('!H', buff[host_len + 5: host_len + 7])
                self.remaining_buff = buff[host_len + 7:]
        except Exception:
            return (None, None, None)
        return (req_type, host, port)

    def read_connect(self):
        """
        Connect response: version, auth method
        """
        try:
            buff = self._recv()
        except (SSL.WantReadError, SSL.WantWriteError,
                SSL.WantX509LookupError), e:
            log.info("SSL rehandshake request : " + repr(e))
            raise e
        try:
            version, method = struct.unpack('!BB', buff)
        except Exception:
            version, method = None, None
        if version != 0x05 or method == 0xff:
            self.disconnect()

    def continue_paused_transfer(self):
        if self.state < 5:
            return
zimio's avatar
zimio committed
        if self.file_props.type_ == 'r':
            self.idlequeue.plug_idle(self, False, True)
        else:
            self.idlequeue.plug_idle(self, True, False)

    def _get_sha1_auth(self):
        """
        Get sha of sid + Initiator jid + Target jid
        """
Yann Leboulanger's avatar
Yann Leboulanger committed

zimio's avatar
zimio committed
        if self.file_props.is_a_proxy:
            self.file_props.is_a_proxy = None # Is this necesary?
            return hashlib.sha1('%s%s%s' % (self.sid,
zimio's avatar
zimio committed
                self.file_props.proxy_sender,
                self.file_props.proxy_receiver)).hexdigest()
Yann Leboulanger's avatar
Yann Leboulanger committed
        return hashlib.sha1('%s%s%s' % (self.sid, self.initiator,
            self.target)).hexdigest()
Yann Leboulanger's avatar
Yann Leboulanger committed

zimio's avatar
zimio committed
class Socks5Sender(IdleObject):
    """
    Class for sending file to socket over socks5
    """

zimio's avatar
zimio committed
    def __init__(self, idlequeue, sock_hash, parent, _sock, host=None,
Yann Leboulanger's avatar
Yann Leboulanger committed
    port=None, fingerprint = None, connected=True, file_props={}):
        self.fingerprint = fingerprint
        self.queue_idx = sock_hash
        self.queue = parent
        self.file_props = file_props
zimio's avatar
zimio committed
        self.proxy = False
        self._sock = _sock
        if _sock is not None:
            if self.fingerprint is not None and not isinstance(self._sock,
            OpenSSL.SSL.Connection):
                self._sock = OpenSSL.SSL.Connection(
Yann Leboulanger's avatar
Yann Leboulanger committed
                    jingle_xtls.get_context('server'), _sock)
            else:
                self._sock.setblocking(False)
            self.fd = _sock.fileno()
            self._recv = _sock.recv
            self._send = _sock.send
        self.connected = connected
        self.state = 1 # waiting for first bytes
        self.connect_timeout = 0
Yann Leboulanger's avatar
Yann Leboulanger committed

zimio's avatar
zimio committed
        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.elapsed_time = 0
        self.file_props.last_time = self.idlequeue.current_time()
        self.file_props.received_len = 0
Yann Leboulanger's avatar
Yann Leboulanger committed
        self.type_ = 'sender'
    def start_transfer(self):
        """
        Send the file
        """
        return self.write_next()
Yann Leboulanger's avatar
Yann Leboulanger committed

    def set_connection_sock(self, _sock):
        self._sock = _sock
        if self.fingerprint is not None:
            self._sock = OpenSSL.SSL.Connection(
                jingle_xtls.get_context('client'), _sock)
        else:
            self._sock.setblocking(False)
        self.fd = _sock.fileno()
        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)
Yann Leboulanger's avatar
Yann Leboulanger committed

    def send_file(self):
        """
        Start sending the file over verified connection
        """
        self.pauses = 0
        self.state = 7
        # plug for writing
        self.idlequeue.plug_idle(self, True, False)
        return self.write_next() # initial for nl byte

    def disconnect(self, cb=True):
        """
        Close the socket
        """
        # close connection and remove us from the queue
        Socks5.disconnect(self)
        if self.file_props is not None:
zimio's avatar
zimio committed
            self.file_props.connected = False
            self.file_props.disconnect_cb = None
        if self.queue is not None:
            self.queue.remove_sender(self.queue_idx, False)
zimio's avatar
zimio committed

zimio's avatar
zimio committed
class Socks5Receiver(IdleObject):
zimio's avatar
zimio committed
    def __init__(self, idlequeue, streamhost, sid, file_props = None,
    fingerprint=None):
        """
Yann Leboulanger's avatar
Yann Leboulanger committed
        fingerprint: fingerprint of certificates we shall use, set to None if
        TLS connection not desired
zimio's avatar
zimio committed
        self.streamhost = streamhost
zimio's avatar
zimio committed
        self.connect_timeout = 0
        self.connected = False
        self.pauses = 0
        self.file_props = file_props
zimio's avatar
zimio committed
        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
        self.file_props.stalled = False
        self.file_props.received_len = 0
Yann Leboulanger's avatar
Yann Leboulanger committed

zimio's avatar
zimio committed
    def receive_file(self):
        """
        Start receiving the file over verified connection
        """
zimio's avatar
zimio committed
        if self.file_props.started:
zimio's avatar
zimio committed
            return
zimio's avatar
zimio committed
        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.elapsed_time = 0
        self.file_props.last_time = self.idlequeue.current_time()