Skip to content
Snippets Groups Projects
socks5.py 33.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>
## 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>
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
dkirov's avatar
dkirov committed

from errno import EWOULDBLOCK
from errno import ENOBUFS
from errno import EINTR
from errno import EISCONN
from errno import EINPROGRESS
from xmpp.idlequeue import IdleObject
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
dkirov's avatar
dkirov committed
class SocksQueue:
	"""
	Queue for all file requests objects
	"""

	def __init__(self, idlequeue, complete_transfer_cb=None,
			progress_transfer_cb=None, error_cb=None):
dkirov's avatar
dkirov committed
		self.connected = 0
		self.readers = {}
		self.files_props = {}
dkirov's avatar
dkirov committed
		self.senders = {}
dkirov's avatar
dkirov committed
		self.idx = 1
dkirov's avatar
dkirov committed
		self.listener = None
		self.sha_handlers = {}
		# handle all io events in the global idle queue, instead of processing
		# each foo seconds
		self.idlequeue = idlequeue
dkirov's avatar
dkirov committed
		self.complete_transfer_cb = complete_transfer_cb
		self.progress_transfer_cb = progress_transfer_cb
		self.on_success = None
		self.on_failure = None
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	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
		"""
dkirov's avatar
dkirov committed
		self.sha_handlers[sha_str] = (sha_handler, sid)
dkirov's avatar
dkirov committed
			self.listener = Socks5Listener(self.idlequeue, port)
			self.listener.queue = self
dkirov's avatar
dkirov committed
			self.listener.bind()
Yann Leboulanger's avatar
Yann Leboulanger committed
			if self.listener.started is False:
				self.listener = None
				# 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.'))
				return None
dkirov's avatar
dkirov committed
			self.connected += 1
		return self.listener
Yann Leboulanger's avatar
Yann Leboulanger committed

	def send_success_reply(self, file_props, streamhost):
		if 'streamhost-used' in file_props and \
Yann Leboulanger's avatar
Yann Leboulanger committed
		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:
			for host in file_props['streamhosts']:
dkirov's avatar
dkirov committed
				if streamhost['state'] == 1:
					return 0
			streamhost['state'] = 1
			self.on_success(streamhost)
dkirov's avatar
dkirov committed
			return 1
		return 0
Yann Leboulanger's avatar
Yann Leboulanger committed

	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
Yann Leboulanger's avatar
Yann Leboulanger committed

		# add streamhosts to the queue
		for streamhost in file_props['streamhosts']:
			receiver = Socks5Receiver(self.idlequeue, streamhost, sid, file_props)
			self.add_receiver(account, receiver)
			streamhost['idx'] = receiver.queue_idx
Yann Leboulanger's avatar
Yann Leboulanger committed

	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:
				if host['state'] == 1:
					# remove current
					self.remove_receiver(streamhost['idx'])
					return
				# set state -2, meaning that this streamhost is stopped,
				# but it may be connectected later
Yann Leboulanger's avatar
Yann Leboulanger committed
				if host['state'] >= 0:
					self.remove_receiver(host['idx'])
					host['idx'] = -1
					host['state'] = -2
Yann Leboulanger's avatar
Yann Leboulanger committed

	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 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
Yann Leboulanger's avatar
Yann Leboulanger committed
					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)
Yann Leboulanger's avatar
Yann Leboulanger committed

	def _connection_refused(self, streamhost, file_props, idx):
		"""
		Called when we loose connection during transfer
		"""
		if file_props is None:
			return
		streamhost['state'] = -1
		self.remove_receiver(idx, False)
		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']:
Yann Leboulanger's avatar
Yann Leboulanger committed
			file_props['failure_cb'](streamhost['initiator'], streamhost['id'],
dkirov's avatar
dkirov committed
				file_props['sid'], code = 404)
			del(file_props['failure_cb'])
Yann Leboulanger's avatar
Yann Leboulanger committed

	def add_receiver(self, account, sock5_receiver):
		"""
		Add new file request
		"""
dkirov's avatar
dkirov committed
		self.readers[self.idx] = sock5_receiver
		sock5_receiver.queue_idx = self.idx
		sock5_receiver.queue = self
		sock5_receiver.account = account
dkirov's avatar
dkirov committed
		self.idx += 1
		result = sock5_receiver.connect()
dkirov's avatar
dkirov committed
		self.connected += 1
			result = sock5_receiver.main()
			self.process_result(result, sock5_receiver)
			return 1
		return None
Yann Leboulanger's avatar
Yann Leboulanger committed

	def get_file_from_sender(self, file_props, account):
		if file_props is None:
			return
Yann Leboulanger's avatar
Yann Leboulanger committed
		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)
Yann Leboulanger's avatar
Yann Leboulanger committed

	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)
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def activate_proxy(self, idx):
		if idx not in self.readers:
dkirov's avatar
dkirov committed
			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['elapsed-time'] = 0
			reader.file_props['last-time'] = self.idlequeue.current_time()
dkirov's avatar
dkirov committed
			reader.file_props['received-len'] = 0
			reader.pauses = 0
			# start sending file to proxy
			self.idlequeue.set_read_timeout(reader.fd, STALLED_TIMEOUT)
			self.idlequeue.plug_idle(reader, True, False)
dkirov's avatar
dkirov committed
			result = reader.write_next()
			self.process_result(result, reader)
Yann Leboulanger's avatar
Yann Leboulanger committed

	def send_file(self, file_props, account):
Yann Leboulanger's avatar
Yann Leboulanger committed
		if 'hash' in file_props and file_props['hash'] in self.senders:
			sender = self.senders[file_props['hash']]
			file_props['streamhost-used'] = True
			sender.account = account
			if file_props['type'] == 's':
Yann Leboulanger's avatar
Yann Leboulanger committed
				sender.file_props = file_props
				result = sender.send_file()
				self.process_result(result, sender)
			else:
				file_props['elapsed-time'] = 0
				file_props['last-time'] = self.idlequeue.current_time()
				file_props['received-len'] = 0
				sender.file_props = file_props
Yann Leboulanger's avatar
Yann Leboulanger committed

	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
		"""
