Repository: abhinavsingh/proxy.py
Pull request #1047 (2022-01-23 10:47:08)

[SOCKS4] Packet parser & builder

codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) Report
> Merging [#1047](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (953e8dd) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/8e15dc4fbddbef92ae1db1385ce72a67bf0b225c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (8e15dc4) will **decrease** coverage by `0.26%`.
> The diff coverage is `83.78%`.

[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/graphs/tree.svg?width=650&height=150&src=pr&token=Zh9J7b4la2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)

```diff
@@             Coverage Diff             @@
##           develop    #1047      +/-   ##
===========================================
- Coverage    87.00%   86.74%   -0.27%     
===========================================
  Files          150      155       +5     
  Lines         6481     6555      +74     
  Branches       661      666       +5     
===========================================
+ Hits          5639     5686      +47     
- Misses         732      755      +23     
- Partials       110      114       +4     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `86.55% <83.78%> (-0.27%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | Coverage Δ | |
|---|---|---|
| [proxy/socks/client.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvc29ja3MvY2xpZW50LnB5) | `0.00% <0.00%> (ø)` | |
| [proxy/socks/socks4.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvc29ja3Mvc29ja3M0LnB5) | `0.00% <0.00%> (ø)` | |
| [proxy/socks/packet.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvc29ja3MvcGFja2V0LnB5) | `96.66% <96.66%> (ø)` | |
| [proxy/socks/operations.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvc29ja3Mvb3BlcmF0aW9ucy5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/socks/test\_packet.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvc29ja3MvdGVzdF9wYWNrZXQucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/http/parser/test\_proxy\_protocol.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvaHR0cC9wYXJzZXIvdGVzdF9wcm94eV9wcm90b2NvbC5weQ==) | `96.07% <0.00%> (-3.93%)` | :arrow_down: |
| [tests/testing/test\_embed.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvdGVzdGluZy90ZXN0X2VtYmVkLnB5) | `90.32% <0.00%> (-3.23%)` | :arrow_down: |
| [proxy/http/handler.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvaHR0cC9oYW5kbGVyLnB5) | `74.55% <0.00%> (-3.13%)` | :arrow_down: |
| [proxy/core/work/threadless.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS93b3JrL3RocmVhZGxlc3MucHk=) | `80.66% <0.00%> (-1.89%)` | :arrow_down: |
| [tests/common/test\_flags.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvY29tbW9uL3Rlc3RfZmxhZ3MucHk=) | `98.61% <0.00%> (-1.39%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Last update [8e15dc4...953e8dd](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1047?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).


Patch:
diff --git a/proxy/socks/__init__.py b/proxy/socks/__init__.py
new file mode 100644
index 0000000..b09e14b
--- /dev/null
+++ b/proxy/socks/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+from .client import SocksClientConnection
+from .packet import Socks4Packet
+from .handler import SocksProtocolHandler
+from .operations import Socks4Operations, socks4Operations
+
+
+__all__ = [
+    'Socks4Packet',
+    'socks4Operations',
+    'Socks4Operations',
+    'SocksProtocolHandler',
+    'SocksClientConnection',
+]
diff --git a/proxy/socks/client.py b/proxy/socks/client.py
new file mode 100644
index 0000000..287abf6
--- /dev/null
+++ b/proxy/socks/client.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+from ..core.connection import TcpClientConnection
+
+
+class SocksClientConnection(TcpClientConnection):
+    pass
diff --git a/proxy/socks/handler.py b/proxy/socks/handler.py
new file mode 100644
index 0000000..82f56a4
--- /dev/null
+++ b/proxy/socks/handler.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+from typing import Any, Optional
+
+from .client import SocksClientConnection
+from ..core.base import BaseTcpServerHandler
+
+
+class SocksProtocolHandler(BaseTcpServerHandler[SocksClientConnection]):
+    """Reference https://www.openssh.com/txt/socks4.protocol"""
+
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        super().__init__(*args, **kwargs)
+
+    @staticmethod
+    def create(**kwargs: Any) -> SocksClientConnection:
+        return SocksClientConnection(**kwargs)
+
+    def handle_data(self, data: memoryview) -> Optional[bool]:
+        return super().handle_data(data)
diff --git a/proxy/socks/operations.py b/proxy/socks/operations.py
new file mode 100644
index 0000000..8a2d822
--- /dev/null
+++ b/proxy/socks/operations.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+from typing import NamedTuple
+
+
+Socks4Operations = NamedTuple(
+    'Socks4Operations', [
+        ('CONNECT', int),
+        ('BIND', int),
+    ],
+)
+
+socks4Operations = Socks4Operations(1, 2)
diff --git a/proxy/socks/packet.py b/proxy/socks/packet.py
new file mode 100644
index 0000000..3d8c2df
--- /dev/null
+++ b/proxy/socks/packet.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import struct
+from typing import Optional
+
+
+NULL = b'\x00'
+
+
+class Socks4Packet:
+    """SOCKS4 and SOCKS4a protocol parser.
+
+    FIXME: Currently doesn't buffer during parsing and expects
+    packet to arrive within a single socket receive event.
+    """
+
+    def __init__(self) -> None:
+        # 1 byte, must be equal to 4
+        self.vn: Optional[int] = None
+        # 1 byte
+        self.cd: Optional[int] = None
+        # 2 bytes
+        self.dstport: Optional[int] = None
+        # 4 bytes
+        self.dstip: Optional[bytes] = None
+        # Variable bytes, NULL terminated
+        self.userid: Optional[bytes] = None
+
+    def parse(self, raw: memoryview) -> None:
+        cursor = 0
+        # Parse vn
+        if self.vn is None:
+            assert int(raw[cursor]) == 4
+            self.vn = 4
+        cursor += 1
+        # Parse cd
+        self.cd = raw[cursor]
+        cursor += 1
+        # Parse dstport
+        self.dstport = struct.unpack('!H', raw[cursor:cursor+2])[0]
+        cursor += 2
+        # Parse dstip
+        self.dstip = struct.unpack('!4s', raw[cursor:cursor+4])[0]
+        cursor += 4
+        # Parse userid
+        ulen = len(raw) - cursor - 1
+        self.userid = struct.unpack(
+            '!%ds' % ulen, raw[cursor:cursor+ulen],
+        )[0]
+        cursor += ulen
+        # Assert null terminated
+        assert raw[cursor] == NULL[0]
+
+    def pack(self) -> bytes:
+        user_id = self.userid or b''
+        return struct.pack(
+            '!bbH4s%ds' % len(user_id),
+            self.vn, self.cd,
+            self.dstport, self.dstip,
+            user_id,
+        ) + NULL


Test patch:
diff --git a/tests/socks/__init__.py b/tests/socks/__init__.py
new file mode 100644
index 0000000..232621f
--- /dev/null
+++ b/tests/socks/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
diff --git a/tests/socks/test_handler.py b/tests/socks/test_handler.py
new file mode 100644
index 0000000..cd53649
--- /dev/null
+++ b/tests/socks/test_handler.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import pytest
+
+from pytest_mock import MockerFixture
+
+from proxy.socks import SocksProtocolHandler, SocksClientConnection
+from proxy.common.flag import FlagParser
+from ..test_assertions import Assertions
+
+
+class TestHttpProtocolHandlerWithoutServerMock(Assertions):
+
+    @pytest.fixture(autouse=True)   # type: ignore[misc]
+    def _setUp(self, mocker: MockerFixture) -> None:
+        self.mock_fromfd = mocker.patch('socket.fromfd')
+        self.mock_selector = mocker.patch('selectors.DefaultSelector')
+
+        self.fileno = 10
+        self._addr = ('127.0.0.1', 54382)
+        self._conn = self.mock_fromfd.return_value
+
+        self.flags = FlagParser.initialize(threaded=True)
+
+        self.handler = SocksProtocolHandler(
+            SocksClientConnection(conn=self._conn, addr=self._addr),
+            flags=self.flags,
+        )
+        self.handler.initialize()
+
+    def test(self) -> None:
+        self.assertEqual(1, 1)
diff --git a/tests/socks/test_packet.py b/tests/socks/test_packet.py
new file mode 100644
index 0000000..5247414
--- /dev/null
+++ b/tests/socks/test_packet.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import re
+import socket
+import binascii
+
+import unittest
+
+from proxy.socks import Socks4Packet, socks4Operations
+
+
+def unhexlify(raw: str) -> bytes:
+    return binascii.unhexlify(re.sub(r'\s', '', raw))
+
+
+# Examples taken from https://en.wikipedia.org/wiki/SOCKS
+CLIENT_CONNECT_REQ = unhexlify('04 01 00 50 42 66 07 63 46 72 65 64 00')
+SERVER_CONNECT_OK = unhexlify('00 5A')
+
+
+class TestSocks4Packet(unittest.TestCase):
+
+    def test_pack(self) -> None:
+        pkt = Socks4Packet()
+        pkt.vn = 4
+        pkt.cd = socks4Operations.CONNECT
+        pkt.dstport = 80
+        pkt.dstip = socket.inet_aton('66.102.7.99')
+        pkt.userid = b'Fred'
+        self.assertEqual(
+            pkt.pack(),
+            CLIENT_CONNECT_REQ,
+        )
+
+    def test_parse(self) -> None:
+        wiki = memoryview(CLIENT_CONNECT_REQ)
+        pkt = Socks4Packet()
+        pkt.parse(wiki)
+        self.assertEqual(pkt.vn, 4)
+        self.assertEqual(pkt.cd, socks4Operations.CONNECT)
+        self.assertEqual(pkt.dstport, 80)
+        assert pkt.dstip
+        self.assertEqual(socket.inet_ntoa(pkt.dstip), '66.102.7.99')
+        self.assertEqual(pkt.userid, b'Fred')