From 9c5dd024b190e69583d06fae07db50896a41b454 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 20 Aug 2015 20:02:14 +0100 Subject: [PATCH] Fix the xep_0065 plugin, by rewriting its socks5 implementation. --- slixmpp/plugins/xep_0065/__init__.py | 1 + slixmpp/plugins/xep_0065/proxy.py | 173 ++++++------ slixmpp/plugins/xep_0065/socks5.py | 265 +++++++++++++++++++ slixmpp/thirdparty/__init__.py | 1 - slixmpp/thirdparty/socks.py | 378 --------------------------- 5 files changed, 352 insertions(+), 466 deletions(-) create mode 100644 slixmpp/plugins/xep_0065/socks5.py delete mode 100644 slixmpp/thirdparty/socks.py diff --git a/slixmpp/plugins/xep_0065/__init__.py b/slixmpp/plugins/xep_0065/__init__.py index 2bfe007c..c392bd23 100644 --- a/slixmpp/plugins/xep_0065/__init__.py +++ b/slixmpp/plugins/xep_0065/__init__.py @@ -1,5 +1,6 @@ from slixmpp.plugins.base import register_plugin +from slixmpp.plugins.xep_0065.socks5 import Socks5Protocol from slixmpp.plugins.xep_0065.stanza import Socks5 from slixmpp.plugins.xep_0065.proxy import XEP_0065 diff --git a/slixmpp/plugins/xep_0065/proxy.py b/slixmpp/plugins/xep_0065/proxy.py index 766c46cf..28b2baa5 100644 --- a/slixmpp/plugins/xep_0065/proxy.py +++ b/slixmpp/plugins/xep_0065/proxy.py @@ -1,3 +1,4 @@ +import asyncio import logging import threading import socket @@ -5,8 +6,6 @@ import socket from hashlib import sha1 from uuid import uuid4 -from slixmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5 - from slixmpp.stanza import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin @@ -14,7 +13,7 @@ from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.base import BasePlugin -from slixmpp.plugins.xep_0065 import stanza, Socks5 +from slixmpp.plugins.xep_0065 import stanza, Socks5, Socks5Protocol log = logging.getLogger(__name__) @@ -23,7 +22,7 @@ log = logging.getLogger(__name__) class XEP_0065(BasePlugin): name = 'xep_0065' - description = "Socks5 Bytestreams" + description = "XEP-0065: SOCKS5 Bytestreams" dependencies = set(['xep_0030']) default_config = { 'auto_accept': False @@ -65,12 +64,12 @@ class XEP_0065(BasePlugin): connection. """ if not self._proxies: - self._proxies = self.discover_proxies() + self._proxies = yield from self.discover_proxies() if sid is None: sid = uuid4().hex - used = self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) + used = yield from self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) proxy = used['socks']['streamhost_used']['jid'] if proxy not in self._proxies: @@ -78,19 +77,20 @@ class XEP_0065(BasePlugin): return with self._sessions_lock: - self._sessions[sid] = self._connect_proxy( - sid, - self.xmpp.boundjid, - to, - self._proxies[proxy][0], - self._proxies[proxy][1], - peer=to) + try: + self._sessions[sid] = (yield from self._connect_proxy( + self._get_dest_sha1(sid, self.xmpp.boundjid, to), + self._proxies[proxy][0], + self._proxies[proxy][1]))[1] + except socket.error: + return None + addr, port = yield from self._sessions[sid].connected # Request that the proxy activate the session with the target. - self.activate(proxy, sid, to, timeout=timeout) - socket = self.get_socket(sid) - self.xmpp.event('stream:%s:%s' % (sid, to), socket) - return socket + yield from self.activate(proxy, sid, to, timeout=timeout) + sock = self.get_socket(sid) + self.xmpp.event('stream:%s:%s' % (sid, to), sock) + return sock def request_stream(self, to, sid=None, ifrom=None, timeout=None, callback=None): if sid is None: @@ -119,11 +119,16 @@ class XEP_0065(BasePlugin): discovered = set() - disco_items = self.xmpp['xep_0030'].get_items(jid, timeout=timeout) + disco_items = yield from self.xmpp['xep_0030'].get_items(jid, timeout=timeout) + disco_items = {item[0] for item in disco_items['disco_items']['items']} - for item in disco_items['disco_items']['items']: + disco_info_futures = {} + for item in disco_items: + disco_info_futures[item] = self.xmpp['xep_0030'].get_info(item, timeout=timeout) + + for item in disco_items: try: - disco_info = self.xmpp['xep_0030'].get_info(item[0], timeout=timeout) + disco_info = yield from disco_info_futures[item] except XMPPError: continue else: @@ -135,7 +140,7 @@ class XEP_0065(BasePlugin): for jid in discovered: try: - addr = self.get_network_address(jid, ifrom=ifrom, timeout=timeout) + addr = yield from self.get_network_address(jid, ifrom=ifrom, timeout=timeout) self._proxies[jid] = (addr['socks']['streamhost']['host'], addr['socks']['streamhost']['port']) except XMPPError: @@ -149,6 +154,15 @@ class XEP_0065(BasePlugin): iq.enable('socks') return iq.send(timeout=timeout, callback=callback) + def _get_dest_sha1(self, sid, requester, target): + # The hostname MUST be SHA1(SID + Requester JID + Target JID) + # where the output is hexadecimal-encoded (not binary). + digest = sha1() + digest.update(sid.encode('utf8')) + digest.update(str(requester).encode('utf8')) + digest.update(str(target).encode('utf8')) + return digest.hexdigest() + def _handle_streamhost(self, iq): """Handle incoming SOCKS5 session request.""" sid = iq['socks']['sid'] @@ -160,39 +174,60 @@ class XEP_0065(BasePlugin): streamhosts = iq['socks']['streamhosts'] conn = None - used_streamhost = None + requester = iq['from'] + target = iq['to'] - sender = iq['from'] + dest = self._get_dest_sha1(sid, requester, target) + + proxy_futures = [] for streamhost in streamhosts: - try: - conn = self._connect_proxy(sid, - sender, - self.xmpp.boundjid, + proxy_futures.append(self._connect_proxy( + dest, streamhost['host'], - streamhost['port'], - peer=sender) - used_streamhost = streamhost['jid'] - break - except socket.error: - continue - else: - raise XMPPError(etype='cancel', condition='item-not-found') + streamhost['port'])) - iq = iq.reply() - with self._sessions_lock: - self._sessions[sid] = conn - iq['socks']['sid'] = sid - iq['socks']['streamhost_used']['jid'] = used_streamhost - iq.send() - self.xmpp.event('socks5_stream', conn) - self.xmpp.event('stream:%s:%s' % (sid, conn.peer_jid), conn) + @asyncio.coroutine + def gather(futures, iq, streamhosts): + proxies = yield from asyncio.gather(*futures, return_exceptions=True) + for streamhost, proxy in zip(streamhosts, proxies): + if isinstance(proxy, ValueError): + continue + elif isinstance(proxy, socket.error): + log.error('Socket error while connecting to the proxy.') + continue + proxy = proxy[1] + # TODO: what if the future never happens? + try: + addr, port = yield from proxy.connected + except socket.error: + log.exception('Socket error while connecting to the proxy.') + continue + # TODO: make a better choice than just the first working one. + used_streamhost = streamhost['jid'] + conn = proxy + break + else: + raise XMPPError(etype='cancel', condition='item-not-found') + + # TODO: close properly the connection to the other proxies. + + iq = iq.reply() + with self._sessions_lock: + self._sessions[sid] = conn + iq['socks']['sid'] = sid + iq['socks']['streamhost_used']['jid'] = used_streamhost + iq.send() + self.xmpp.event('socks5_stream', conn) + self.xmpp.event('stream:%s:%s' % (sid, requester), conn) + + asyncio.async(gather(proxy_futures, iq, streamhosts)) def activate(self, proxy, sid, target, ifrom=None, timeout=None, callback=None): """Activate the socks5 session that has been negotiated.""" iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom) iq['socks']['sid'] = sid iq['socks']['activate'] = target - iq.send(timeout=timeout, callback=callback) + return iq.send(timeout=timeout, callback=callback) def deactivate(self, sid): """Closes the proxy socket associated with this SID.""" @@ -217,53 +252,17 @@ class XEP_0065(BasePlugin): with self._sessions_lock: self._sessions = {} - def _connect_proxy(self, sid, requester, target, proxy, proxy_port, peer=None): - """ Establishes a connection between the client and the server-side + def _connect_proxy(self, dest, proxy, proxy_port): + """ Returns a future to a connection between the client and the server-side Socks5 proxy. - sid : The StreamID. - requester : The JID of the requester. - target : The JID of the target. - proxy_host : The hostname or the IP of the proxy. - proxy_port : The port of the proxy. or - peer : The JID for the other side of the stream, regardless - of target or requester status. + dest : The SHA-1 of (SID + Requester JID + Target JID), in hex. + host : The hostname or the IP of the proxy. + port : The port of the proxy. or """ - # Because the xep_0065 plugin uses the proxy_port as string, - # the Proxy class accepts the proxy_port argument as a string - # or an integer. Here, we force to use the port as an integer. - proxy_port = int(proxy_port) - sock = socksocket() - sock.setproxy(PROXY_TYPE_SOCKS5, proxy, port=proxy_port) - - # The hostname MUST be SHA1(SID + Requester JID + Target JID) - # where the output is hexadecimal-encoded (not binary). - digest = sha1() - digest.update(sid) - digest.update(str(requester)) - digest.update(str(target)) - - dest = digest.hexdigest() - - # The port MUST be 0. - sock.connect((dest, 0)) - log.info('Socket connected.') - - _close = sock.close - def close(*args, **kwargs): - with self._sessions_lock: - if sid in self._sessions: - del self._sessions[sid] - _close() - log.info('Socket closed.') - sock.close = close - - sock.peer_jid = peer - sock.self_jid = target if requester == peer else requester - - self.xmpp.event('socks_connected', sid) - return sock + factory = lambda: Socks5Protocol(dest, 0, self.xmpp.event) + return self.xmpp.loop.create_connection(factory, proxy, proxy_port) def _accept_stream(self, iq): receiver = iq['to'] diff --git a/slixmpp/plugins/xep_0065/socks5.py b/slixmpp/plugins/xep_0065/socks5.py new file mode 100644 index 00000000..54267b32 --- /dev/null +++ b/slixmpp/plugins/xep_0065/socks5.py @@ -0,0 +1,265 @@ +'''Pure asyncio implementation of RFC 1928 - SOCKS Protocol Version 5.''' + +import asyncio +import enum +import logging +import socket +import struct + +from slixmpp.stringprep import punycode, StringprepError + + +log = logging.getLogger(__name__) + + +class ProtocolMismatch(Exception): + '''We only implement SOCKS5, no other version or protocol.''' + + +class ProtocolError(Exception): + '''Some protocol error.''' + + +class MethodMismatch(Exception): + '''The server answered with a method we didn’t ask for.''' + + +class MethodUnacceptable(Exception): + '''None of our methods is supported by the server.''' + + +class AddressTypeUnacceptable(Exception): + '''The address type (ATYP) field isn’t one of IPv4, IPv6 or domain name.''' + + +class ReplyError(Exception): + '''The server answered with an error.''' + + possible_values = ( + "succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + + def __init__(self, result): + if result < 9: + Exception.__init__(self, self.possible_values[result]) + else: + Exception.__init__(self, self.possible_values[9]) + + +class Method(enum.IntEnum): + '''Known methods for a SOCKS5 session.''' + none = 0 + gssapi = 1 + password = 2 + # Methods 3 to 127 are reserved by IANA. + # Methods 128 to 254 are reserved for private use. + unacceptable = 255 + not_yet_selected = -1 + + +class Command(enum.IntEnum): + '''Existing commands for requests.''' + connect = 1 + bind = 2 + udp_associate = 3 + + +class AddressType(enum.IntEnum): + '''Existing address types.''' + ipv4 = 1 + domain = 3 + ipv6 = 4 + + +class Socks5Protocol(asyncio.Protocol): + '''This implements SOCKS5 as an asyncio protocol.''' + + def __init__(self, dest_addr, dest_port, event): + self.methods = {Method.none} + self.selected_method = Method.not_yet_selected + self.transport = None + self.dest = (dest_addr, dest_port) + self.connected = asyncio.Future() + self.event = event + self.paused = asyncio.Future() + self.paused.set_result(None) + + def register_method(self, method): + '''Register a SOCKS5 method.''' + self.methods.add(method) + + def unregister_method(self, method): + '''Unregister a SOCKS5 method.''' + self.methods.remove(method) + + def connection_made(self, transport): + '''Called when the connection to the SOCKS5 server is established.''' + + log.debug('SOCKS5 connection established.') + + self.transport = transport + self._send_methods() + + def data_received(self, data): + '''Called when we received some data from the SOCKS5 server.''' + + log.debug('SOCKS5 message received.') + + # If we are already connected, this is a data packet. + if self.connected.done(): + return self.event('socks5_data', data) + + # Every SOCKS5 message starts with the protocol version. + if data[0] != 5: + raise ProtocolMismatch() + + # Then select the correct handler for the data we just received. + if self.selected_method == Method.not_yet_selected: + self._handle_method(data) + else: + self._handle_connect(data) + + def connection_lost(self, exc): + log.debug('SOCKS5 connection closed.') + self.event('socks5_closed', exc) + + def pause_writing(self): + self.paused = asyncio.Future() + + def resume_writing(self): + self.paused.set_result(None) + + def write(self, data): + yield from self.paused + self.transport.write(data) + + def _send_methods(self): + '''Send the methods request, first thing a client should do.''' + + # Create the buffer for our request. + request = bytearray(len(self.methods) + 2) + + # Protocol version. + request[0] = 5 + + # Number of methods to send. + request[1] = len(self.methods) + + # List every method we support. + for i, method in enumerate(self.methods): + request[i + 2] = method + + # Send the request. + self.transport.write(request) + + def _send_request(self, command): + '''Send a request, should be done after having negociated a method.''' + + # Encode the destination address to embed it in our request. + # We need to do that first because its length is variable. + address, port = self.dest + addr = self._encode_addr(address) + + # Create the buffer for our request. + request = bytearray(5 + len(addr)) + + # Protocol version. + request[0] = 5 + + # Specify the command we want to use. + request[1] = command + + # request[2] is reserved, keeping it at 0. + + # Add our destination address and port. + request[3:3+len(addr)] = addr + request[-2:] = struct.pack('>H', port) + + # Send the request. + log.debug('SOCKS5 message sent.') + self.transport.write(request) + + def _handle_method(self, data): + '''Handle a method reply from the server.''' + + if len(data) != 2: + raise ProtocolError() + selected_method = data[1] + if selected_method not in self.methods: + raise MethodMismatch() + if selected_method == Method.unacceptable: + raise MethodUnacceptable() + self.selected_method = selected_method + self._send_request(Command.connect) + + def _handle_connect(self, data): + '''Handle a connect reply from the server.''' + + try: + addr, port = self._parse_result(data) + except ReplyError as exception: + self.connected.set_exception(exception) + self.connected.set_result((addr, port)) + self.event('socks5_connected', (addr, port)) + + def _parse_result(self, data): + '''Parse a reply from the server.''' + + result = data[1] + if result != 0: + raise ReplyError(result) + addr = self._parse_addr(data[3:-2]) + port = struct.unpack('>H', data[-2:])[0] + return (addr, port) + + @staticmethod + def _parse_addr(addr): + '''Parse an address (IP or domain) from a bytestream.''' + + addr_type = addr[0] + if addr_type == AddressType.ipv6: + try: + return socket.inet_ntop(socket.AF_INET6, addr[1:]) + except ValueError as e: + raise AddressTypeUnacceptable(e) + if addr_type == AddressType.ipv4: + try: + return socket.inet_ntop(socket.AF_INET, addr[1:]) + except ValueError as e: + raise AddressTypeUnacceptable(e) + if addr_type == AddressType.domain: + length = addr[1] + address = addr[2:] + if length != len(address): + raise Exception('Size mismatch') + return address.decode() + raise AddressTypeUnacceptable(addr_type) + + @staticmethod + def _encode_addr(addr): + '''Encode an address (IP or domain) into a bytestream.''' + + try: + ipv6 = socket.inet_pton(socket.AF_INET6, addr) + return b'\x04' + ipv6 + except OSError: + pass + try: + ipv4 = socket.inet_aton(addr) + return b'\x01' + ipv4 + except OSError: + pass + try: + domain = punycode(addr) + return b'\x03' + bytes([len(domain)]) + domain + except StringprepError: + pass + raise Exception('Err…') diff --git a/slixmpp/thirdparty/__init__.py b/slixmpp/thirdparty/__init__.py index 5caa28d3..fe1056e6 100644 --- a/slixmpp/thirdparty/__init__.py +++ b/slixmpp/thirdparty/__init__.py @@ -3,5 +3,4 @@ try: except: from slixmpp.thirdparty.gnupg import GPG -from slixmpp.thirdparty import socks from slixmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso diff --git a/slixmpp/thirdparty/socks.py b/slixmpp/thirdparty/socks.py deleted file mode 100644 index 9239a7b9..00000000 --- a/slixmpp/thirdparty/socks.py +++ /dev/null @@ -1,378 +0,0 @@ -"""SocksiPy - Python SOCKS module. -Version 1.00 - -Copyright 2006 Dan-Haim. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. - - -This module provides a standard socket-like interface for Python -for tunneling connections through SOCKS proxies. - - -Minor modifications made by Christopher Gilbert (http://motomastyle.com/) -for use in PyLoris (http://pyloris.sourceforge.net/) - -Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) -mainly to merge bug fixes found in Sourceforge - -""" - -import socket -import struct - -PROXY_TYPE_SOCKS4 = 1 -PROXY_TYPE_SOCKS5 = 2 -PROXY_TYPE_HTTP = 3 - -_defaultproxy = None -_orgsocket = socket.socket - -class ProxyError(Exception): pass -class GeneralProxyError(ProxyError): pass -class Socks5AuthError(ProxyError): pass -class Socks5Error(ProxyError): pass -class Socks4Error(ProxyError): pass -class HTTPError(ProxyError): pass - -_generalerrors = ("success", - "invalid data", - "not connected", - "not available", - "bad proxy type", - "bad input") - -_socks5errors = ("succeeded", - "general SOCKS server failure", - "connection not allowed by ruleset", - "Network unreachable", - "Host unreachable", - "Connection refused", - "TTL expired", - "Command not supported", - "Address type not supported", - "Unknown error") - -_socks5autherrors = ("succeeded", - "authentication is required", - "all offered authentication methods were rejected", - "unknown username or invalid password", - "unknown error") - -_socks4errors = ("request granted", - "request rejected or failed", - "request rejected because SOCKS server cannot connect to identd on the client", - "request rejected because the client program and identd report different user-ids", - "unknown error") - -def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets a default proxy which all further socksocket objects will use, - unless explicitly changed. - """ - global _defaultproxy - _defaultproxy = (proxytype, addr, port, rdns, username, password) - -def wrapmodule(module): - """wrapmodule(module) - Attempts to replace a module's socket library with a SOCKS socket. Must set - a default proxy using setdefaultproxy(...) first. - This will only work on modules that import socket directly into the namespace; - most of the Python Standard Library falls into this category. - """ - if _defaultproxy != None: - module.socket.socket = socksocket - else: - raise GeneralProxyError((4, "no proxy specified")) - -class socksocket(socket.socket): - """socksocket([family[, type[, proto]]]) -> socket object - Open a SOCKS enabled socket. The parameters are the same as - those of the standard socket init. In order for SOCKS to work, - you must specify family=AF_INET, type=SOCK_STREAM and proto=0. - """ - - def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): - _orgsocket.__init__(self, family, type, proto, _sock) - if _defaultproxy != None: - self.__proxy = _defaultproxy - else: - self.__proxy = (None, None, None, None, None, None) - self.__proxysockname = None - self.__proxypeername = None - - def __recvall(self, count): - """__recvall(count) -> data - Receive EXACTLY the number of bytes requested from the socket. - Blocks until the required number of bytes have been received. - """ - data = self.recv(count) - while len(data) < count: - d = self.recv(count-len(data)) - if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) - data = data + d - return data - - def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets the proxy to be used. - proxytype - The type of the proxy to be used. Three types - are supported: PROXY_TYPE_SOCKS4 (including socks4a), - PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP - addr - The address of the server (IP or DNS). - port - The port of the server. Defaults to 1080 for SOCKS - servers and 8080 for HTTP proxy servers. - rdns - Should DNS queries be preformed on the remote side - (rather than the local side). The default is True. - Note: This has no effect with SOCKS4 servers. - username - Username to authenticate with to the server. - The default is no authentication. - password - Password to authenticate with to the server. - Only relevant when username is also provided. - """ - self.__proxy = (proxytype, addr, port, rdns, username, password) - - def __negotiatesocks5(self, destaddr, destport): - """__negotiatesocks5(self,destaddr,destport) - Negotiates a connection through a SOCKS5 server. - """ - # First we'll send the authentication packages we support. - if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): - # The username/password details were supplied to the - # setproxy method so we support the USERNAME/PASSWORD - # authentication (in addition to the standard none). - self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) - else: - # No username/password were entered, therefore we - # only support connections with no authentication. - self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) - # We'll receive the server's response to determine which - # method was selected - chosenauth = self.__recvall(2) - if chosenauth[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - # Check the chosen authentication method - if chosenauth[1:2] == chr(0x00).encode(): - # No authentication is required - pass - elif chosenauth[1:2] == chr(0x02).encode(): - # Okay, we need to perform a basic username/password - # authentication. - self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) - authstat = self.__recvall(2) - if authstat[0:1] != chr(0x01).encode(): - # Bad response - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if authstat[1:2] != chr(0x00).encode(): - # Authentication failed - self.close() - raise Socks5AuthError((3, _socks5autherrors[3])) - # Authentication succeeded - else: - # Reaching here is always bad - self.close() - if chosenauth[1] == chr(0xFF).encode(): - raise Socks5AuthError((2, _socks5autherrors[2])) - else: - raise GeneralProxyError((1, _generalerrors[1])) - # Now we can request the actual connection - req = struct.pack('BBB', 0x05, 0x01, 0x00) - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. - try: - ipaddr = socket.inet_aton(destaddr) - req = req + chr(0x01).encode() + ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. - if self.__proxy[3]: - # Resolve remotely - ipaddr = None - req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr - else: - # Resolve locally - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) - self.sendall(req) - # Get the response - resp = self.__recvall(4) - if resp[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - elif resp[1:2] != chr(0x00).encode(): - # Connection failed - self.close() - if ord(resp[1:2])<=8: - raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) - else: - raise Socks5Error((9, _socks5errors[9])) - # Get the bound address/port - elif resp[3:4] == chr(0x01).encode(): - boundaddr = self.__recvall(4) - elif resp[3:4] == chr(0x03).encode(): - resp = resp + self.recv(1) - boundaddr = self.__recvall(ord(resp[4:5])) - else: - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - boundport = struct.unpack(">H", self.__recvall(2))[0] - self.__proxysockname = (boundaddr, boundport) - if ipaddr != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def getproxysockname(self): - """getsockname() -> address info - Returns the bound IP address and port number at the proxy. - """ - return self.__proxysockname - - def getproxypeername(self): - """getproxypeername() -> address info - Returns the IP and port number of the proxy. - """ - return _orgsocket.getpeername(self) - - def getpeername(self): - """getpeername() -> address info - Returns the IP address and port number of the destination - machine (note: getproxypeername returns the proxy) - """ - return self.__proxypeername - - def __negotiatesocks4(self,destaddr,destport): - """__negotiatesocks4(self,destaddr,destport) - Negotiates a connection through a SOCKS4 server. - """ - # Check if the destination address provided is an IP address - rmtrslv = False - try: - ipaddr = socket.inet_aton(destaddr) - except socket.error: - # It's a DNS name. Check where it should be resolved. - if self.__proxy[3]: - ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) - rmtrslv = True - else: - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - # Construct the request packet - req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr - # The username parameter is considered userid for SOCKS4 - if self.__proxy[4] != None: - req = req + self.__proxy[4] - req = req + chr(0x00).encode() - # DNS name if remote resolving is required - # NOTE: This is actually an extension to the SOCKS4 protocol - # called SOCKS4A and may not be supported in all cases. - if rmtrslv: - req = req + destaddr + chr(0x00).encode() - self.sendall(req) - # Get the response from the server - resp = self.__recvall(8) - if resp[0:1] != chr(0x00).encode(): - # Bad data - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - if resp[1:2] != chr(0x5A).encode(): - # Server returned an error - self.close() - if ord(resp[1:2]) in (91, 92, 93): - self.close() - raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) - else: - raise Socks4Error((94, _socks4errors[4])) - # Get the bound address/port - self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) - if rmtrslv != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def __negotiatehttp(self, destaddr, destport): - """__negotiatehttp(self,destaddr,destport) - Negotiates a connection through an HTTP server. - """ - # If we need to resolve locally, we do this now - if not self.__proxy[3]: - addr = socket.gethostbyname(destaddr) - else: - addr = destaddr - self.sendall(("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n").encode()) - # We read the response until we get the string "\r\n\r\n" - resp = self.recv(1) - while resp.find("\r\n\r\n".encode()) == -1: - resp = resp + self.recv(1) - # We just need the first line to check if the connection - # was successful - statusline = resp.splitlines()[0].split(" ".encode(), 2) - if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - try: - statuscode = int(statusline[1]) - except ValueError: - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if statuscode != 200: - self.close() - raise HTTPError((statuscode, statusline[2])) - self.__proxysockname = ("0.0.0.0", 0) - self.__proxypeername = (addr, destport) - - def connect(self, destpair): - """connect(self, despair) - Connects to the specified destination through a proxy. - destpar - A tuple of the IP/DNS address and the port number. - (identical to socket's connect). - To select the proxy server use setproxy(). - """ - # Do a minimal input check first - if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int): - raise GeneralProxyError((5, _generalerrors[5])) - if self.__proxy[0] == PROXY_TYPE_SOCKS5: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatesocks5(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_SOCKS4: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatesocks4(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_HTTP: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 8080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatehttp(destpair[0], destpair[1]) - elif self.__proxy[0] == None: - _orgsocket.connect(self, (destpair[0], destpair[1])) - else: - raise GeneralProxyError((4, _generalerrors[4]))