## common/xmpp/socks5.py ## ## Gajim Team: ## - Yann Le Boulanger ## - Vincent Hanquez ## - Nikos Kouremenos ## - Dimitur Kirov ## ## Copyright (C) 2003-2005 Gajim Team ## ## This program 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 2 only. ## ## This program 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 ## GNU General Public License for more details. ## import socket import select import os import struct import sha from errno import EWOULDBLOCK from errno import ENOBUFS from errno import EINTR MAX_BUFF_LEN = 65536 class SocksQueue: ''' queue for all file requests objects ''' def __init__(self, complete_transfer_cb = None, progress_transfer_cb = None): self.connected = 0 self.readers = {} self.files_props = {} self.senders = {} self.idx = 1 self.listener = None self.sha_handlers = {} self.complete_transfer_cb = complete_transfer_cb self.progress_transfer_cb = progress_transfer_cb self.on_success = None self.on_failure = None def start_listener(self, host, port, sha_str, sha_handler, sid): self.sha_handlers[sha_str] = (sha_handler, sid) if self.listener == None: self.listener = Socks5Listener(host, port) self.listener.bind() if self.listener.started is False: self.listener = None import sys sys.stderr.write('\n\n\n========================================\ ========================\nUnable to bind to port %s. \nMaybe you have another \ running instance of Gajim. \nFile Transfer will be canceled.\n==================\ ==============================================\n\n\n' % port) return None self.connected += 1 return self.listener def send_success_reply(self, file_props, streamhost): if file_props.has_key('streamhost-used') and \ file_props['streamhost-used'] is True: if file_props.has_key('proxyhosts'): for proxy in file_props['proxyhosts']: if proxy == streamhost: self.on_success(streamhost) return 2 return 0 if file_props.has_key('streamhosts'): for host in file_props['streamhosts']: if streamhost['state'] == 1: return 0 streamhost['state'] = 1 self.on_success(streamhost) return 1 return 0 def connect_to_hosts(self, account, sid, on_success = None, on_failure = None): self.on_success = on_success self.on_failure = on_failure if not self.files_props.has_key(account): pass # FIXME ---- show error dialog else: file_props = self.files_props[account][sid] file_props['success_cb'] = on_success file_props['failure_cb'] = on_failure # add streamhosts to the queue for streamhost in file_props['streamhosts']: receiver = Socks5Receiver(streamhost, sid, file_props) self.add_receiver(account, receiver) streamhost['idx'] = receiver.queue_idx def _socket_connected(self, streamhost, file_props): for host in file_props['streamhosts']: if host != streamhost and host.has_key('idx'): if host['state'] == 1: self.remove_receiver(streamhost['idx']) return else: host['state'] = -1 self.remove_receiver(host['idx']) streamhost def _connection_refused(self, streamhost, file_props, idx): if file_props is None: return streamhost['state'] = -1 self.remove_receiver(idx) if file_props.has_key('streamhosts'): for host in file_props['streamhosts']: if host['state'] != -1: return if file_props['failure_cb']: file_props['failure_cb'](streamhost['initiator'], streamhost['id'], file_props['sid'], code = 404) 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 sock5_receiver.account = account self.idx += 1 result = sock5_receiver.connect() self.connected += 1 if result != 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 file_props is None: return file_props['hash'] if file_props.has_key('hash') and \ self.senders.has_key(file_props['hash']): sender = self.senders[file_props['hash']] sender.account = account result = get_file_contents(0) self.process_result(result, sender) def result_sha(self, sha_str, idx): if self.sha_handlers.has_key(sha_str): props = self.sha_handlers[sha_str] props[0](props[1], idx) def activate_proxy(self, idx): if not self.readers.has_key(idx): return reader = self.readers[idx] if reader.file_props['type'] != 's': return if reader.state != 5: return reader.state = 6 if reader.connected: reader.fd = open(reader.file_props['file-name']) 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['received-len'] = 0 reader.pauses = 0 reader.send_raw(reader._get_nl_byte()) result = reader.write_next() self.process_result(result, reader) def send_file(self, file_props, account): if file_props.has_key('hash') and \ self.senders.has_key(file_props['hash']): sender = self.senders[file_props['hash']] file_props['streamhost-used'] = True sender.account = account if file_props['type'] == 's': sender.file_props = file_props result = sender.send_file() self.process_result(result, sender) else: 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 \ file_props.has_key('sid') is False: return _id = file_props['sid'] if not self.files_props.has_key(account): self.files_props[account] = {} self.files_props[account][_id] = file_props def remove_file_props(self, account, sid): if self.files_props.has_key(account): fl_props = self.files_props[account] if fl_props.has_key(sid): del(fl_props[sid]) 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 self.files_props.has_key(account): fl_props = self.files_props[account] if fl_props.has_key(sid): return fl_props[sid] return None def process(self, timeout=0): ''' Process all registered connection. they can be receivers, senders and one listener ''' if self.listener is not None: if self.listener.pending_connection(): _sock = self.listener.accept_conn() sock_hash = _sock.__hash__() if not self.senders.has_key(sock_hash): self.senders[sock_hash] = Socks5Sender(sock_hash, self, _sock[0], _sock[1][0], _sock[1][1]) self.connected += 1 for idx in self.senders.keys(): sender = self.senders[idx] if sender.connected: if sender.state < 5: if sender.pending_data(timeout): result = sender.main() if sender.state == 4: self.result_sha(sender.sha_msg, idx) if result is None: continue if result == -1: sender.disconnect() elif sender.state == 5: if sender.file_props is not None and \ sender.file_props['type'] == 'r': result = sender.get_file_contents(0) self.process_result(result, sender) elif sender.state == 7 and not sender.file_props['paused']: if not sender.connected: self.process_result(-1, sender) break if sender.state == 8: self.remove_sender(idx) break result = sender.write_next() self.process_result(result, sender) if sender.file_props['stalled']: break elif sender.state == 8: self.remove_sender(idx) else: self.remove_sender(idx) keys = self.readers.keys() for idx in keys: if not self.readers.has_key(idx): continue receiver = self.readers[idx] if receiver.state == 0: res = receiver.do_connect() continue if receiver.connected: if receiver.file_props['paused']: continue if receiver.state < 5: pd = receiver.pending_data(0) if pd: result = receiver.main(0) self.process_result(result, receiver) elif receiver.state == 5: # wait for proxy reply pass else: if receiver.file_props['type'] == 'r': result = receiver.get_file_contents(timeout) else: result = receiver.write_next() self.process_result(result, receiver) else: self.remove_receiver(idx) 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 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) def remove_receiver(self, idx, do_disconnect = True): ''' Remove reciver from the list and decrease the number of active connections with 1''' if idx != -1: if self.readers.has_key(idx): if do_disconnect: self.readers[idx].disconnect() else: if self.readers[idx].streamhost is not None: self.readers[idx].streamhost['state'] = -1 del(self.readers[idx]) def remove_sender(self, idx, do_disconnect = True): ''' Remove reciver from the list of senders and decrease the number of active connections with 1''' if idx != -1: if self.senders.has_key(idx): if do_disconnect: self.senders[idx].disconnect() else: del(self.senders[idx]) if self.connected > 0: self.connected -= 1 class Socks5: def __init__(self, host, port, initiator, target, sid): if host is not None: self.host = socket.gethostbyname(host) 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 = '' def open_file_for_reading(self): self.fd = open(self.file_props['file-name']) self.fd.seek(self.size) def close_file(self): try: self.fd.close() except: pass def get_fd(self): ''' Test if file is already open and return its fd, or just open the file and return the fd. ''' if self.file_props.has_key('fd'): fd = self.file_props['fd'] else: fd = open(self.file_props['file-name'],'w') self.file_props['fd'] = fd self.file_props['received-len'] = 0 return fd def rem_fd(self, fd): if self.file_props.has_key('fd'): del(self.file_props['fd']) try: fd.close() except: pass def receive(self): ''' Reads small chunks of data. Calls owner's disconnected() method if appropriate.''' if self.pending_read(): received = '' try: add = self._recv(64) except Exception, e: add='' received +=add if len(add) == 0: self.disconnect() else: return None return add def send_raw(self,raw_data): ''' Writes raw outgoing data. ''' try: lenn = self._send(raw_data) except Exception, e: self.disconnect() return len(raw_data) def write_next(self, initial = False): if initial: try: # send lenn = self._send(self._get_nl_byte()) except Exception, e: errnum = e.args[0] # peer cannot read at the moment, continue later if errnum in [EINTR, ENOBUFS, EWOULDBLOCK]: pass else: # peer will not be able to read anymore self.state = 8 self.disconnect() self.file_props['error'] = -1 return -1 self.state = 7 return 1 if self.remaining_buff != '': buff = self.remaining_buff self.remaining_buff = '' else: self.open_file_for_reading() buff = self.fd.read(MAX_BUFF_LEN) self.close_file() if len(buff) > 0: lenn = 0 try: lenn = self._send(buff) except Exception, e: if e.args[0] not in [EINTR, ENOBUFS, EWOULDBLOCK]: # peer stopped reading self.state = 8 # end connection self.close_file() self.disconnect() self.file_props['error'] = -1 return -1 self.size += lenn self.file_props['received-len'] = self.size if self.size == int(self.file_props['size']): self.state = 8 # end connection self.file_props['error'] = 0 self.disconnect() return -1 if lenn != len(buff): self.remaining_buff = buff[lenn:] else: self.remaining_buff = '' if lenn == 0: self.pauses +=1 else: self.pauses = 0 if self.pauses > 24: self.file_props['stalled'] = True else: self.file_props['stalled'] = False self.state = 7 # continue to write in the socket if lenn == 0 and self.file_props['stalled'] is False: return None 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 ''', \ self.file_props['type'], self.file_props['sid'] if self.file_props is None or \ self.file_props.has_key('file-name') is False: self.file_props['error'] = -2 return None fd = None if self.remaining_buff != '': fd = self.get_fd() fd.write(self.remaining_buff) lenn = len(self.remaining_buff) 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: while self.pending_read(timeout): fd = self.get_fd() try: buff = self._recv(MAX_BUFF_LEN) except Exception, e: buff = '' first_byte = False if self.file_props['received-len'] == 0: if len(buff) > 0: # delimiter between auth and data if ord(buff[0]) == 0xD: first_byte = True buff = buff[1:] self.file_props['received-len'] += len(buff) fd.write(buff) if len(buff) == 0 and first_byte is False: # Transfer stopped somehow: # reset, paused or network error self.rem_fd(fd) self.disconnect(False) self.file_props['error'] = -1 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 == None: self.pauses +=1 else: self.pauses = 0 if self.pauses > 24: self.file_props['stalled'] = True else: self.file_props['stalled'] = False if fd == None and self.file_props['stalled'] is False: return None if self.file_props.has_key('received-len'): if self.file_props['received-len'] != 0: return self.file_props['received-len'] return None def disconnect(self, cb = True): ''' Closes the socket. ''' self._sock.close() self.connected = False def pending_read(self,timeout=0): ''' Returns true if there is a data ready to be read. ''' if self._sock is None: return False try: return select.select([self._sock],[],[],timeout)[0] except Exception, e: return False def pending_connection(self,timeout=0): ''' Returns true if there is a data ready to be read. ''' if self._sock is None: return False try: return select.select([],[self._sock],[],timeout)[0] except Exception, e: return False 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: ver, num_auth = struct.unpack('!BB', buff[:2]) for i in range(num_auth): mechanism, = struct.unpack('!B', buff[1 + i]) auth_mechanisms.append(mechanism) except: 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): ''' Connect request by domain name ''' buff = struct.pack('!BBBBB%dsBB' % len(self.host), 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 _get_nl_byte(self): ''' This is sent between auth and real data ''' return struct.pack('!B', 0x0D) def _parse_request_buff(self, buff): try: # don't trust on what comes from the outside version, req_type, reserved, host_type, = \ struct.unpack('!BBBB', buff[:4]) if host_type == 0x01: host_arr = struct.unpack('!iiii', buff[4:8]) host, = reduce(lambda e1, e2: str(e1) + "." + str(e2), 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: return (None, None, None) return (req_type, host, port) def read_connect(self): ''' connect responce: version, auth method ''' buff = self._recv() try: version, method = struct.unpack('!BB', buff) except: version, method = None, None if version != 0x05 or method == 0xff: self.disconnect() def _get_sha1_auth(self): ''' get sha of sid + Initiator jid + Target jid ''' if self.file_props.has_key('is_a_proxy'): del(self.file_props['is_a_proxy']) return sha.new('%s%s%s' % (self.sid, self.file_props['proxy_sender'], self.file_props['proxy_receiver'])).hexdigest() return sha.new('%s%s%s' % (self.sid, self.initiator, self.target)).hexdigest() class Socks5Sender(Socks5): ''' class for sending file to socket over socks5 ''' def __init__(self, sock_hash, parent, _sock, host = None, port = None): self.queue_idx = sock_hash self.queue = parent Socks5.__init__(self, host, port, None, None, None) self._sock = _sock self._sock.setblocking(False) self._recv = _sock.recv self._send = _sock.send self.connected = True self.state = 1 # waiting for first bytes self.file_props = None def send_file(self): ''' 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['stalled'] = False self.file_props['connected'] = True self.file_props['received-len'] = 0 self.pauses = 0 return self.write_next(initial = True) # initial for nl byte def main(self): ''' initial requests for verifying the connection ''' if self.state == 1: # initial read 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 == 2: # send reply with desired auth type self.send_raw(self._get_auth_response()) elif self.state == 3: # get next request buff = self.receive() (req_type, self.sha_msg, port) = self._parse_request_buff(buff) if req_type != 0x01: return -1 # request is not of type 'connect' elif self.state == 4: # send positive response to the 'connect' self.send_raw(self._get_request_buff(self.sha_msg, 0x00)) self.state += 1 # go to the next step return None def pending_data(self,timeout=0.01): ''' return true if there is a data ready to be read ''' if self._sock is None: return False try: if self.state in [1, 3, 5]: # (initial, request, send file) return self.pending_read(timeout) elif self.state in [2, 4]: # send auth and positive response return True except Exception, e: return False return False def disconnect(self, cb = True): ''' Closes the socket. ''' # close connection and remove us from the queue try: self._sock.close() except: pass self.connected = False if self.file_props is not None: 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) class Socks5Listener: def __init__(self, host, port): self.host, self.port = host, port self.queue_idx = -1 self.queue = None self.started = False self._sock = None def bind(self): try: self._serv=socket.socket(socket.AF_INET, socket.SOCK_STREAM) 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) self._serv.bind(('0.0.0.0', self.port)) self._serv.listen(socket.SOMAXCONN) self._serv.setblocking(False) except Exception, e: # unable to bind, show error dialog return None self.started = True def accept_conn(self): _sock = self._serv.accept() _sock[0].setblocking(False) return _sock def pending_connection(self,timeout=0.005): ''' Returns true if there is a data ready to be read. ''' if self._serv is None: return False try: res = select.select([self._serv],[],[],timeout) return res[0] except Exception, e: self.file_props['started'] = True return False class Socks5Receiver(Socks5): def __init__(self, streamhost, sid, file_props = None): self.queue_idx = -1 self.streamhost = streamhost self.queue = None self.file_props = file_props self.connect_timeout = 0 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['stalled'] = False Socks5.__init__(self, streamhost['host'], int(streamhost['port']), streamhost['initiator'], streamhost['target'], sid) def connect(self): ''' create the socket and start the connect loop ''' self._sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.settimeout(50) # this will not block the GUI self._sock.setblocking(False) self.state = 0 # about to be connected res = self.do_connect() return res def do_connect(self): try: self._sock.connect((self.host, self.port)) self._sock.setblocking(False) self._send=self._sock.send self._recv=self._sock.recv except Exception, ee: (errnum, errstr) = ee self.connect_timeout += 1 if errnum == 111 or self.connect_timeout > 1000: self.queue._connection_refused(self.streamhost, self.file_props, self.queue_idx) return None self.buff = '' self.connected = True self.file_props['connected'] = True self.file_props['disconnect_cb'] = self.disconnect self.state = 1 # connected self.queue._socket_connected(self.streamhost, self.file_props) return 1 # we are connected def main(self, timeout = 0): ''' begin negotiation. on success 'address' != 0 ''' result = 1 if self.state == 1: # send initially: version and auth types self.send_raw(self._get_auth_buff()) elif self.state == 2: # read auth response buff = self.receive() if buff is None or len(buff) != 2: return None version, method = struct.unpack('!BB', buff[:2]) if version != 0x05 or method == 0xff: self.disconnect() elif self.state == 3: # send 'connect' request self.send_raw(self._get_request_buff(self._get_sha1_auth())) elif self.state == 4: # get approve of our request buff = self.receive() if buff == None: return None sub_buff = buff[:4] if len(sub_buff) < 4: return None version, command, rsvd, address_type = struct.unpack('!BBBB', buff[:4]) addrlen, address, port = 0, 0, 0 if address_type == 0x03: addrlen = ord(buff[4]) address = struct.unpack('!%ds' % addrlen, buff[5:addrlen + 5]) portlen = len(buff[addrlen + 5:]) if portlen == 1: port, = struct.unpack('!B', buff[addrlen + 5]) elif portlen == 2: port, = struct.unpack('!H', buff[addrlen + 5:]) else: # Gaim bug :) port, = struct.unpack('!H', buff[addrlen + 5:addrlen + 7]) self.remaining_buff = buff[addrlen + 7:] self.state = 5 # for senders: init file_props and send '\n' if self.queue.on_success: result = self.queue.send_success_reply(self.file_props, self.streamhost) if result == 0: self.state = 8 self.disconnect() # for senders: init file_props and send '\n' if result == 1 and self.state == 5: if self.file_props['type'] == 's': self.fd = open(self.file_props['file-name']) 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['stalled'] = False self.file_props['received-len'] = 0 self.pauses = 0 self.send_raw(self._get_nl_byte()) self.state = 6 # send/get file contents if self.state < 5: self.state += 1 return None # we have set up the connection, next - retrieve file def pending_data(self, timeout=0): ''' Returns true if there is a data ready to be read. ''' if self._sock is None: return False try: if self.state in [2, 4, 6]: # auth response, connect, file data return self.pending_read(0.01) elif self.state in [1, 3, 5]: # auth types, connect request return True except Exception, e: return False return False def disconnect(self, cb = True): ''' Closes the socket. ''' # close connection and remove us from the queue if self._sock: try: self._sock.close() except: pass self.connected = False if cb is True: self.file_props['disconnect_cb'] = None if self.queue is not None: self.queue.remove_receiver(self.queue_idx, False)