Yann Leboulanger's avatar
Yann Leboulanger committed
		if file_props is None or ('sid' in file_props) is False:
dkirov's avatar
dkirov committed
			return
		_id = file_props['sid']
		if account not in self.files_props:
			self.files_props[account] = {}
		self.files_props[account][_id] = file_props
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def remove_file_props(self, account, sid):
		if account in self.files_props:
dkirov's avatar
dkirov committed
			fl_props = self.files_props[account]
dkirov's avatar
dkirov committed
				del(fl_props[sid])
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
		if len(self.files_props) == 0:
			self.connected = 0
Yann Leboulanger's avatar
Yann Leboulanger committed

	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]
dkirov's avatar
dkirov committed
				return fl_props[sid]
dkirov's avatar
dkirov committed
		return None
Yann Leboulanger's avatar
Yann Leboulanger committed

	def on_connection_accepted(self, sock):
Yann Leboulanger's avatar
Yann Leboulanger committed
		sock_hash = sock.__hash__()
		if sock_hash not in self.senders:
Yann Leboulanger's avatar
Yann Leboulanger committed
			self.senders[sock_hash] = Socks5Sender(self.idlequeue, sock_hash, self,
				sock[0], sock[1][0], sock[1][1])
			self.connected += 1
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov 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
nkour's avatar
nkour committed
		if result in (0, -1) and self.complete_transfer_cb is not None:
			account = actor.account
			if account is None and 'tt_account' in actor.file_props:
				account = actor.file_props['tt_account']
nkour's avatar
nkour committed
			self.complete_transfer_cb(account, actor.file_props)
dkirov's avatar
dkirov committed
		elif self.progress_transfer_cb is not None:
nkour's avatar
nkour committed
			self.progress_transfer_cb(actor.account, actor.file_props)
Yann Leboulanger's avatar
Yann Leboulanger committed

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

	def remove_sender(self, idx, do_disconnect=True):
		"""
		Remove sender from the list of senders and decrease the number of active
		connections with 1
		"""
