From 9a6b090506989e462254f0e619d8eec3d4b6973f Mon Sep 17 00:00:00 2001
From: Yann Leboulanger <asterix@lagaule.org>
Date: Wed, 24 Jan 2007 21:50:59 +0000
Subject: [PATCH] begining of socks5 proxy support. error handling is missing.
 see #799

---
 data/glade/manage_proxies_window.glade |   4 +-
 src/common/connection.py               |   1 +
 src/common/xmpp/client_nb.py           |  17 ++-
 src/common/xmpp/transports_nb.py       | 152 +++++++++++++++++++++++++
 src/config.py                          |  13 ++-
 5 files changed, 179 insertions(+), 8 deletions(-)

diff --git a/data/glade/manage_proxies_window.glade b/data/glade/manage_proxies_window.glade
index db00551887..d4f6122804 100644
--- a/data/glade/manage_proxies_window.glade
+++ b/data/glade/manage_proxies_window.glade
@@ -17,6 +17,7 @@
   <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
   <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
   <property name="focus_on_map">True</property>
+  <property name="urgency_hint">False</property>
   <signal name="destroy" handler="on_manage_proxies_window_destroy" last_modification_time="Wed, 08 Jun 2005 17:33:08 GMT"/>
 
   <child>
@@ -209,7 +210,8 @@
 			  <child>
 			    <widget class="GtkComboBox" id="proxytype_combobox">
 			      <property name="visible">True</property>
-			      <property name="items" translatable="yes">HTTP Connect</property>
+			      <property name="items" translatable="yes">HTTP Connect
+SOCKS5</property>
 			      <property name="add_tearoffs">False</property>
 			      <property name="focus_on_click">True</property>
 			      <signal name="changed" handler="on_proxytype_combobox_changed" last_modification_time="Wed, 08 Jun 2005 17:45:26 GMT"/>
diff --git a/src/common/connection.py b/src/common/connection.py
index 3e52aec5e0..15bb5a4283 100644
--- a/src/common/connection.py
+++ b/src/common/connection.py
@@ -326,6 +326,7 @@ class Connection(ConnectionHandlers):
 			proxy['port'] = gajim.config.get_per('proxies', p, 'port')
 			proxy['user'] = gajim.config.get_per('proxies', p, 'user')
 			proxy['password'] = gajim.config.get_per('proxies', p, 'pass')
+			proxy['type'] = gajim.config.get_per('proxies', p, 'type')
 		else:
 			proxy = None
 
diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py
index 314f0a2326..f946bcfc0d 100644
--- a/src/common/xmpp/client_nb.py
+++ b/src/common/xmpp/client_nb.py
@@ -88,6 +88,8 @@ class NBCommonClient(CommonClient):
 			self.NonBlockingTLS.PlugOut()
 		if self.__dict__.has_key('NBHTTPPROXYsocket'):
 			self.NBHTTPPROXYsocket.PlugOut()
+		if self.__dict__.has_key('NBSOCKS5PROXYsocket'):
+			self.NBSOCKS5PROXYsocket.PlugOut()
 		if self.__dict__.has_key('NonBlockingTcp'):
 			self.NonBlockingTcp.PlugOut()
 		
@@ -102,9 +104,18 @@ class NBCommonClient(CommonClient):
 			server = (self.Server, self.Port)
 		self._Server,  self._Proxy, self._Ssl = server ,  proxy, ssl
 		self.on_stream_start = on_stream_start
-		if proxy: 
-			self.socket = transports_nb.NBHTTPPROXYsocket(self._on_connected, 
-				self._on_connected_failure, proxy, server)
+		if proxy:
+			if proxy.has_key('type'):
+				type_ = proxy['type']
+				if type_ == 'socks5':
+					self.socket = transports_nb.NBSOCKS5PROXYsocket(self._on_connected,
+						self._on_connected_failure, proxy, server)
+				elif type_ == 'http':
+					self.socket = transports_nb.NBHTTPPROXYsocket(self._on_connected,
+						self._on_connected_failure, proxy, server)
+			else:
+				self.socket = transports_nb.NBHTTPPROXYsocket(self._on_connected,
+					self._on_connected_failure, proxy, server)
 		else: 
 			self.connected = 'tcp'
 			self.socket = transports_nb.NonBlockingTcp(self._on_connected, 
diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py
index be55dfc895..4f90e21b8d 100644
--- a/src/common/xmpp/transports_nb.py
+++ b/src/common/xmpp/transports_nb.py
@@ -15,6 +15,7 @@
 ##   GNU General Public License for more details.
 
 import socket,select,base64,dispatcher_nb
+import struct
 from simplexml import ustr
 from client import PlugIn
 from idlequeue import IdleObject
@@ -885,6 +886,8 @@ class NBHTTPPROXYsocket(NonBlockingTcp):
 			self.DEBUG('Invalid proxy reply: %s %s %s' % (proto, code, desc),'error')
 			self._owner.disconnected()
 			return
+		if len(reply) != 2:
+			pass
 		self.onreceive(self._on_proxy_auth)
 	
 	def _on_proxy_auth(self, reply):
@@ -901,3 +904,152 @@ class NBHTTPPROXYsocket(NonBlockingTcp):
 	def DEBUG(self, text, severity):
 		''' Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".'''
 		return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity)
