| @@ -6,7 +6,9 @@ from .errors import ( | |||||
| from .helpers import ( | from .helpers import ( | ||||
| SocksAddr, Socks4Addr, Socks5Addr, Socks4Auth, Socks5Auth | SocksAddr, Socks4Addr, Socks5Addr, Socks4Auth, Socks5Auth | ||||
| ) | ) | ||||
| from .protocols import Socks4Protocol, Socks5Protocol, DEFAULT_LIMIT | |||||
| from .protocols import ( | |||||
| Socks4Protocol, Socks5Protocol, Socks5DGramProtocol, DEFAULT_LIMIT | |||||
| ) | |||||
| __version__ = '0.2.6' | __version__ = '0.2.6' | ||||
| @@ -17,6 +19,108 @@ __all__ = ('Socks4Protocol', 'Socks5Protocol', 'Socks4Auth', | |||||
| 'InvalidServerReply', 'create_connection', 'open_connection') | 'InvalidServerReply', 'create_connection', 'open_connection') | ||||
| # https://stackoverflow.com/a/53789029 | |||||
| def chain__await__(f): | |||||
| return lambda *args, **kwargs: f(*args, **kwargs).__await__() | |||||
| class DGram(object): | |||||
| '''An object that represents a datagram object. | |||||
| Use the send method to send data to the remote host. | |||||
| To receive data, simply await on the instance, and the next available | |||||
| datagram will be returned when available. | |||||
| When done, call the close method to shut everything down.''' | |||||
| def __init__(self, socksproto, hdr): | |||||
| self._sockproto = socksproto | |||||
| self._hdr = hdr | |||||
| self._q = asyncio.Queue() | |||||
| def connection_made(self, transport): | |||||
| self._dgtrans = transport | |||||
| def datagram_received(self, data, addr): | |||||
| '''Process relay UDP packets from the SOCKS server.''' | |||||
| frag, addr, payload = self._sockproto.parse_udp(data) | |||||
| if frag != 0: | |||||
| return | |||||
| self._q.put_nowait((payload, addr)) | |||||
| @property | |||||
| def proxy_sockname(self): | |||||
| return self._sockproto.proxy_sockname | |||||
| def send(self, data): | |||||
| '''Send datagram to the SOCKS server. | |||||
| This will wrap the datagram as needed before sending it on. | |||||
| This currently does not fragment UDP packets.''' | |||||
| self._dgtrans.sendto(self._hdr + data, None) | |||||
| def close(self): | |||||
| pass | |||||
| @chain__await__ | |||||
| async def __await__(self): | |||||
| '''Receive a datagram.''' | |||||
| return await self._q.get() | |||||
| async def open_datagram(proxy, proxy_auth, dst, *, | |||||
| remote_resolve=True, loop=None, family=0, | |||||
| proto=0, flags=0, sock=None, local_addr=None, | |||||
| server_hostname=None, reader_limit=DEFAULT_LIMIT): | |||||
| '''Create a transport object used to receive and send UDP packets | |||||
| to dst, via the SOCKS v5 proxy specified by proxy. | |||||
| The returned value is an instance of DGram.''' | |||||
| loop = loop or asyncio.get_event_loop() | |||||
| waiter = asyncio.Future(loop=loop) | |||||
| def sockdgram_factory(): | |||||
| if not isinstance(proxy, Socks5Addr): | |||||
| raise ValueError('only SOCKS v5 supports UDP') | |||||
| return Socks5DGramProtocol(proxy=proxy, proxy_auth=proxy_auth, dst=dst, | |||||
| app_protocol_factory=None, | |||||
| waiter=waiter, remote_resolve=remote_resolve, | |||||
| loop=loop, server_hostname=server_hostname, | |||||
| reader_limit=reader_limit) | |||||
| try: | |||||
| # connect to socks proxy | |||||
| transport, protocol = await loop.create_connection( | |||||
| sockdgram_factory, proxy.host, proxy.port, family=family, | |||||
| proto=proto, flags=flags, sock=sock, local_addr=local_addr) | |||||
| except OSError as exc: | |||||
| raise SocksConnectionError( | |||||
| '[Errno %s] Can not connect to proxy %s:%d [%s]' % | |||||
| (exc.errno, proxy.host, proxy.port, exc.strerror)) from exc | |||||
| try: | |||||
| await waiter | |||||
| except Exception: # noqa | |||||
| transport.close() | |||||
| raise | |||||
| # Build the header that the SOCKS UDP relay expects | |||||
| # https://tools.ietf.org/html/rfc1928#section-7 | |||||
| hdr = (await protocol.build_dst_address(*dst))[0] | |||||
| hdr = protocol.flatten_req([ 0, 0, 0, ] + hdr) | |||||
| # connect to the UDP relay the socks server told us to | |||||
| dgtrans, dgproto = await loop.create_datagram_endpoint( | |||||
| lambda: DGram(protocol, hdr), remote_addr=protocol.proxy_sockname) | |||||
| return dgproto | |||||
| async def create_connection(protocol_factory, proxy, proxy_auth, dst, *, | async def create_connection(protocol_factory, proxy, proxy_auth, dst, *, | ||||
| remote_resolve=True, loop=None, ssl=None, family=0, | remote_resolve=True, loop=None, ssl=None, family=0, | ||||
| proto=0, flags=0, sock=None, local_addr=None, | proto=0, flags=0, sock=None, local_addr=None, | ||||
| @@ -17,6 +17,8 @@ DEFAULT_LIMIT = getattr(asyncio.streams, '_DEFAULT_LIMIT', 2**16) | |||||
| class BaseSocksProtocol(asyncio.StreamReaderProtocol): | class BaseSocksProtocol(asyncio.StreamReaderProtocol): | ||||
| cmd = c.SOCKS_CMD_CONNECT | |||||
| def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, *, | def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, *, | ||||
| remote_resolve=True, loop=None, ssl=False, | remote_resolve=True, loop=None, ssl=False, | ||||
| server_hostname=None, negotiate_done_cb=None, | server_hostname=None, negotiate_done_cb=None, | ||||
| @@ -133,7 +135,8 @@ class BaseSocksProtocol(asyncio.StreamReaderProtocol): | |||||
| async def socks_request(self, cmd): | async def socks_request(self, cmd): | ||||
| raise NotImplementedError | raise NotImplementedError | ||||
| def write_request(self, request): | |||||
| @staticmethod | |||||
| def flatten_req(request): | |||||
| bdata = bytearray() | bdata = bytearray() | ||||
| for item in request: | for item in request: | ||||
| @@ -143,6 +146,11 @@ class BaseSocksProtocol(asyncio.StreamReaderProtocol): | |||||
| bdata += item | bdata += item | ||||
| else: | else: | ||||
| raise ValueError('Unsupported item') | raise ValueError('Unsupported item') | ||||
| return bdata | |||||
| def write_request(self, request): | |||||
| bdata = self.flatten_req(request) | |||||
| self._stream_writer.write(bdata) | self._stream_writer.write(bdata) | ||||
| async def read_response(self, n): | async def read_response(self, n): | ||||
| @@ -389,3 +397,37 @@ class Socks5Protocol(BaseSocksProtocol): | |||||
| port = struct.unpack('>H', port)[0] | port = struct.unpack('>H', port)[0] | ||||
| return addr, port | return addr, port | ||||
| async def build_udp(self, frag, addr, payload=b''): | |||||
| req, _ = await self.build_dst_address(*addr) | |||||
| return self.flatten_req([ 0, 0, frag ] + req + [ payload ]) | |||||
| @staticmethod | |||||
| def parse_udp(payload): | |||||
| resv, frag, atype = struct.unpack('>HBB', payload[:4]) | |||||
| if resv != 0: | |||||
| raise InvalidServerReply('SOCKS5 proxy server sent invalid data') | |||||
| pos = 4 | |||||
| if atype == c.SOCKS5_ATYP_IPv4: | |||||
| last = pos + 4 | |||||
| addr = socket.inet_ntoa(payload[pos:last]) | |||||
| elif atype == c.SOCKS5_ATYP_DOMAIN: | |||||
| length = payload[pos] | |||||
| pos += 1 | |||||
| last = pos + length | |||||
| addr = payload[pos:pos + length] | |||||
| addr = addr.decode('idna') | |||||
| elif atype == c.SOCKS5_ATYP_IPv6: | |||||
| last = pos + 16 | |||||
| addr = socket.inet_ntop(socket.AF_INET6, payload[pos:last]) | |||||
| else: | |||||
| raise InvalidServerReply('SOCKS5 proxy server sent invalid data') | |||||
| port = int.from_bytes(payload[last:last + 2], 'big') | |||||
| last += 2 | |||||
| return frag, (addr, port), payload[last:] | |||||
| class Socks5DGramProtocol(Socks5Protocol): | |||||
| cmd = c.SOCKS_CMD_UDP_ASSOCIATE | |||||
| @@ -1,12 +1,18 @@ | |||||
| import pytest | import pytest | ||||
| import aiosocks | import aiosocks | ||||
| import aiohttp | import aiohttp | ||||
| import asyncio | |||||
| import os | import os | ||||
| import ssl | import ssl | ||||
| import struct | |||||
| from aiohttp import web | from aiohttp import web | ||||
| from aiohttp.test_utils import RawTestServer | from aiohttp.test_utils import RawTestServer | ||||
| from aiohttp.test_utils import make_mocked_coro | |||||
| from aiosocks.test_utils import FakeSocksSrv, FakeSocks4Srv | from aiosocks.test_utils import FakeSocksSrv, FakeSocks4Srv | ||||
| from aiosocks.connector import ProxyConnector, ProxyClientRequest | from aiosocks.connector import ProxyConnector, ProxyClientRequest | ||||
| from aiosocks.errors import SocksConnectionError | |||||
| from async_timeout import timeout | |||||
| from unittest import mock | |||||
| async def test_socks4_connect_success(loop): | async def test_socks4_connect_success(loop): | ||||
| @@ -56,6 +62,109 @@ async def test_socks4_srv_error(loop): | |||||
| assert '0x5b' in str(ct) | assert '0x5b' in str(ct) | ||||
| # https://stackoverflow.com/a/55693498 | |||||
| def with_timeout(t): | |||||
| def wrapper(corofunc): | |||||
| async def run(*args, **kwargs): | |||||
| with timeout(t): | |||||
| return await corofunc(*args, **kwargs) | |||||
| return run | |||||
| return wrapper | |||||
| async def test_socks4_datagram_failure(): | |||||
| loop = asyncio.get_event_loop() | |||||
| async with FakeSocksSrv(loop, b'') as srv: | |||||
| addr = aiosocks.Socks4Addr('127.0.0.1', srv.port) | |||||
| with pytest.raises(ValueError): | |||||
| await aiosocks.open_datagram(addr, None, None, loop=loop) | |||||
| async def test_socks4_datagram_connect_failure(): | |||||
| loop = asyncio.get_event_loop() | |||||
| async def raiseconnerr(*args, **kwargs): | |||||
| raise OSError(1) | |||||
| async with FakeSocksSrv(loop, b'') as srv: | |||||
| addr = aiosocks.Socks4Addr('127.0.0.1', srv.port) | |||||
| with mock.patch.object(loop, 'create_connection', | |||||
| raiseconnerr), pytest.raises(SocksConnectionError): | |||||
| await aiosocks.open_datagram(addr, None, None, loop=loop) | |||||
| @with_timeout(2) | |||||
| async def test_socks5_datagram_success_anonymous(): | |||||
| # | |||||
| # This code is testing aiosocks.open_datagram. | |||||
| # | |||||
| # The server it is interacting with is srv (FakeSocksSrv). | |||||
| # | |||||
| # We mock the UDP Protocol to the SOCKS server w/ | |||||
| # sockservdgram (FakeDGramTransport) | |||||
| # | |||||
| # UDP packet flow: | |||||
| # dgram (DGram) -> sockservdgram (FakeDGramTransport) | |||||
| # which reflects it back for delivery | |||||
| # | |||||
| loop = asyncio.get_event_loop() | |||||
| pld = b'\x05\x00\x05\x00\x00\x01\x01\x01\x01\x01\x04W' | |||||
| respdata = b'response data' | |||||
| async with FakeSocksSrv(loop, pld) as srv: | |||||
| addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) | |||||
| auth = aiosocks.Socks5Auth('usr', 'pwd') | |||||
| dname = 'python.org' | |||||
| portnum = 53 | |||||
| dst = (dname, portnum) | |||||
| class FakeDGramTransport(asyncio.DatagramTransport): | |||||
| def sendto(self, data, addr=None): | |||||
| # Verify correct packet was receieved | |||||
| frag, addr, payload = aiosocks.protocols.Socks5Protocol.parse_udp(data) | |||||
| assert frag == 0 | |||||
| assert addr == ('python.org', 53) | |||||
| assert payload == b'some data' | |||||
| # Send frag reply, make sure it's ignored | |||||
| ba = bytearray() | |||||
| ba.extend([ 0, 0, 1, 1, 2, 2, 2, 2, ]) | |||||
| ba += (53).to_bytes(2, 'big') | |||||
| ba += respdata | |||||
| dgram.datagram_received(ba, ('3.3.3.3', 0)) | |||||
| # Send reply | |||||
| # wish I could use build_udp here, but it's async | |||||
| ba = bytearray() | |||||
| ba.extend([ 0, 0, 0, 1, 2, 2, 2, 2, ]) | |||||
| ba += (53).to_bytes(2, 'big') | |||||
| ba += respdata | |||||
| dgram.datagram_received(ba, ('3.3.3.3', 0)) | |||||
| sockservdgram = FakeDGramTransport() | |||||
| async def fake_cde(factory, remote_addr): | |||||
| assert remote_addr == ('1.1.1.1', 1111) | |||||
| proto = factory() | |||||
| proto.connection_made(sockservdgram) | |||||
| return sockservdgram, proto | |||||
| with mock.patch.object(loop, 'create_datagram_endpoint', | |||||
| fake_cde) as m: | |||||
| dgram = await aiosocks.open_datagram(addr, None, dst, loop=loop) | |||||
| assert dgram.proxy_sockname == ('1.1.1.1', 1111) | |||||
| dgram.send(b'some data') | |||||
| # XXX -- assert from fakesockssrv | |||||
| assert await dgram == (respdata, ('2.2.2.2', 53)) | |||||
| dgram.close() | |||||
| async def test_socks5_connect_success_anonymous(loop): | async def test_socks5_connect_success_anonymous(loop): | ||||
| pld = b'\x05\x00\x05\x00\x00\x01\x01\x01\x01\x01\x04Wtest' | pld = b'\x05\x00\x05\x00\x00\x01\x01\x01\x01\x01\x04Wtest' | ||||
| @@ -8,6 +8,7 @@ from asyncio import coroutine as coro, sslproto | |||||
| from aiohttp.test_utils import make_mocked_coro | from aiohttp.test_utils import make_mocked_coro | ||||
| import aiosocks.constants as c | import aiosocks.constants as c | ||||
| from aiosocks.protocols import BaseSocksProtocol | from aiosocks.protocols import BaseSocksProtocol | ||||
| from aiosocks.errors import InvalidServerReply | |||||
| def make_base(loop, *, dst=None, waiter=None, ap_factory=None, ssl=None): | def make_base(loop, *, dst=None, waiter=None, ap_factory=None, ssl=None): | ||||
| @@ -604,6 +605,52 @@ async def test_socks5_rd_addr_domain(loop): | |||||
| assert r == (b'python.org', 80) | assert r == (b'python.org', 80) | ||||
| async def test_socks5_build_udp_ipv4(loop): | |||||
| proto = make_socks5(loop) | |||||
| assert (await proto.build_udp(5, ('1.2.3.4', 16)) == | |||||
| b'\x00\x00\x05\x01\x01\x02\x03\x04\x00\x10') | |||||
| async def test_socks5_parse_udp_ipv4(loop): | |||||
| proto = make_socks5(loop) | |||||
| frag, addr, data = proto.parse_udp(b'\x00\x00\x07\x01\x01\x02\x09\x04\x00\x20foobar') | |||||
| assert frag == 7 | |||||
| assert addr == ('1.2.9.4', 32) | |||||
| assert data == b'foobar' | |||||
| async def test_socks5_parse_udp_domain(loop): | |||||
| proto = make_socks5(loop) | |||||
| frag, addr, data = proto.parse_udp(b'\x00\x00\x07\x03\x06domain\x00\x20foobar') | |||||
| assert frag == 7 | |||||
| assert addr == ('domain', 32) | |||||
| assert data == b'foobar' | |||||
| async def test_socks5_parse_udp_ipv6(loop): | |||||
| proto = make_socks5(loop) | |||||
| frag, addr, data = proto.parse_udp(b'\x00\x00\x07\x04' | |||||
| b' \x01\r\xb8\x11\xa3\t\xd7\x1f4\x8a.\x07\xa0v]' | |||||
| b'\x00\x20foobar') | |||||
| assert frag == 7 | |||||
| assert addr == ('2001:db8:11a3:9d7:1f34:8a2e:7a0:765d', 32) | |||||
| assert data == b'foobar' | |||||
| async def test_socks5_parse_udp_invalid(loop): | |||||
| proto = make_socks5(loop) | |||||
| for i in [ | |||||
| b'\x01\x00\x07\x01\x01\x02\x09\x04\x00\x20foobar', | |||||
| b'\x00\x01\x07\x01\x01\x02\x09\x04\x00\x20foobar', | |||||
| b'\x00\x00\x07\x09\x01\x02\x09\x04\x00\x20foobar', | |||||
| ]: | |||||
| with pytest.raises(InvalidServerReply): | |||||
| proto.parse_udp(i) | |||||
| async def test_socks5_socks_req_inv_ver(loop): | async def test_socks5_socks_req_inv_ver(loop): | ||||
| proto = make_socks5(loop, r=[b'\x05\x00', b'\x04\x00\x00']) | proto = make_socks5(loop, r=[b'\x05\x00', b'\x04\x00\x00']) | ||||