dkirov's avatar
dkirov committed
		if idx != -1:
dkirov's avatar
dkirov committed
				if do_disconnect:
					self.senders[idx].disconnect()
dkirov's avatar
dkirov committed
				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
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
class Socks5:
	def __init__(self, idlequeue, host, port, initiator, target, sid):
dkirov's avatar
dkirov committed
		if host is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
				self.ais = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
					socket.SOCK_STREAM)
			except socket.gaierror:
		self.idlequeue = idlequeue
		self.fd = -1
dkirov's avatar
dkirov committed
		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
Yann Leboulanger's avatar
Yann Leboulanger committed

	def open_file_for_reading(self):
dkirov's avatar
dkirov committed
			try:
				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.seek(self.size)
					self.file_props['received-len'] = self.size
dkirov's avatar
dkirov committed
			except IOError, e:
				self.close_file()
				raise IOError, e
Yann Leboulanger's avatar
Yann Leboulanger committed

	def close_file(self):
		if self.file:
			if not self.file.closed:
				try:
					self.file.close()
				except Exception:
					pass
			self.file = None
Yann Leboulanger's avatar
Yann Leboulanger committed

	def get_fd(self):
		"""
		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:
			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['fd'] = fd
			self.file_props['elapsed-time'] = 0
			self.file_props['last-time'] = self.idlequeue.current_time()
			self.file_props['received-len'] = offset
Yann Leboulanger's avatar
Yann Leboulanger committed

	def rem_fd(self, fd):
		if 'fd' in self.file_props:
			del(self.file_props['fd'])
		except Exception:
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def receive(self):
		"""
		Read small chunks of data. Call owner's disconnected() method if
		appropriate
		"""
		received = ''
Yann Leboulanger's avatar
Yann Leboulanger committed
		try:
			add = self._recv(64)
		except Exception:
Yann Leboulanger's avatar
Yann Leboulanger committed
			add = ''
		received += add
		if len(add) == 0:
			self.disconnect()
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def send_raw(self,raw_data):
		"""
		Write raw outgoing data
		"""
dkirov's avatar
dkirov committed
		try:
			self._send(raw_data)
		except Exception:
dkirov's avatar
dkirov committed
			self.disconnect()
		return len(raw_data)
Yann Leboulanger's avatar
Yann Leboulanger committed

	def write_next(self):
		if self.remaining_buff != '':
			buff = self.remaining_buff
			self.remaining_buff = ''
		else:
dkirov's avatar
dkirov committed
			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
			buff = self.file.read(MAX_BUFF_LEN)
		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.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.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 = ''
			self.state = 7 # continue to write in the socket
			if lenn == 0:
			self.file_props['stalled'] = False
			return lenn
		else:
			self.state = 8 # end connection
			self.disconnect()
			return -1
Yann Leboulanger's avatar
Yann Leboulanger committed

	def get_file_contents(self, timeout):
		"""
		Read file contents from socket and write them to file
		"""
Yann Leboulanger's avatar
Yann Leboulanger committed
		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
Yann Leboulanger's avatar
Yann Leboulanger committed
			try:
				buff = self._recv(MAX_BUFF_LEN)
			except Exception:
				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'] += 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
			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
Yann Leboulanger's avatar
Yann Leboulanger committed

	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)
			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
Yann Leboulanger's avatar
Yann Leboulanger committed

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

dkirov's avatar
dkirov committed
	def _parse_auth_buff(self, buff):
		"""
		Parse the initial message and create a list of auth mechanisms
		"""
dkirov's avatar
dkirov committed
		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)
		except Exception:
			return None
dkirov's avatar
dkirov committed
		return auth_mechanisms
Yann Leboulanger's avatar
Yann Leboulanger committed

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

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

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

dkirov's avatar
dkirov committed
	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:])
Yann Leboulanger's avatar
Yann Leboulanger committed
			if portlen == 1:
				port, = struct.unpack('!B', buff[host_len + 5])
Yann Leboulanger's avatar
Yann Leboulanger committed
			elif portlen == 2:
				port, = struct.unpack('!H', buff[host_len + 5:])
			# file data, comes with auth message (Gaim bug)
Yann Leboulanger's avatar
Yann Leboulanger committed
			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)
dkirov's avatar
dkirov committed
		return (req_type, host, port)
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def read_connect(self):
		"""
		Connect response: version, auth method
		"""
dkirov's avatar
dkirov committed
		buff = self._recv()
		try:
			version, method = struct.unpack('!BB', buff)
		except Exception:
			version, method = None, None
dkirov's avatar
dkirov committed
		if version != 0x05 or method == 0xff:
			self.disconnect()
Yann Leboulanger's avatar
Yann Leboulanger committed

	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)
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def _get_sha1_auth(self):
		"""
		Get sha of sid + Initiator jid + Target jid
		"""
		if 'is_a_proxy' in self.file_props:
dkirov's avatar
dkirov committed
			del(self.file_props['is_a_proxy'])
			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)).\
Yann Leboulanger's avatar
Yann Leboulanger committed
			hexdigest()

class Socks5Sender(Socks5, IdleObject):
	"""
	Class for sending file to socket over socks5
	"""

Yann Leboulanger's avatar
Yann Leboulanger committed
	def __init__(self, idlequeue, sock_hash, parent, _sock, host=None,
dkirov's avatar
dkirov committed
		self.queue_idx = sock_hash
		self.queue = parent
		Socks5.__init__(self, idlequeue, host, port, None, None, None)
dkirov's avatar
dkirov committed
		self._sock = _sock
		self._sock.setblocking(False)
		self.fd = _sock.fileno()
dkirov's avatar
dkirov committed
		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 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:
Yann Leboulanger's avatar
Yann Leboulanger committed
				# stop transfer, there is no error code for this
				self.pollend()
Yann Leboulanger's avatar
Yann Leboulanger committed

	def pollout(self):
		if not self.connected:
			self.disconnect()
		self.idlequeue.remove_timeout(self.fd)
		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)
Yann Leboulanger's avatar
Yann Leboulanger committed
				return
			result = self.write_next()
			self.queue.process_result(result, self)
			if result is None or result <= 0:
				self.disconnect()
				return
			self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT)
		elif self.state == 8:
			self.disconnect()
			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)
Yann Leboulanger's avatar
Yann Leboulanger committed

	def pollend(self):
		self.state = 8 # end connection
		self.disconnect()
		self.file_props['error'] = -1
		self.queue.process_result(-1, self)
Yann Leboulanger's avatar
Yann Leboulanger committed

	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()
Yann Leboulanger's avatar
Yann Leboulanger committed

			elif self.state == 5:
Yann Leboulanger's avatar
Yann Leboulanger committed
				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:
			self.disconnect()
Yann Leboulanger's avatar
Yann Leboulanger committed

	def send_file(self):
		"""
		Start sending the file over verified connection
		"""
dkirov's avatar
dkirov committed
		if self.file_props['started']:
			return
		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
dkirov's avatar
dkirov committed
		self.pauses = 0
		self.state = 7
		# plug for writing
		self.idlequeue.plug_idle(self, True, False)
		return self.write_next() # initial for nl byte
Yann Leboulanger's avatar
Yann Leboulanger committed

	def main(self):
		"""
		Initial requests for verifying the connection
		"""
		if self.state == 1: # initial read
dkirov's avatar
dkirov committed
			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
dkirov's avatar
dkirov committed
			buff = self.receive()
			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)
dkirov's avatar
dkirov committed
		return None
Yann Leboulanger's avatar
Yann Leboulanger committed

	def disconnect(self, cb=True):
dkirov's avatar
dkirov committed
		# close connection and remove us from the queue
		Socks5.disconnect(self)
		if self.file_props is not None:
			self.file_props['connected'] = False
			self.file_props['disconnect_cb'] = None
dkirov's avatar
dkirov committed
		if self.queue is not None:
dkirov's avatar
dkirov committed
			self.queue.remove_sender(self.queue_idx, False)
class Socks5Listener(IdleObject):
dkirov's avatar
dkirov committed
	def __init__(self, idlequeue, port):
		"""
		Handle all incomming connections on (0.0.0.0, port)

		This class implements IdleObject, but we will expect
		only pollin events though