+
+class NBSOCKS5PROXYsocket(NonBlockingTcp):
+	'''SOCKS5 proxy connection class. Uses TCPsocket as the base class
+		redefines only connect method. Allows to use SOCKS5 proxies with
+		(optionally) simple authentication (only USERNAME/PASSWORD auth). 
+	'''
+	def __init__(self, on_connect = None, on_connect_failure = None,
+	proxy = None, server = None, use_srv = True):
+		''' Caches proxy and target addresses.
+			'proxy' argument is a dictionary with mandatory keys 'host' and 'port'
+			(proxy address) and optional keys 'user' and 'password' to use for
+			authentication. 'server' argument is a tuple of host and port -
+			just like TCPsocket uses. '''
+		self.on_connect_proxy = on_connect  
+		self.on_connect_failure = on_connect_failure
+		NonBlockingTcp.__init__(self, self._on_tcp_connect, on_connect_failure,
+			server, use_srv)
+		self.DBG_LINE=DBG_CONNECT_PROXY
+		self.server = server
+		self.proxy = proxy
+		self.ipaddr = None
+
+	def plugin(self, owner):
+		''' Starts connection. Used interally. Returns non-empty string on
+		success.'''
+		owner.debug_flags.append(DBG_CONNECT_PROXY)
+		NonBlockingTcp.plugin(self, owner)
+
+	def connect(self, dupe = None):
+		''' Starts connection. Connects to proxy, supplies login and password to
+			it (if were specified while creating instance). Instructs proxy to make
+			connection to the target server. Returns non-empty sting on success.
+		'''
+		NonBlockingTcp.connect(self, (self.proxy['host'], self.proxy['port']))
+		
+	def _on_tcp_connect(self):
+		self.DEBUG('Proxy server contacted, performing authentification', 'start')
+		if self.proxy.has_key('user') and self.proxy.has_key('password'):
+			to_send = '\x05\x02\x00\x02'
+		else:
+			to_send = '\x05\x01\x00'
+		self.onreceive(self._on_greeting_sent)
+		self.send(to_send)
+
+	def _on_greeting_sent(self, reply):
+		if reply is None:
+			return
+		if len(reply) != 2:
+			raise error('Invalid proxy reply')
+		if reply[0] != '\x05':
+			self.DEBUG('Invalid proxy reply', 'error')
+			self._owner.disconnected()
+			return
+		if reply[1] == '\x00':
+			return self._on_proxy_auth('\x01\x00')
+		elif reply[1] == '\x02':
+			# TODO: Do authentification
+			self.onreceive(self._on_proxy_auth)
+		else:
+			if reply[1] == '\xff':
+				self.DEBUG('Authentification to proxy impossible: no acceptable '
+					'auth method', 'error')
+			else:
+				self.DEBUG('Invalid proxy reply', 'error')
+			self._owner.disconnected()
+			return
+
+	def _on_proxy_auth(self, reply):
+		if reply is None:
+			return
+		if len(reply) != 2:
+			self.DEBUG('Invalid proxy reply', 'error')
+			self._owner.disconnected()
+			raise error('Invalid proxy reply')
+		if reply[0] != '\x01':
+			self.DEBUG('Invalid proxy reply', 'error')
+			self._owner.disconnected()
+			raise error('Invalid proxy reply')
+		if reply[1] != '\x00':
+			self.DEBUG('Authentification to proxy failed', 'error')
+			self._owner.disconnected()
+			return
+		self.DEBUG('Authentification successfull. Jabber server contacted.','ok')
+		# Request connection
+		req = "\x05\x01\x00"
+		# If the given destination address is an IP address, we'll
+		# use the IPv4 address request even if remote resolving was specified.
+		try:
+			self.ipaddr = socket.inet_aton(self.server[0])
+			req = req + "\x01" + ipaddr
+		except socket.error:
+			# Well it's not an IP number,  so it's probably a DNS name.
+#			if self.__proxy[3]==True:
+			# Resolve remotely
+			self.ipaddr = None
+			req = req + "\x03" + chr(len(self.server[0])) + self.server[0]
+#			else:
+#				# Resolve locally
+#				self.ipaddr = socket.inet_aton(socket.gethostbyname(self.server[0]))
+#				req = req + "\x01" + ipaddr
+		req = req + struct.pack(">H",self.server[1])
+		self.onreceive(self._on_req_sent)
+		self.send(req)
+
+	def _on_req_sent(self, reply):
+		if reply is None:
+			return
+		if len(reply) < 10:
+			self.DEBUG('Invalid proxy reply', 'error')
+			self._owner.disconnected()
+			raise error('Invalid proxy reply')
+		if reply[0] != '\x05':
+			self.DEBUG('Invalid proxy reply', 'error')
+			self._owner.disconnected()
+			raise error('Invalid proxy reply')
+		if reply[1] != "\x00":
+			# Connection failed
+			self._owner.disconnected()
+			if ord(reply[1])<9:
+				errors = ['general SOCKS server failure',
+					'connection not allowed by ruleset',
+					'Network unreachable',
+					'Host unreachable',
+					'Connection refused',
+					'TTL expired',
+					'Command not supported',
+					'Address type not supported'
+				]
+				txt = errors[ord(reply[1])-1]
+			else:
+				txt = 'Invalid proxy reply'
+			self.DEBUG(txt, 'error')
+			return
+		# Get the bound address/port
+		elif reply[3] == "\x01":
+			begin, end = 3, 7
+		elif reply[3] == "\x03":
+			begin, end = 4, 4 + reply[4]
+		else:
+			self.DEBUG('Invalid proxy reply', 'error')
+			self._owner.disconnected()
+			return
+
+		if self.on_connect_proxy:
+			self.on_connect_proxy()
+
+	def DEBUG(self, text, severity):
+		''' Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".'''
+		return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity)
diff --git a/src/config.py b/src/config.py
index 62f6e023a1..29955915e2 100644
--- a/src/config.py
+++ b/src/config.py
@@ -1655,6 +1655,7 @@ class ManageProxiesWindow:
 		self.window.set_transient_for(gajim.interface.roster.window)
 		self.proxies_treeview = self.xml.get_widget('proxies_treeview')
 		self.proxyname_entry = self.xml.get_widget('proxyname_entry')
