diff options
author | mathieui <mathieui@mathieui.net> | 2015-08-23 17:14:53 +0200 |
---|---|---|
committer | mathieui <mathieui@mathieui.net> | 2015-08-23 17:14:53 +0200 |
commit | 804b23d390fe5733f4f2cbd1b588fbc9b7c42e21 (patch) | |
tree | de208053ef66ad1650f650e888b836d49915603e | |
parent | 6e61adf3dba6945423bb4b8812c8ed0107274f50 (diff) | |
parent | 04eaf52b1d919e06f2a3c60ab63d625bc5d0797f (diff) | |
download | slixmpp-804b23d390fe5733f4f2cbd1b588fbc9b7c42e21.tar.gz slixmpp-804b23d390fe5733f4f2cbd1b588fbc9b7c42e21.tar.bz2 slixmpp-804b23d390fe5733f4f2cbd1b588fbc9b7c42e21.tar.xz slixmpp-804b23d390fe5733f4f2cbd1b588fbc9b7c42e21.zip |
Merge branch 'socks5' of http://git.linkmauve.fr/slixmpp
-rwxr-xr-x | examples/s5b_transfer/s5b_receiver.py | 90 | ||||
-rwxr-xr-x | examples/s5b_transfer/s5b_sender.py | 124 | ||||
-rw-r--r-- | slixmpp/plugins/xep_0065/__init__.py | 1 | ||||
-rw-r--r-- | slixmpp/plugins/xep_0065/proxy.py | 198 | ||||
-rw-r--r-- | slixmpp/plugins/xep_0065/socks5.py | 265 | ||||
-rw-r--r-- | slixmpp/stringprep.py | 16 | ||||
-rw-r--r-- | slixmpp/stringprep.pyx | 20 | ||||
-rw-r--r-- | slixmpp/thirdparty/__init__.py | 1 | ||||
-rw-r--r-- | slixmpp/thirdparty/socks.py | 378 |
9 files changed, 608 insertions, 485 deletions
diff --git a/examples/s5b_transfer/s5b_receiver.py b/examples/s5b_transfer/s5b_receiver.py new file mode 100755 index 00000000..bedeaa04 --- /dev/null +++ b/examples/s5b_transfer/s5b_receiver.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2015 Emmanuel Gil Peyrot + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import asyncio +import logging +from getpass import getpass +from argparse import ArgumentParser + +import slixmpp + + +class S5BReceiver(slixmpp.ClientXMPP): + + """ + A basic example of creating and using a SOCKS5 bytestream. + """ + + def __init__(self, jid, password, filename): + slixmpp.ClientXMPP.__init__(self, jid, password) + + self.file = open(filename, 'wb') + + self.add_event_handler("socks5_connected", self.stream_opened) + self.add_event_handler("socks5_data", self.stream_data) + self.add_event_handler("socks5_closed", self.stream_closed) + + def stream_opened(self, sid): + logging.info('Stream opened. %s', sid) + + def stream_data(self, data): + self.file.write(data) + + def stream_closed(self, exception): + logging.info('Stream closed. %s', exception) + self.file.close() + self.disconnect() + +if __name__ == '__main__': + # Setup the command line arguments. + parser = ArgumentParser() + + # Output verbosity options. + parser.add_argument("-q", "--quiet", help="set logging to ERROR", + action="store_const", dest="loglevel", + const=logging.ERROR, default=logging.INFO) + parser.add_argument("-d", "--debug", help="set logging to DEBUG", + action="store_const", dest="loglevel", + const=logging.DEBUG, default=logging.INFO) + + # JID and password options. + parser.add_argument("-j", "--jid", dest="jid", + help="JID to use") + parser.add_argument("-p", "--password", dest="password", + help="password to use") + parser.add_argument("-o", "--out", dest="filename", + help="file to save to") + + args = parser.parse_args() + + # Setup logging. + logging.basicConfig(level=args.loglevel, + format='%(levelname)-8s %(message)s') + + if args.jid is None: + args.jid = input("Username: ") + if args.password is None: + args.password = getpass("Password: ") + if args.filename is None: + args.filename = input("File path: ") + + # Setup the S5BReceiver and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = S5BReceiver(args.jid, args.password, args.filename) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0065', { + 'auto_accept': True + }) # SOCKS5 Bytestreams + + # Connect to the XMPP server and start processing XMPP stanzas. + xmpp.connect() + xmpp.process(forever=False) diff --git a/examples/s5b_transfer/s5b_sender.py b/examples/s5b_transfer/s5b_sender.py new file mode 100755 index 00000000..70a9704f --- /dev/null +++ b/examples/s5b_transfer/s5b_sender.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2015 Emmanuel Gil Peyrot + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import asyncio +import logging +from getpass import getpass +from argparse import ArgumentParser + +import slixmpp +from slixmpp.exceptions import IqError, IqTimeout + + +class S5BSender(slixmpp.ClientXMPP): + + """ + A basic example of creating and using a SOCKS5 bytestream. + """ + + def __init__(self, jid, password, receiver, filename): + slixmpp.ClientXMPP.__init__(self, jid, password) + + self.receiver = receiver + + self.file = open(filename, 'rb') + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. + self.add_event_handler("session_start", self.start) + + @asyncio.coroutine + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + + try: + # Open the S5B stream in which to write to. + proxy = yield from self['xep_0065'].handshake(self.receiver) + + # Send the entire file. + while True: + data = self.file.read(1048576) + if not data: + break + yield from proxy.write(data) + + # And finally close the stream. + proxy.transport.write_eof() + except (IqError, IqTimeout): + print('File transfer errored') + else: + print('File transfer finished') + finally: + self.file.close() + self.disconnect() + + +if __name__ == '__main__': + # Setup the command line arguments. + parser = ArgumentParser() + + # Output verbosity options. + parser.add_argument("-q", "--quiet", help="set logging to ERROR", + action="store_const", dest="loglevel", + const=logging.ERROR, default=logging.INFO) + parser.add_argument("-d", "--debug", help="set logging to DEBUG", + action="store_const", dest="loglevel", + const=logging.DEBUG, default=logging.INFO) + + # JID and password options. + parser.add_argument("-j", "--jid", dest="jid", + help="JID to use") + parser.add_argument("-p", "--password", dest="password", + help="password to use") + parser.add_argument("-r", "--receiver", dest="receiver", + help="JID of the receiver") + parser.add_argument("-f", "--file", dest="filename", + help="file to send") + parser.add_argument("-m", "--use-messages", action="store_true", + help="use messages instead of iqs for file transfer") + + args = parser.parse_args() + + # Setup logging. + logging.basicConfig(level=args.loglevel, + format='%(levelname)-8s %(message)s') + + if args.jid is None: + args.jid = input("Username: ") + if args.password is None: + args.password = getpass("Password: ") + if args.receiver is None: + args.receiver = input("Receiver: ") + if args.filename is None: + args.filename = input("File path: ") + + # Setup the S5BSender and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = S5BSender(args.jid, args.password, args.receiver, args.filename) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0065') # SOCKS5 Bytestreams + + # Connect to the XMPP server and start processing XMPP stanzas. + xmpp.connect() + xmpp.process(forever=False) 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..3e75b710 100644 --- a/slixmpp/plugins/xep_0065/proxy.py +++ b/slixmpp/plugins/xep_0065/proxy.py @@ -1,12 +1,10 @@ +import asyncio import logging -import threading 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 +12,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 +21,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 @@ -34,9 +32,6 @@ class XEP_0065(BasePlugin): self._proxies = {} self._sessions = {} - self._sessions_lock = threading.Lock() - - self._preauthed_sids_lock = threading.Lock() self._preauthed_sids = {} self.xmpp.register_handler( @@ -65,32 +60,32 @@ 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: log.warning('Received unknown SOCKS5 proxy: %s', proxy) return - with self._sessions_lock: - self._sessions[sid] = self._connect_proxy( - sid, - self.xmpp.boundjid, - 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], - peer=to) + 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 +114,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']} + + 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['disco_items']['items']: + 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 +135,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 +149,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'] @@ -159,40 +168,59 @@ class XEP_0065(BasePlugin): raise XMPPError(etype='modify', condition='not-acceptable') 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) + streamhost['port'])) + + @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 - except socket.error: - continue - else: - raise XMPPError(etype='cancel', condition='item-not-found') + 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: + iq = iq.reply() 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) + 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.""" @@ -204,66 +232,28 @@ class XEP_0065(BasePlugin): except socket.error: pass # Though this should not be neccessary remove the closed session anyway - with self._sessions_lock: - if sid in self._sessions: - log.warn(('SOCKS5 session with sid = "%s" was not ' + - 'removed from _sessions by sock.close()') % sid) - del self._sessions[sid] + if sid in self._sessions: + log.warn(('SOCKS5 session with sid = "%s" was not ' + + 'removed from _sessions by sock.close()') % sid) + del self._sessions[sid] def close(self): """Closes all proxy sockets.""" for sid, sock in self._sessions.items(): sock.close() - with self._sessions_lock: - self._sessions = {} + 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. <str> - requester : The JID of the requester. <str> - target : The JID of the target. <str> - proxy_host : The hostname or the IP of the proxy. <str> - proxy_port : The port of the proxy. <str> or <int> - 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. <str> + host : The hostname or the IP of the proxy. <str> + port : The port of the proxy. <str> or <int> """ - # 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'] @@ -278,15 +268,13 @@ class XEP_0065(BasePlugin): return self.auto_accept def _authorized_sid(self, jid, sid, ifrom, iq): - with self._preauthed_sids_lock: - log.debug('>>> authed sids: %s', self._preauthed_sids) - log.debug('>>> lookup: %s %s %s', jid, sid, ifrom) - if (jid, sid, ifrom) in self._preauthed_sids: - del self._preauthed_sids[(jid, sid, ifrom)] - return True - return False + log.debug('>>> authed sids: %s', self._preauthed_sids) + log.debug('>>> lookup: %s %s %s', jid, sid, ifrom) + if (jid, sid, ifrom) in self._preauthed_sids: + del self._preauthed_sids[(jid, sid, ifrom)] + return True + return False def _preauthorize_sid(self, jid, sid, ifrom, data): log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data) - with self._preauthed_sids_lock: - self._preauthed_sids[(jid, sid, ifrom)] = True + self._preauthed_sids[(jid, sid, ifrom)] = True 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/stringprep.py b/slixmpp/stringprep.py index e0757ef2..99506d78 100644 --- a/slixmpp/stringprep.py +++ b/slixmpp/stringprep.py @@ -101,5 +101,21 @@ def idna(domain): domain_parts.append(label) return '.'.join(domain_parts) +def punycode(domain): + domain_parts = [] + for label in domain.split('.'): + try: + label = encodings.idna.nameprep(label) + encodings.idna.ToASCII(label) + except UnicodeError: + raise StringprepError + + for char in label: + if char in ILLEGAL_CHARS: + raise StringprepError + + domain_parts.append(label) + return b'.'.join(domain_parts) + logging.getLogger(__name__).warning('Using slower stringprep, consider ' 'compiling the faster cython/libidn one.') diff --git a/slixmpp/stringprep.pyx b/slixmpp/stringprep.pyx index e17c62c3..e751c8ea 100644 --- a/slixmpp/stringprep.pyx +++ b/slixmpp/stringprep.pyx @@ -19,7 +19,8 @@ from libc.stdlib cimport free # Those are Cython declarations for the C function we’ll be using. cdef extern from "stringprep.h" nogil: - int stringprep_profile(const char* in_, char** out, const char* profile, int flags) + int stringprep_profile(const char* in_, char** out, const char* profile, + int flags) cdef extern from "idna.h" nogil: int idna_to_ascii_8z(const char* in_, char** out, int flags) @@ -40,16 +41,19 @@ cdef str _stringprep(str in_, const char* profile): free(out) return unicode_out + def nodeprep(str node): """The nodeprep profile of stringprep used to validate the local, or username, portion of a JID.""" return _stringprep(node, 'Nodeprep') + def resourceprep(str resource): """The resourceprep profile of stringprep, which is used to validate the resource portion of a JID.""" return _stringprep(resource, 'Resourceprep') + def idna(str domain): """The idna conversion functions, which are used to validate the domain portion of a JID.""" @@ -69,3 +73,17 @@ def idna(str domain): unicode_domain = utf8_domain.decode('utf-8') free(utf8_domain) return unicode_domain + + +def punycode(str domain): + """Converts a domain name to its punycode representation.""" + + cdef char* ascii_domain + cdef bytes bytes_domain + + ret = idna_to_ascii_8z(domain.encode('utf-8'), &ascii_domain, 0) + if ret != 0: + raise StringprepError(ret) + bytes_domain = ascii_domain + free(ascii_domain) + return bytes_domain 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])) |