dkirov's avatar
dkirov committed
		self.port = port
		self.ais = socket.getaddrinfo(None, port, socket.AF_UNSPEC,
Yann Leboulanger's avatar
Yann Leboulanger committed
			socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_PASSIVE)
		self.ais.sort(reverse=True) # Try IPv6 first
Yann Leboulanger's avatar
Yann Leboulanger committed
		self.queue_idx = -1
		self.idlequeue = idlequeue
		self.queue = None
dkirov's avatar
dkirov committed
		self.started = False
		self._sock = None
		self.fd = -1
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def bind(self):
		for ai in self.ais:
			# 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
			except Exception:
				self.ai = None
				continue
		if not self.ai:
			# unable to bind, show error dialog
			return None
		self._serv.listen(socket.SOMAXCONN)
		self._serv.setblocking(False)
		self.fd = self._serv.fileno()
		self.idlequeue.plug_idle(self, False, True)
dkirov's avatar
dkirov committed
		self.started = True
Yann Leboulanger's avatar
Yann Leboulanger committed

	def pollend(self):
		"""
		Called when we stop listening on (host, port)
		"""
		self.disconnect()
Yann Leboulanger's avatar
Yann Leboulanger committed

	def pollin(self):
		"""
		Accept a new incomming connection and notify queue
		"""
		sock = self.accept_conn()
		self.queue.on_connection_accepted(sock)