+		self.proxytype_combobox = self.xml.get_widget('proxytype_combobox')
 		self.init_list()
 		self.xml.signal_autoconnect(self)
 		self.window.show_all()
@@ -1670,7 +1671,7 @@ class ManageProxiesWindow:
 
 	def init_list(self):
 		self.xml.get_widget('remove_proxy_button').set_sensitive(False)
-		self.xml.get_widget('proxytype_combobox').set_sensitive(False)
+		self.proxytype_combobox.set_sensitive(False)
 		self.xml.get_widget('proxy_table').set_sensitive(False)
 		model = gtk.ListStore(str)
 		self.proxies_treeview.set_model(model)
@@ -1755,7 +1756,9 @@ class ManageProxiesWindow:
 				'user'))
 			proxypass_entry.set_text(gajim.config.get_per('proxies', proxy,
 				'pass'))
-			#FIXME: if we have several proxy types, set the combobox
+			proxytype = gajim.config.get_per('proxies', proxy, 'type')
+			types = ['http', 'socks5']
+			self.proxytype_combobox.set_active(types.index(proxytype))
 			if gajim.config.get_per('proxies', proxy, 'user'):
 				useauth_checkbutton.set_active(True)
 
@@ -1782,8 +1785,10 @@ class ManageProxiesWindow:
 		model.set_value(iter, 0, new_name)
 
 	def on_proxytype_combobox_changed(self, widget):
-		#FIXME: if we have several proxy types take them into account
-		pass
+		types = ['http', 'socks5']
+		type_ = self.proxytype_combobox.get_active()
+		proxy = self.proxyname_entry.get_text().decode('utf-8')
+		gajim.config.set_per('proxies', proxy, 'type', types[type_])
 
 	def on_proxyhost_entry_changed(self, widget):
 		value = widget.get_text().decode('utf-8')
-- 
GitLab