Yann Leboulanger's avatar
Yann Leboulanger committed

	def disconnect(self):
		"""
		Free all resources, we are not listening anymore
		"""
		self.idlequeue.remove_timeout(self.fd)
		self.idlequeue.unplug_idle(self.fd)
		self.fd = -1
		self.state = -1
		self.started = False
		try:
			self._serv.close()
		except Exception:
Yann Leboulanger's avatar
Yann Leboulanger committed

dkirov's avatar
dkirov committed
	def accept_conn(self):
		"""
		Accept a new incomming connection
		"""
dkirov's avatar
dkirov committed
		_sock  = self._serv.accept()
		_sock[0].setblocking(False)
dkirov's avatar
dkirov committed
		return _sock
Yann Leboulanger's avatar
Yann Leboulanger committed

class Socks5Receiver(Socks5, IdleObject):
	def __init__(self, idlequeue, streamhost, sid, file_props = None):
dkirov's avatar
dkirov committed
		self.queue_idx = -1
		self.streamhost = streamhost
dkirov's avatar
dkirov committed
		self.queue = None
		self.file_props = file_props
		self.connect_timeout = 0
		self.connected = False
		self.pauses = 0
dkirov's avatar
dkirov committed
		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
dkirov's avatar
dkirov committed
		self.file_props['stalled'] = False
Yann Leboulanger's avatar
Yann Leboulanger committed
		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:
Yann Leboulanger's avatar
Yann Leboulanger committed
				# stop transfer, there is no error code for this
				self.pollend()
			self.queue.reconnect_receiver(self, self.streamhost)
Yann Leboulanger's avatar
Yann Leboulanger committed

	def connect(self):
		"""
		Create the socket and plug it to the idlequeue
		"""
js's avatar
js committed
			return None

		for ai in self.ais:
			try:
Yann Leboulanger's avatar
Yann Leboulanger committed
				self._sock = socket.socket(*ai[:3])
				# this will not block the GUI
				self._sock.setblocking(False)
Yann Leboulanger's avatar
Yann Leboulanger committed
				self._server = ai[4]
			except socket.error, e:
				if not isinstance(e, basestring) and e[0] == EINPROGRESS:
				# for all other errors, we try other addresses
		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
Yann Leboulanger's avatar
Yann Leboulanger committed

	def _is_connected(self):
		if self.state < 5:
			return False