pax_global_header 0000666 0000000 0000000 00000000064 13646347054 0014526 g ustar 00root root 0000000 0000000 52 comment=e69158473125060879319368e9465d7881cee0b8
socksio-1.0.0/ 0000775 0000000 0000000 00000000000 13646347054 0013176 5 ustar 00root root 0000000 0000000 socksio-1.0.0/.flake8 0000664 0000000 0000000 00000000070 13646347054 0014346 0 ustar 00root root 0000000 0000000 [flake8]
ignore = W503, E203, B305
max-line-length = 88
socksio-1.0.0/.gitignore 0000664 0000000 0000000 00000003261 13646347054 0015170 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
socksio-1.0.0/.readthedocs.yml 0000664 0000000 0000000 00000000172 13646347054 0016264 0 ustar 00root root 0000000 0000000 version: 2
sphinx:
configuration: docs/source/conf.py
python:
version: 3.7
install:
- method: pip
path: .
socksio-1.0.0/.travis.yml 0000664 0000000 0000000 00000001142 13646347054 0015305 0 ustar 00root root 0000000 0000000 dist: xenial
language: python
cache: pip
services:
- docker
branches:
only:
- master
matrix:
include:
- python: 3.7
env: NOX_SESSION=check
- python: 3.6
env: NOX_SESSION=test-3.6
- python: 3.7
env: NOX_SESSION=test-3.7
- python: 3.8-dev
env: NOX_SESSION=test-3.8
- python: 3.7
script:
- pip install .
- bash tests/run_acceptance_tests.sh
install:
- pip install --upgrade nox
script:
- nox -s ${NOX_SESSION}
after_script:
- if [ -f .coverage ]; then
python -m pip install codecov;
codecov --required;
fi
socksio-1.0.0/CHANGELOG.md 0000664 0000000 0000000 00000001246 13646347054 0015012 0 ustar 00root root 0000000 0000000 # Changelog
## 1.0.0 (2020-04-17)
### Changed
- `from_address` methods now accept `str` or `bytes` objects
[#46](https://github.com/sethmlarson/socksio/pull/46).
## 0.2.0 (2020-01-28)
### Changed
- **BREAKING**: API redesign using request objects and reducing the methods in
the different connection classes [#37](https://github.com/sethmlarson/socksio/pull/37).
## 0.1.1 (2020-01-10)
### Fixed
- SOCKS5 prefixing domain name with length [#29](https://github.com/sethmlarson/socksio/pull/29).
- SOCKS5 requiring authentication even if no authentication method is specified
[#30](https://github.com/sethmlarson/socksio/pull/30).
## 0.1.0 (2019-12-03)
Initial release.
socksio-1.0.0/LICENSE 0000664 0000000 0000000 00000002064 13646347054 0014205 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2019 Seth Michael Larson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
socksio-1.0.0/MANIFEST.in 0000664 0000000 0000000 00000000042 13646347054 0014730 0 ustar 00root root 0000000 0000000 include README.md
include LICENSE
socksio-1.0.0/README.md 0000664 0000000 0000000 00000012421 13646347054 0014455 0 ustar 00root root 0000000 0000000 # SOCKSIO
[](https://travis-ci.org/sethmlarson/socksio)
[](https://codecov.io/gh/sethmlarson/socksio)
[](https://pypi.org/project/socksio)
[](https://pypi.org/project/socksio)
Client-side sans-I/O SOCKS proxy implementation.
Supports SOCKS4, SOCKS4A, and SOCKS5.
`socksio` is a sans-I/O library similar to
[`h11`](https://github.com/python-hyper/h11) or
[`h2`](https://github.com/python-hyper/hyper-h2/), this means the library itself
does not handle the actual sending of the bytes through the network, it only
deals with the implementation details of the SOCKS protocols so you can use
it in any I/O library you want.
## Current status: stable
Features not yet implemented:
- SOCKS5 GSS-API authentication.
- SOCKS5 UDP associate requests.
## Usage
TL;DR check the [examples directory](examples/).
Being sans-I/O means that in order to test `socksio` you need an I/O library.
And the most basic I/O is, of course, the standard library's `socket` module.
You'll need to know ahead of time the type of SOCKS proxy you want to connect
to. Assuming we have a SOCKS4 proxy running in our machine on port 8080, we
will first create a connection to it:
```python
import socket
sock = socket.create_connection(("localhost", 8080))
```
`socksio` exposes modules for SOCKS4, SOCKS4A and SOCKS5, each of them includes
a `Connection` class:
```python
from socksio import socks4
# The SOCKS4 protocol requires a `user_id` to be supplied.
conn = socks4.SOCKS4Connection(user_id=b"socksio")
```
Since `socksio` is a sans-I/O library, we will use the socket to send and
receive data to our SOCKS4 proxy. The raw data, however, will be created and
parsed by our `SOCKS4Connection`.
We need to tell our connection we want to make a request to the proxy. We do
that by first creating a request object.
In SOCKS4 we only need to send a command along with an IP address and port.
`socksio` exposes the different types of commands as enumerables and a
convenience `from_address` class method in the request classes to create a
valid request object:
```python
# SOCKS4 does not allow domain names, below is an IP for google.com
request = socks4.SOCKS4Request.from_address(
socks4.SOCKS4Command.CONNECT, ("216.58.204.78", 80))
```
`from_address` methods are available on all request classes in `socksio`, they
accept addresses as tuples of `(address, port)` as well as string `address:port`.
Now we ask the connection to send our request:
```python
conn.send(request)
```
The `SOCKS4Connection` will then compose the necessary `bytes` in the proper
format for us to send to our proxy:
```python
data = conn.data_to_send()
sock.sendall(data)
```
If all goes well the proxy will have sent reply, we just need to read from the
socket and pass the data to the `SOCKS4Connection`:
```python
data = sock.recv(1024)
event = conn.receive_data(data)
```
The connection will parse the data and return an event from it, in this case, a
`SOCKS4Reply` that includes attributes for the fields in the SOCKS reply:
```python
if event.reply_code != socks4.SOCKS4ReplyCode.REQUEST_GRANTED:
raise Exception(
"Server could not connect to remote host: {}".format(event.reply_code)
)
```
If all went well the connection has been established correctly and we can
start sending our request directly to the proxy:
```python
sock.sendall(b"GET / HTTP/1.1\r\nhost: google.com\r\n\r\n")
data = receive_data(sock)
print(data)
# b'HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.google.com/...`
```
The same methodology is used for all protocols, check out the
[examples directory](https://github.com/sethmlarson/socksio/tree/master/examples/)
for more information.
## Development
Install the test requirements with `pip install -r test-requirements.txt`.
Install the project in pseudo-editable mode with `flit install -s`.
Tests can be ran directly invoking `pytest`.
This project uses [`nox`](https://nox.thea.codes/en/stable/) to automate
testing and linting tasks. `nox` is installed as part of the test requirements.
Invoking `nox` will run all sessions, but you may also run only some them, for
example `nox -s lint` will only run the linting session.
In order to test against a live proxy server a Docker setup is provided based
on the [`Dante`](https://www.inet.no/dante/) SOCKS server.
A container will start `danted` listening on port 1080. The docker-compose.yml
will start the container and map the ports appropriately. To start the container
in the background:
```
docker-compose -f docker/docker-compose.yml up -d
```
To stop it:
```
docker-compose -f docker/docker-compose.yml down
```
Alternatively, remove the `-d` flag to run the containers in the foreground.
## Reference documents
Each implementation follows the documents as listed below:
- SOCKS4: https://www.openssh.com/txt/socks4.protocol
- SOCKS4A: https://www.openssh.com/txt/socks4a.protocol
- SOCKS5: https://www.ietf.org/rfc/rfc1928.txt
- SOCKS5 username/password authentication: https://www.ietf.org/rfc/rfc1929.txt
- SOCKS5 GSS-API authentication: https://www.ietf.org/rfc/rfc1961.txt
## License
MIT
socksio-1.0.0/docker/ 0000775 0000000 0000000 00000000000 13646347054 0014445 5 ustar 00root root 0000000 0000000 socksio-1.0.0/docker/Dockerfile 0000664 0000000 0000000 00000000377 13646347054 0016446 0 ustar 00root root 0000000 0000000 FROM ubuntu:bionic
# Add a fake user for Dante to pick up credentials for
RUN useradd -s /sbin/nologin socksio
RUN echo 'socksio:socksio' | chpasswd
RUN apt-get update && apt-get install -y dante-server
COPY danted.conf /etc/
EXPOSE 1080
CMD [ "danted" ]
socksio-1.0.0/docker/danted.conf 0000664 0000000 0000000 00000000535 13646347054 0016556 0 ustar 00root root 0000000 0000000 logoutput: stderr
internal: 0.0.0.0 port = 1080
external: eth0
socksmethod: username none
client pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
log: error connect disconnect
}
socks pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
protocol: tcp udp
command: bind connect udpassociate
log: error connect disconnect
socksmethod: username none
}
socksio-1.0.0/docker/docker-compose.yml 0000664 0000000 0000000 00000000117 13646347054 0020101 0 ustar 00root root 0000000 0000000 version: "3.7"
services:
dante:
build: .
ports:
- "1080:1080"
socksio-1.0.0/docs/ 0000775 0000000 0000000 00000000000 13646347054 0014126 5 ustar 00root root 0000000 0000000 socksio-1.0.0/docs/Makefile 0000664 0000000 0000000 00000001201 13646347054 0015560 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?= -W
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
socksio-1.0.0/docs/source/ 0000775 0000000 0000000 00000000000 13646347054 0015426 5 ustar 00root root 0000000 0000000 socksio-1.0.0/docs/source/api_socks4.rst 0000664 0000000 0000000 00000001277 13646347054 0020226 0 ustar 00root root 0000000 0000000 .. _SOCKS4-API-documentation:
.. currentmodule:: socksio.socks4
SOCKS4 and SOCKS4A API documentation
====================================
SOCKS4 and SOCKS4A are almost identical protocols, as such the API is implemented
in a single module and most components are shared.
The only practical difference is the usage of a :class:`SOCKS4Request` versus
:class:`SOCKS4ARequest`.
Remember SOCKS4 allows only for IPv4 addresses and SOCKS4A supports domain names.
Neither support IPv6.
.. autoclass:: SOCKS4Connection
:members:
.. autoclass:: SOCKS4Request
:members: from_address, dumps
.. autoclass:: SOCKS4ARequest
:members: from_address, dumps
.. autoclass:: SOCKS4Reply
:members: loads
socksio-1.0.0/docs/source/api_socks5.rst 0000664 0000000 0000000 00000001022 13646347054 0020213 0 ustar 00root root 0000000 0000000 .. _SOCKS5-API-documentation:
.. currentmodule:: socksio.socks5
SOCKS5 API documentation
====================================
.. autoclass:: SOCKS5Connection
:members:
.. autoclass:: SOCKS5AuthMethodsRequest
:members: dumps
.. autoclass:: SOCKS5AuthReply
:members: loads
.. autoclass:: SOCKS5UsernamePasswordRequest
:members: dumps
.. autoclass:: SOCKS5UsernamePasswordReply
:members: loads
.. autoclass:: SOCKS5CommandRequest
:members: from_address, dumps
.. autoclass:: SOCKS5Reply
:members: loads
socksio-1.0.0/docs/source/conf.py 0000664 0000000 0000000 00000004042 13646347054 0016725 0 ustar 00root root 0000000 0000000 # Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import datetime
import socksio
# -- Project information -----------------------------------------------------
project = "socksio"
copyright = "{}, Seth Michael Larson".format(datetime.date.today().year)
author = "Seth Michael Larson"
# The full version, including alpha/beta/rc tags
release = socksio.__version__
# -- General configuration ---------------------------------------------------
master_doc = "index"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
html_theme_path = ["_themes"]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
socksio-1.0.0/docs/source/development.rst 0000664 0000000 0000000 00000002027 13646347054 0020503 0 ustar 00root root 0000000 0000000 Development
-----------
Install the test requirements with
``pip install -r test-requirements.txt``.
Install the project in pseudo-editable mode with ``flit install -s``.
Tests can be ran directly invoking ``pytest``.
This project uses `nox `_ to
automate testing and linting tasks. ``nox`` is installed as part of the
test requirements. Invoking ``nox`` will run all sessions, but you may
also run only some them, for example ``nox -s lint`` will only run the
linting session.
In order to test against a live proxy server a Docker setup is provided
based on the `Dante `_ SOCKS server.
A container will start ``danted`` listening on port 1080. The
docker-compose.yml will start the container and map the ports
appropriately. To start the container in the background:
::
docker-compose -f docker/docker-compose.yml up -d
To stop it:
::
docker-compose -f docker/docker-compose.yml down
Alternatively, remove the ``-d`` flag to run the containers in the
foreground.
socksio-1.0.0/docs/source/index.rst 0000664 0000000 0000000 00000002232 13646347054 0017266 0 ustar 00root root 0000000 0000000 socksio: Client-side sans-I/O SOCKS proxy implementation
========================================================
``socksio`` is a sans-I/O library similar to
`h11 `_ or
`h2 `_, this means the
library itself does not handle the actual sending of the bytes through
the network, it only deals with the implementation details of the SOCKS
protocols so you can use it in any I/O library you want.
Current status: stable
----------------------
Features not yet implemented:
- SOCKS5 GSS-API authentication.
- SOCKS5 UDP associate requests.
Contents
--------
.. toctree::
:maxdepth: 2
usage.rst
development.rst
api_socks4.rst
api_socks5.rst
Reference documents
-------------------
Each implementation follows the documents as listed below:
- SOCKS4: https://www.openssh.com/txt/socks4.protocol
- SOCKS4A: https://www.openssh.com/txt/socks4a.protocol
- SOCKS5: https://www.ietf.org/rfc/rfc1928.txt
- SOCKS5 username/password authentication:
https://www.ietf.org/rfc/rfc1929.txt
- SOCKS5 GSS-API authentication: https://www.ietf.org/rfc/rfc1961.txt
License
-------
MIT
socksio-1.0.0/docs/source/usage.rst 0000664 0000000 0000000 00000006352 13646347054 0017272 0 ustar 00root root 0000000 0000000 Usage
-----
.. currentmodule:: socksio
TL;DR check the `examples directory
`_.
Being sans-I/O means that in order to test ``socksio`` you need an I/O
library. And the most basic I/O is, of course, the standard library’s
``socket`` module.
You’ll need to know ahead of time the type of SOCKS proxy you want to
connect to. Assuming we have a SOCKS4 proxy running in our machine on
port 8080, we will first create a connection to it:
.. code:: python
import socket
sock = socket.create_connection(("localhost", 8080))
``socksio`` exposes modules for SOCKS4, SOCKS4A and SOCKS5, each of them
includes a ``Connection`` class:
.. code:: python
from socksio import socks4
# The SOCKS4 protocol requires a `user_id` to be supplied.
conn = socks4.SOCKS4Connection(user_id=b"socksio")
Since ``socksio`` is a sans-I/O library, we will use the socket to send
and receive data to our SOCKS4 proxy. The raw data, however, will be
created and parsed by our :class:`SOCKS4Connection `.
We need to tell our connection we want to make a request to the proxy.
We do that by first creating a request object.
In SOCKS4 we only need to send a command along with an IP address and
port. ``socksio`` exposes the different types of commands as enumerables
and a convenience :meth:`~socks4.SOCKS4Request.from_address`
class method in the request classes to create a valid request object:
.. code:: python
# SOCKS4 does not allow domain names, below is an IP for google.com
request = socks4.SOCKS4Request.from_address(
socks4.SOCKS4Command.CONNECT, ("216.58.204.78", 80))
``from_address`` methods are available on all request classes in
``socksio``, they accept addresses as tuples of ``(address, port)`` as
well as string ``address:port``.
Now we ask the connection to send our request:
.. code:: python
conn.send(request)
The :class:`SOCKS4Connection ` will then compose the
necessary ``bytes`` in the proper format for us to send to our proxy:
.. code:: python
data = conn.data_to_send()
sock.sendall(data)
If all goes well the proxy will have sent reply, we just need to read
from the socket and pass the data to the
:class:`SOCKS4Connection `:
.. code:: python
data = sock.recv(1024)
event = conn.receive_data(data)
The connection will parse the data and return an event from it, in this
case, a :class:`SOCKS4Reply ` that includes attributes for
the fields in the SOCKS reply:
.. code:: python
if event.reply_code != socks4.SOCKS4ReplyCode.REQUEST_GRANTED:
raise Exception(
"Server could not connect to remote host: {}".format(event.reply_code)
)
If all went well the connection has been established correctly and we
can start sending our request directly to the proxy:
.. code:: python
sock.sendall(b"GET / HTTP/1.1\r\nhost: google.com\r\n\r\n")
data = receive_data(sock)
print(data)
# b'HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.google.com/...`
The same methodology is used for all protocols, check out the
`examples directory `_
for more information.
socksio-1.0.0/examples/ 0000775 0000000 0000000 00000000000 13646347054 0015014 5 ustar 00root root 0000000 0000000 socksio-1.0.0/examples/example_socks4.py 0000664 0000000 0000000 00000002305 13646347054 0020307 0 ustar 00root root 0000000 0000000 import socket
from socksio import socks4
def send_data(sock, data):
print("sending:", data)
sock.sendall(data)
def receive_data(sock):
data = sock.recv(1024)
print("received:", data)
return data
def main():
# Assuming a running SOCKS4 proxy running in localhost:1080
sock = socket.create_connection(("localhost", 1080))
conn = socks4.SOCKS4Connection(user_id=b"socksio")
# Request to connect to google.com port 80
# SOCKS4 does not allow domain names, below is an IP for google.com
request = socks4.SOCKS4Request.from_address(
socks4.SOCKS4Command.CONNECT, ("216.58.204.78", 80) # or "216.58.204.78:80"
)
conn.send(request)
send_data(sock, conn.data_to_send())
data = receive_data(sock)
event = conn.receive_data(data)
print("Request reply:", event)
if event.reply_code != socks4.SOCKS4ReplyCode.REQUEST_GRANTED:
raise Exception(
"Server could not connect to remote host: {}".format(event.reply_code)
)
# Send an HTTP request to the connected proxy
sock.sendall(b"GET / HTTP/1.1\r\nhost: google.com\r\n\r\n")
data = receive_data(sock)
print(data)
if __name__ == "__main__":
main()
socksio-1.0.0/examples/example_socks4a.py 0000664 0000000 0000000 00000002137 13646347054 0020453 0 ustar 00root root 0000000 0000000 import socket
from socksio import socks4
def send_data(sock, data):
print("sending:", data)
sock.sendall(data)
def receive_data(sock):
data = sock.recv(1024)
print("received:", data)
return data
def main():
# Assuming a running SOCKS4 proxy running in localhost:1080
sock = socket.create_connection(("localhost", 1080))
conn = socks4.SOCKS4Connection(user_id=b"socksio")
# Request to connect to google.com port 80
request = socks4.SOCKS4ARequest.from_address(
socks4.SOCKS4Command.CONNECT, "google.com:80"
)
conn.send(request)
send_data(sock, conn.data_to_send())
data = receive_data(sock)
event = conn.receive_data(data)
print("Request reply:", event)
if event.reply_code != socks4.SOCKS4ReplyCode.REQUEST_GRANTED:
raise Exception(
"Server could not connect to remote host: {}".format(event.reply_code)
)
# Send an HTTP request to the connected proxy
sock.sendall(b"GET / HTTP/1.1\r\nhost: google.com\r\n\r\n")
data = receive_data(sock)
print(data)
if __name__ == "__main__":
main()
socksio-1.0.0/examples/example_socks5.py 0000664 0000000 0000000 00000003734 13646347054 0020317 0 ustar 00root root 0000000 0000000 import socket
from socksio import socks5
def send_data(sock, data):
print("Sending:", data)
sock.sendall(data)
def receive_data(sock):
data = sock.recv(1024)
print("Received:", data)
return data
def main():
# Assuming a running SOCKS5 proxy running in localhost:1080
sock = socket.create_connection(("localhost", 1080))
conn = socks5.SOCKS5Connection()
# The proxy may return any of these options
request = socks5.SOCKS5AuthMethodsRequest(
[
socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED,
socks5.SOCKS5AuthMethod.USERNAME_PASSWORD,
]
)
conn.send(request)
send_data(sock, conn.data_to_send())
data = receive_data(sock)
event = conn.receive_data(data)
print("Auth reply:", event)
# If the proxy requires username/password you'll have to edit them below
if event.method == socks5.SOCKS5AuthMethod.USERNAME_PASSWORD:
request = socks5.SOCKS5UsernamePasswordRequest(b"socksio", b"socksio")
conn.send(request)
send_data(sock, conn.data_to_send())
data = receive_data(sock)
event = conn.receive_data(data)
print("User/pass auth reply:", event)
if not event.success:
raise Exception("Invalid username/password")
# Request to connect to google.com port 80
request = socks5.SOCKS5CommandRequest.from_address(
socks5.SOCKS5Command.CONNECT, ("google.com", 80)
)
conn.send(request)
send_data(sock, conn.data_to_send())
data = receive_data(sock)
event = conn.receive_data(data)
print("Request reply:", event)
if event.reply_code != socks5.SOCKS5ReplyCode.SUCCEEDED:
raise Exception(
"Server could not connect to remote host: {}".format(event.reply_code)
)
# Send an HTTP request to the connected proxy
sock.sendall(b"GET / HTTP/1.1\r\nhost: google.com\r\n\r\n")
data = receive_data(sock)
print("Response", data)
if __name__ == "__main__":
main()
socksio-1.0.0/mypy.ini 0000664 0000000 0000000 00000000232 13646347054 0014672 0 ustar 00root root 0000000 0000000 # mypy does not support pyproject.toml yet
# https://github.com/python/mypy/issues/5205
[mypy]
disallow_untyped_defs = true
ignore_missing_imports = true
socksio-1.0.0/noxfile.py 0000664 0000000 0000000 00000002543 13646347054 0015220 0 ustar 00root root 0000000 0000000 import nox
nox.options.stop_on_first_error = True
source_files = ("socksio", "tests/", "noxfile.py", "examples/", "docs/source/")
@nox.session()
def lint(session):
session.install("autoflake", "black", "flake8", "isort", "seed-isort-config")
session.run("autoflake", "--in-place", "--recursive", *source_files)
session.run("seed-isort-config", "--application-directories=socksio")
session.run("isort", "--project=socksio", "--recursive", "--apply", *source_files)
session.run("black", "--target-version=py36", *source_files)
check(session)
@nox.session(reuse_venv=True)
def check(session):
session.install(
"black", "flake8", "flake8-bugbear", "flake8-comprehensions", "mypy", "isort"
)
session.run(
"isort", "--project=socksio", "--recursive", "--check-only", *source_files
)
session.run("black", "--check", "--diff", "--target-version=py36", *source_files)
session.run("flake8", *source_files)
session.run("mypy", "--strict", "socksio")
@nox.session(python=["3.6", "3.7", "3.8"])
def test(session):
session.install("-r", "test-requirements.txt")
session.run("python", "-m", "pytest", *session.posargs)
@nox.session(reuse_venv=True)
def docs(session):
session.install("sphinx", "sphinx_rtd_theme", ".")
session.run("sphinx-build", "-b", "html", "docs/source/", "docs/build/html/")
socksio-1.0.0/pyproject.toml 0000664 0000000 0000000 00000001532 13646347054 0016113 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["flit_core >=2,<3"]
build-backend = "flit_core.buildapi"
[tool.flit.metadata]
module = "socksio"
author = "Seth Michael Larson"
author-email = "sethmichaellarson@gmail.com"
home-page = "https://github.com/sethmlarson/socksio"
requires-python=">=3.6"
description-file="README.md"
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Internet :: Proxy Servers",
]
[tool.isort]
combine_as_imports = true
force_grid_wrap = 0
include_trailing_comma = true
known_first_party = "socksio,tests"
known_third_party = ["nox", "pytest", "socksio"]
line_length = 88
multi_line_output = 3
socksio-1.0.0/pytest.ini 0000664 0000000 0000000 00000000264 13646347054 0015231 0 ustar 00root root 0000000 0000000 # pytest does not support pyproject.toml yet
# https://github.com/pytest-dev/pytest/issues/1556
[pytest]
addopts = --cov=socksio --cov=tests --cov-branch --cov-report=term-missing
socksio-1.0.0/socksio/ 0000775 0000000 0000000 00000000000 13646347054 0014650 5 ustar 00root root 0000000 0000000 socksio-1.0.0/socksio/__init__.py 0000664 0000000 0000000 00000001706 13646347054 0016765 0 ustar 00root root 0000000 0000000 """Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5."""
from .exceptions import ProtocolError, SOCKSError
from .socks4 import (
SOCKS4ARequest,
SOCKS4Command,
SOCKS4Connection,
SOCKS4Reply,
SOCKS4ReplyCode,
SOCKS4Request,
)
from .socks5 import (
SOCKS5AType,
SOCKS5AuthMethod,
SOCKS5AuthMethodsRequest,
SOCKS5AuthReply,
SOCKS5Command,
SOCKS5CommandRequest,
SOCKS5Connection,
SOCKS5Reply,
SOCKS5ReplyCode,
SOCKS5UsernamePasswordRequest,
)
__version__ = "1.0.0"
__all__ = [
"SOCKS4Request",
"SOCKS4ARequest",
"SOCKS4Reply",
"SOCKS4Connection",
"SOCKS4Command",
"SOCKS4ReplyCode",
"SOCKS5AType",
"SOCKS5AuthMethodsRequest",
"SOCKS5AuthReply",
"SOCKS5AuthMethod",
"SOCKS5Connection",
"SOCKS5Command",
"SOCKS5CommandRequest",
"SOCKS5ReplyCode",
"SOCKS5Reply",
"SOCKS5UsernamePasswordRequest",
"SOCKSError",
"ProtocolError",
]
socksio-1.0.0/socksio/_types.py 0000664 0000000 0000000 00000000065 13646347054 0016526 0 ustar 00root root 0000000 0000000 import typing
StrOrBytes = typing.Union[str, bytes]
socksio-1.0.0/socksio/compat.py 0000664 0000000 0000000 00000006744 13646347054 0016520 0 ustar 00root root 0000000 0000000 """Backport of @functools.singledispatchmethod to Python <3.7.
Adapted from https://github.com/ikalnytskyi/singledispatchmethod
removing 2.7 specific code.
"""
import functools
import typing
if hasattr(functools, "singledispatchmethod"): # pragma: nocover
singledispatchmethod = functools.singledispatchmethod # type: ignore
else:
update_wrapper = functools.update_wrapper
singledispatch = functools.singledispatch
# The type: ignore below is to avoid mypy erroring due to a
# "already defined" singledispatchmethod, oddly this does not
# happen when using `if sys.version_info >= (3, 8)`
class singledispatchmethod(object): # type: ignore
"""Single-dispatch generic method descriptor.
TODO: Figure out how to type this:
`mypy --strict` returns errors like the following for all decorated methods:
"Untyped decorator makes function "send" untyped."
But this is not a normal function-base decorator, it's a class and it
doesn't have a __call__ method. When decorating the "base" method
__init__ is called, but of course its return type is None.
"""
def __init__(self, func: typing.Callable[..., typing.Any]) -> None:
if not callable(func) and not hasattr(func, "__get__"):
raise TypeError("{!r} is not callable or a descriptor".format(func))
self.dispatcher = singledispatch(func)
self.func = func
def register(
self,
cls: typing.Callable[..., typing.Any],
method: typing.Optional[typing.Callable[..., typing.Any]] = None,
) -> typing.Callable[..., typing.Any]:
"""Register a method on a class for a particular type.
Note in Python <= 3.6 this methods cannot infer the type from the
argument's type annotation, users *must* supply it manually on
decoration, i.e.
@my_method.register(TypeToDispatch)
def _(self, arg: TypeToDispatch) -> None:
...
Versus in Python 3.7+:
@my_method.register
def _(self, arg: TypeToDispatch) -> None:
...
"""
# mypy wants method to be non-optional, but it is required to be
# for decoration to work correctly in our case.
# https://github.com/python/cpython/blob/3.8/Lib/functools.py#L887-L920
# is not type annotated either.
return self.dispatcher.register(cls, func=method) # type: ignore
def __get__(
self, obj: typing.Any, cls: typing.Callable[[typing.Any], typing.Any]
) -> typing.Callable[..., typing.Any]:
def _method(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
method = self.dispatcher.dispatch(args[0].__class__) # type: typing.Any
return method.__get__(obj, cls)(*args, **kwargs)
# The type: ignore below is due to `_method` being given a strict
# "Callable[[VarArg(Any), KwArg(Any)], Any]" which causes a
# 'has no attribute "__isabstractmethod__" error'
# felt safe enough to ignore
_method.__isabstractmethod__ = self.__isabstractmethod__ # type: ignore
_method.register = self.register # type: ignore
update_wrapper(_method, self.func)
return _method
@property
def __isabstractmethod__(self) -> typing.Any:
return getattr(self.func, "__isabstractmethod__", False)
socksio-1.0.0/socksio/exceptions.py 0000664 0000000 0000000 00000000203 13646347054 0017376 0 ustar 00root root 0000000 0000000 class SOCKSError(Exception):
"""Generic exception for when something goes wrong"""
class ProtocolError(SOCKSError):
pass
socksio-1.0.0/socksio/py.typed 0000664 0000000 0000000 00000000000 13646347054 0016335 0 ustar 00root root 0000000 0000000 socksio-1.0.0/socksio/socks4.py 0000664 0000000 0000000 00000017236 13646347054 0016441 0 ustar 00root root 0000000 0000000 import enum
import typing
from ._types import StrOrBytes
from .exceptions import ProtocolError, SOCKSError
from .utils import (
AddressType,
decode_address,
encode_address,
get_address_port_tuple_from_address,
)
class SOCKS4ReplyCode(bytes, enum.Enum):
"""Enumeration of SOCKS4 reply codes."""
REQUEST_GRANTED = b"\x5A"
REQUEST_REJECTED_OR_FAILED = b"\x5B"
CONNECTION_FAILED = b"\x5C"
AUTHENTICATION_FAILED = b"\x5D"
class SOCKS4Command(bytes, enum.Enum):
"""Enumeration of SOCKS4 command codes."""
CONNECT = b"\x01"
BIND = b"\x02"
class SOCKS4Request(typing.NamedTuple):
"""Encapsulates a request to the SOCKS4 proxy server
Args:
command: The command to request.
port: The port number to connect to on the target host.
addr: IP address of the target host.
user_id: Optional user ID to be included in the request, if not supplied
the user *must* provide one in the packing operation.
"""
command: SOCKS4Command
port: int
addr: bytes
user_id: typing.Optional[bytes] = None
@classmethod
def from_address(
cls,
command: SOCKS4Command,
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]],
user_id: typing.Optional[bytes] = None,
) -> "SOCKS4Request":
"""Convenience class method to build an instance from command and address.
Args:
command: The command to request.
address: A string in the form 'HOST:PORT' or a tuple of ip address string
and port number.
user_id: Optional user ID.
Returns:
A SOCKS4Request instance.
Raises:
SOCKSError: If a domain name or IPv6 address was supplied.
"""
address, port = get_address_port_tuple_from_address(address)
atype, encoded_addr = encode_address(address)
if atype != AddressType.IPV4:
raise SOCKSError(
"IPv6 addresses and domain names are not supported by SOCKS4"
)
return cls(command=command, addr=encoded_addr, port=port, user_id=user_id)
def dumps(self, user_id: typing.Optional[bytes] = None) -> bytes:
"""Packs the instance into a raw binary in the appropriate form.
Args:
user_id: Optional user ID as an override, if not provided the instance's
will be used, if none was provided at initialization an error is raised.
Returns:
The packed request.
Raises:
SOCKSError: If no user was specified in this call or on initialization.
"""
user_id = user_id or self.user_id
if user_id is None:
raise SOCKSError("SOCKS4 requires a user_id, none was specified")
return b"".join(
[
b"\x04",
self.command,
(self.port).to_bytes(2, byteorder="big"),
self.addr,
user_id,
b"\x00",
]
)
class SOCKS4ARequest(typing.NamedTuple):
"""Encapsulates a request to the SOCKS4A proxy server
Args:
command: The command to request.
port: The port number to connect to on the target host.
addr: IP address of the target host.
user_id: Optional user ID to be included in the request, if not supplied
the user *must* provide one in the packing operation.
"""
command: SOCKS4Command
port: int
addr: bytes
user_id: typing.Optional[bytes] = None
@classmethod
def from_address(
cls,
command: SOCKS4Command,
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]],
user_id: typing.Optional[bytes] = None,
) -> "SOCKS4ARequest":
"""Convenience class method to build an instance from command and address.
Args:
command: The command to request.
address: A string in the form 'HOST:PORT' or a tuple of ip address string
and port number.
user_id: Optional user ID.
Returns:
A SOCKS4ARequest instance.
"""
address, port = get_address_port_tuple_from_address(address)
atype, encoded_addr = encode_address(address)
return cls(command=command, addr=encoded_addr, port=port, user_id=user_id)
def dumps(self, user_id: typing.Optional[bytes] = None) -> bytes:
"""Packs the instance into a raw binary in the appropriate form.
Args:
user_id: Optional user ID as an override, if not provided the instance's
will be used, if none was provided at initialization an error is raised.
Returns:
The packed request.
Raises:
SOCKSError: If no user was specified in this call or on initialization.
"""
user_id = user_id or self.user_id
if user_id is None:
raise SOCKSError("SOCKS4 requires a user_id, none was specified")
return b"".join(
[
b"\x04",
self.command,
(self.port).to_bytes(2, byteorder="big"),
b"\x00\x00\x00\xFF", # arbitrary final non-zero byte
user_id,
b"\x00",
self.addr,
b"\x00",
]
)
class SOCKS4Reply(typing.NamedTuple):
"""Encapsulates a reply from the SOCKS4 proxy server
Args:
reply_code: The code representing the type of reply.
port: The port number returned.
addr: Optional IP address returned.
"""
reply_code: SOCKS4ReplyCode
port: int
addr: typing.Optional[str]
@classmethod
def loads(cls, data: bytes) -> "SOCKS4Reply":
"""Unpacks the reply data into an instance.
Returns:
The unpacked reply instance.
Raises:
ProtocolError: If the data does not match the spec.
"""
if len(data) != 8 or data[0:1] != b"\x00":
raise ProtocolError("Malformed reply")
try:
return cls(
reply_code=SOCKS4ReplyCode(data[1:2]),
port=int.from_bytes(data[2:4], byteorder="big"),
addr=decode_address(AddressType.IPV4, data[4:8]),
)
except ValueError as exc:
raise ProtocolError("Malformed reply") from exc
class SOCKS4Connection:
"""Encapsulates a SOCKS4 and SOCKS4A connection.
Packs request objects into data suitable to be send and unpacks reply
data into their appropriate reply objects.
Args:
user_id: The user ID to be sent as part of the requests.
"""
def __init__(self, user_id: bytes):
self.user_id = user_id
self._data_to_send = bytearray()
self._received_data = bytearray()
def send(self, request: typing.Union[SOCKS4Request, SOCKS4ARequest]) -> None:
"""Packs a request object and adds it to the send data buffer.
Args:
request: The request instance to be packed.
"""
user_id = request.user_id or self.user_id
self._data_to_send += request.dumps(user_id=user_id)
def receive_data(self, data: bytes) -> SOCKS4Reply:
"""Unpacks response data into a reply object.
Args:
data: The raw response data from the proxy server.
Returns:
The appropriate reply object.
"""
self._received_data += data
return SOCKS4Reply.loads(bytes(self._received_data))
def data_to_send(self) -> bytes:
"""Returns the data to be sent via the I/O library of choice.
Also clears the connection's buffer.
"""
data = bytes(self._data_to_send)
self._data_to_send = bytearray()
return data
socksio-1.0.0/socksio/socks5.py 0000664 0000000 0000000 00000027453 13646347054 0016444 0 ustar 00root root 0000000 0000000 import enum
import typing
from ._types import StrOrBytes
from .compat import singledispatchmethod
from .exceptions import ProtocolError
from .utils import (
AddressType,
decode_address,
encode_address,
get_address_port_tuple_from_address,
)
class SOCKS5AuthMethod(bytes, enum.Enum):
"""Enumeration of SOCKS5 authentication methods."""
NO_AUTH_REQUIRED = b"\x00"
GSSAPI = b"\x01"
USERNAME_PASSWORD = b"\x02"
NO_ACCEPTABLE_METHODS = b"\xFF"
class SOCKS5Command(bytes, enum.Enum):
"""Enumeration of SOCKS5 commands."""
CONNECT = b"\x01"
BIND = b"\x02"
UDP_ASSOCIATE = b"\x03"
class SOCKS5AType(bytes, enum.Enum):
"""Enumeration of SOCKS5 address types."""
IPV4_ADDRESS = b"\x01"
DOMAIN_NAME = b"\x03"
IPV6_ADDRESS = b"\x04"
@classmethod
def from_atype(cls, atype: AddressType) -> "SOCKS5AType":
if atype == AddressType.IPV4:
return SOCKS5AType.IPV4_ADDRESS
elif atype == AddressType.DN:
return SOCKS5AType.DOMAIN_NAME
elif atype == AddressType.IPV6:
return SOCKS5AType.IPV6_ADDRESS
raise ValueError(atype)
class SOCKS5ReplyCode(bytes, enum.Enum):
"""Enumeration of SOCKS5 reply codes."""
SUCCEEDED = b"\x00"
GENERAL_SERVER_FAILURE = b"\x01"
CONNECTION_NOT_ALLOWED_BY_RULESET = b"\x02"
NETWORK_UNREACHABLE = b"\x03"
HOST_UNREACHABLE = b"\x04"
CONNECTION_REFUSED = b"\x05"
TTL_EXPIRED = b"\x06"
COMMAND_NOT_SUPPORTED = b"\x07"
ADDRESS_TYPE_NOT_SUPPORTED = b"\x08"
class SOCKS5AuthMethodsRequest(typing.NamedTuple):
"""Encapsulates a request to the proxy for available authentication methods.
Args:
methods: A list of acceptable authentication methods.
"""
methods: typing.List[SOCKS5AuthMethod]
def dumps(self) -> bytes:
"""Packs the instance into a raw binary in the appropriate form."""
return b"".join(
[
b"\x05",
len(self.methods).to_bytes(1, byteorder="big"),
b"".join(self.methods),
]
)
class SOCKS5AuthReply(typing.NamedTuple):
"""Encapsulates a reply from the proxy with the authentication method to be used.
Args:
method: The authentication method to be used.
Raises:
ProtocolError: If the data does not conform with the expected structure.
"""
method: SOCKS5AuthMethod
@classmethod
def loads(cls, data: bytes) -> "SOCKS5AuthReply":
"""Unpacks the authentication reply data into an instance.
Returns:
The unpacked authentication reply instance.
Raises:
ProtocolError: If the data does not match the spec.
"""
if len(data) != 2:
raise ProtocolError("Malformed reply")
try:
return cls(method=SOCKS5AuthMethod(data[1:2]))
except ValueError as exc:
raise ProtocolError("Malformed reply") from exc
class SOCKS5UsernamePasswordRequest(typing.NamedTuple):
"""Encapsulates a username/password authentication request to the proxy server."""
username: bytes
password: bytes
def dumps(self) -> bytes:
"""Packs the instance into a raw binary in the appropriate form.
Returns:
The packed request.
"""
return b"".join(
[
b"\x01",
len(self.username).to_bytes(1, byteorder="big"),
self.username,
len(self.password).to_bytes(1, byteorder="big"),
self.password,
]
)
class SOCKS5UsernamePasswordReply(typing.NamedTuple):
"""Encapsulates a username/password authentication reply from the proxy server."""
success: bool
@classmethod
def loads(cls, data: bytes) -> "SOCKS5UsernamePasswordReply":
"""Unpacks the reply authentication data into an instance.
Returns:
The unpacked authentication reply instance.
"""
return cls(success=data == b"\x01\x00")
class SOCKS5CommandRequest(typing.NamedTuple):
"""Encapsulates a command request to the proxy server.
Args:
command: The command to request.
atype: The address type of the addr field.
addr: Address of the target host.
port: The port number to connect to on the target host.
"""
command: SOCKS5Command
atype: SOCKS5AType
addr: bytes
port: int
@classmethod
def from_address(
cls,
command: SOCKS5Command,
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]],
) -> "SOCKS5CommandRequest":
"""Convenience class method to build an instance from command and address.
Args:
command: The command to request.
address: A string in the form 'HOST:PORT' or a tuple of ip address string
and port number. The address type will be inferred.
Returns:
A SOCKS5CommandRequest instance.
Raises:
SOCKSError: If a domain name or IPv6 address was supplied.
"""
address, port = get_address_port_tuple_from_address(address)
atype, encoded_addr = encode_address(address)
return cls(
command=command,
atype=SOCKS5AType.from_atype(atype),
addr=encoded_addr,
port=port,
)
def dumps(self) -> bytes:
"""Packs the instance into a raw binary in the appropriate form.
Returns:
The packed request.
"""
return b"".join(
[
b"\x05",
self.command,
b"\x00",
self.atype,
self.packed_addr,
(self.port).to_bytes(2, byteorder="big"),
]
)
@property
def packed_addr(self) -> bytes:
"""Property returning the packed address in the correct form for its type."""
if self.atype == SOCKS5AType.IPV4_ADDRESS:
assert len(self.addr) == 4
return self.addr
elif self.atype == SOCKS5AType.IPV6_ADDRESS:
assert len(self.addr) == 16
return self.addr
else:
length = len(self.addr)
return length.to_bytes(1, byteorder="big") + self.addr
class SOCKS5Reply(typing.NamedTuple):
"""Encapsulates a reply from the SOCKS5 proxy server
Args:
reply_code: The code representing the type of reply.
atype: The address type of the addr field.
addr: Optional IP address returned.
port: The port number returned.
"""
reply_code: SOCKS5ReplyCode
atype: SOCKS5AType
addr: str
port: int
@classmethod
def loads(cls, data: bytes) -> "SOCKS5Reply":
"""Unpacks the reply data into an instance.
Returns:
The unpacked reply instance.
Raises:
ProtocolError: If the data does not match the spec.
"""
if data[0:1] != b"\x05":
raise ProtocolError("Malformed reply")
try:
atype = SOCKS5AType(data[3:4])
return cls(
reply_code=SOCKS5ReplyCode(data[1:2]),
atype=atype,
addr=decode_address(AddressType.from_socks5_atype(atype), data[4:-2]),
port=int.from_bytes(data[-2:], byteorder="big"),
)
except ValueError as exc:
raise ProtocolError("Malformed reply") from exc
class SOCKS5Datagram(typing.NamedTuple):
"""Encapsulates a SOCKS5 datagram for UDP connections.
Currently not implemented.
"""
atype: SOCKS5AType
addr: bytes
port: int
data: bytes
fragment: int
last_fragment: bool
@classmethod
def loads(cls, data: bytes) -> "SOCKS5Datagram":
raise NotImplementedError() # pragma: nocover
def dumps(self) -> bytes:
raise NotImplementedError() # pragma: nocover
class SOCKS5State(enum.IntEnum):
"""Enumeration of SOCKS5 protocol states."""
CLIENT_AUTH_REQUIRED = 1
SERVER_AUTH_REPLY = 2
CLIENT_AUTHENTICATED = 3
TUNNEL_READY = 4
CLIENT_WAITING_FOR_USERNAME_PASSWORD = 5
SERVER_VERIFY_USERNAME_PASSWORD = 6
MUST_CLOSE = 7
SOCKS5RequestType = typing.Union[SOCKS5AuthMethodsRequest, SOCKS5CommandRequest]
class SOCKS5Connection:
"""Encapsulates a SOCKS5 connection.
Packs request objects into data suitable to be send and unpacks reply
data into their appropriate reply objects.
"""
def __init__(self) -> None:
self._data_to_send = bytearray()
self._received_data = bytearray()
self._state = SOCKS5State.CLIENT_AUTH_REQUIRED
@property
def state(self) -> SOCKS5State:
"""Returns the current state of the protocol."""
return self._state
@singledispatchmethod # type: ignore
def send(self, request: SOCKS5RequestType) -> None:
"""Packs a request object and adds it to the send data buffer.
Also progresses the protocol state of the connection.
Args:
request: The request instance to be packed.
"""
raise NotImplementedError() # pragma: nocover
@send.register(SOCKS5AuthMethodsRequest) # type: ignore
def _auth_methods(self, request: SOCKS5AuthMethodsRequest) -> None:
self._data_to_send += request.dumps()
self._state = SOCKS5State.SERVER_AUTH_REPLY
@send.register(SOCKS5UsernamePasswordRequest) # type: ignore
def _auth_username_password(self, request: SOCKS5UsernamePasswordRequest) -> None:
if self._state != SOCKS5State.CLIENT_WAITING_FOR_USERNAME_PASSWORD:
raise ProtocolError("Not currently waiting for username and password")
self._state = SOCKS5State.SERVER_VERIFY_USERNAME_PASSWORD
self._data_to_send += request.dumps()
@send.register(SOCKS5CommandRequest) # type: ignore
def _command(self, request: SOCKS5AuthMethodsRequest) -> None:
if self._state < SOCKS5State.CLIENT_AUTHENTICATED:
raise ProtocolError(
"SOCKS5 connections must be authenticated before sending a request"
)
self._data_to_send += request.dumps()
def receive_data(
self, data: bytes
) -> typing.Union[SOCKS5AuthReply, SOCKS5Reply, SOCKS5UsernamePasswordReply]:
"""Unpacks response data into a reply object.
Args:
data: The raw response data from the proxy server.
Returns:
A reply instance corresponding to the connection state and reply data.
"""
if self._state == SOCKS5State.SERVER_AUTH_REPLY:
auth_reply = SOCKS5AuthReply.loads(data)
if auth_reply.method == SOCKS5AuthMethod.USERNAME_PASSWORD:
self._state = SOCKS5State.CLIENT_WAITING_FOR_USERNAME_PASSWORD
elif auth_reply.method == SOCKS5AuthMethod.NO_AUTH_REQUIRED:
self._state = SOCKS5State.CLIENT_AUTHENTICATED
return auth_reply
if self._state == SOCKS5State.SERVER_VERIFY_USERNAME_PASSWORD:
username_password_reply = SOCKS5UsernamePasswordReply.loads(data)
if username_password_reply.success:
self._state = SOCKS5State.CLIENT_AUTHENTICATED
else:
self._state = SOCKS5State.MUST_CLOSE
return username_password_reply
if self._state == SOCKS5State.CLIENT_AUTHENTICATED:
reply = SOCKS5Reply.loads(data)
if reply.reply_code == SOCKS5ReplyCode.SUCCEEDED:
self._state = SOCKS5State.TUNNEL_READY
else:
self._state = SOCKS5State.MUST_CLOSE
return reply
raise NotImplementedError() # pragma: nocover
def data_to_send(self) -> bytes:
"""Returns the data to be sent via the I/O library of choice.
Also clears the connection's buffer.
"""
data = bytes(self._data_to_send)
self._data_to_send = bytearray()
return data
socksio-1.0.0/socksio/utils.py 0000664 0000000 0000000 00000006242 13646347054 0016366 0 ustar 00root root 0000000 0000000 import enum
import functools
import re
import socket
import typing
from ._types import StrOrBytes
if typing.TYPE_CHECKING:
from socksio.socks5 import SOCKS5AType # pragma: nocover
IP_V6_WITH_PORT_REGEX = re.compile(r"^\[(?P[^\]]+)\]:(?P\d+)$")
class AddressType(enum.Enum):
IPV4 = "IPV4"
IPV6 = "IPV6"
DN = "DN"
@classmethod
def from_socks5_atype(cls, socks5atype: "SOCKS5AType") -> "AddressType":
from socksio.socks5 import SOCKS5AType
if socks5atype == SOCKS5AType.IPV4_ADDRESS:
return AddressType.IPV4
elif socks5atype == SOCKS5AType.DOMAIN_NAME:
return AddressType.DN
elif socks5atype == SOCKS5AType.IPV6_ADDRESS:
return AddressType.IPV6
raise ValueError(socks5atype)
@functools.lru_cache(maxsize=64)
def encode_address(addr: StrOrBytes) -> typing.Tuple[AddressType, bytes]:
"""Determines the type of address and encodes it into the format SOCKS expects"""
addr = addr.decode() if isinstance(addr, bytes) else addr
try:
return AddressType.IPV6, socket.inet_pton(socket.AF_INET6, addr)
except OSError:
try:
return AddressType.IPV4, socket.inet_pton(socket.AF_INET, addr)
except OSError:
return AddressType.DN, addr.encode()
@functools.lru_cache(maxsize=64)
def decode_address(address_type: AddressType, encoded_addr: bytes) -> str:
"""Decodes the address from a SOCKS reply"""
if address_type == AddressType.IPV6:
return socket.inet_ntop(socket.AF_INET6, encoded_addr)
elif address_type == AddressType.IPV4:
return socket.inet_ntop(socket.AF_INET, encoded_addr)
else:
assert address_type == AddressType.DN
return encoded_addr.decode()
def split_address_port_from_string(address: StrOrBytes) -> typing.Tuple[str, int]:
"""Returns a tuple (address: str, port: int) from an address string with a port
i.e. '127.0.0.1:8080', '[0:0:0:0:0:0:0:1]:3080' or 'localhost:8080'.
Note no validation is done on the domain or IP itself.
"""
address = address.decode() if isinstance(address, bytes) else address
match = re.match(IP_V6_WITH_PORT_REGEX, address)
if match:
address, str_port = match.group("address"), match.group("port")
else:
address, _, str_port = address.partition(":")
try:
return address, int(str_port)
except ValueError:
raise ValueError(
"Invalid address + port. Please supply a valid domain name, IPV4 or IPV6 "
"address with the port as a suffix, i.e. `127.0.0.1:3080`, "
"`[0:0:0:0:0:0:0:1]:3080` or `localhost:3080`"
) from None
def get_address_port_tuple_from_address(
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]]
) -> typing.Tuple[str, int]:
"""Returns an (address, port) from an address string-like or tuple."""
if isinstance(address, tuple):
address, port = address
if isinstance(address, bytes):
address = address.decode()
if isinstance(port, (str, bytes)):
port = int(port)
else:
address, port = split_address_port_from_string(address)
return address, port
socksio-1.0.0/test-requirements.txt 0000664 0000000 0000000 00000000130 13646347054 0017431 0 ustar 00root root 0000000 0000000 black
flake8
flake8-bugbear
flake8-comprehensions
flit
isort
mypy
nox
pytest
pytest-cov
socksio-1.0.0/tests/ 0000775 0000000 0000000 00000000000 13646347054 0014340 5 ustar 00root root 0000000 0000000 socksio-1.0.0/tests/run_acceptance_tests.sh 0000775 0000000 0000000 00000000404 13646347054 0021071 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -exo
docker-compose -f docker/docker-compose.yml up -d
# TODO: SOCKS4A doesn't seem to work correctly in Dante
# Wondering if this is a Dante bug? PySocks also doesn't work.
python examples/example_socks4.py
python examples/example_socks5.py
socksio-1.0.0/tests/test_compat.py 0000664 0000000 0000000 00000001166 13646347054 0017240 0 ustar 00root root 0000000 0000000 import pytest
from socksio.compat import singledispatchmethod
def test_singledispatchmethod():
class Foo:
@singledispatchmethod
def bar():
raise NotImplementedError() # pragma: nocover
@bar.register(str)
def _(self, arg: str) -> None:
return f"<< {arg} >>"
@bar.register(int)
def _(self, arg: int) -> None:
return f"|| {arg} ||"
obj = Foo()
assert obj.bar("bar") == "<< bar >>"
assert obj.bar(1) == "|| 1 ||"
def test_singledispatchmethod_error():
with pytest.raises(TypeError):
singledispatchmethod("error")
socksio-1.0.0/tests/test_socks4.py 0000664 0000000 0000000 00000011255 13646347054 0017163 0 ustar 00root root 0000000 0000000 import pytest
from socksio import (
ProtocolError,
SOCKS4ARequest,
SOCKS4Command,
SOCKS4Connection,
SOCKS4Reply,
SOCKS4ReplyCode,
SOCKS4Request,
SOCKSError,
)
@pytest.mark.parametrize(
"address,expected_address,expected_port",
[
(("127.0.0.1", 3080), b"\x7f\x00\x00\x01", 3080),
(("127.0.0.1", "3080"), b"\x7f\x00\x00\x01", 3080),
("127.0.0.1:8080", b"\x7f\x00\x00\x01", 8080),
((b"127.0.0.1", 3080), b"\x7f\x00\x00\x01", 3080),
((b"127.0.0.1", b"3080"), b"\x7f\x00\x00\x01", 3080),
(b"127.0.0.1:8080", b"\x7f\x00\x00\x01", 8080),
],
)
def test_socks4request_from_address(address, expected_address, expected_port) -> None:
req = SOCKS4Request.from_address(SOCKS4Command.CONNECT, address, user_id=b"socksio")
assert req.command == SOCKS4Command.CONNECT
assert req.addr == expected_address
assert req.port == expected_port
assert req.user_id == b"socksio"
@pytest.mark.parametrize(
"address,user_id",
[
(("::1", 3080), b"socksio"), # IPV6
("localhost:3080", b"socksio"), # Domain names
],
)
def test_socks4request_from_address_errors(address, user_id) -> None:
with pytest.raises(SOCKSError):
SOCKS4Request.from_address(
command=SOCKS4Command.BIND, address=address, user_id=user_id
)
def test_socks4request_from_address_dump_raises_if_no_user_id():
req = SOCKS4Request.from_address(SOCKS4Command.CONNECT, "127.0.0.1:8080")
with pytest.raises(SOCKSError):
req.dumps()
@pytest.mark.parametrize(
"address,expected_address,expected_port",
[
(("127.0.0.1", 3080), b"\x7f\x00\x00\x01", 3080),
(("127.0.0.1", "3080"), b"\x7f\x00\x00\x01", 3080),
("127.0.0.1:8080", b"\x7f\x00\x00\x01", 8080),
((b"127.0.0.1", 3080), b"\x7f\x00\x00\x01", 3080),
((b"127.0.0.1", b"3080"), b"\x7f\x00\x00\x01", 3080),
(b"127.0.0.1:8080", b"\x7f\x00\x00\x01", 8080),
],
)
def test_socks4arequest_from_address(address, expected_address, expected_port) -> None:
req = SOCKS4ARequest.from_address(
SOCKS4Command.CONNECT, address, user_id=b"socksio"
)
assert req.command == SOCKS4Command.CONNECT
assert req.addr == expected_address
assert req.port == expected_port
assert req.user_id == b"socksio"
def test_socks4arequest_from_address_dump_raises_if_no_user_id():
req = SOCKS4ARequest.from_address(SOCKS4Command.CONNECT, "127.0.0.1:8080")
with pytest.raises(SOCKSError):
req.dumps()
@pytest.mark.parametrize("command", [SOCKS4Command.BIND, SOCKS4Command.CONNECT])
def test_socks4_connection_request(command: SOCKS4Command) -> None:
conn = SOCKS4Connection(user_id=b"socks")
request = SOCKS4Request.from_address(command=command, address=("127.0.0.1", 8080))
conn.send(request)
data = conn.data_to_send()
assert len(data) == 9 + 5
assert data[0:1] == b"\x04"
assert data[1:2] == command
assert data[2:4] == (8080).to_bytes(2, byteorder="big")
assert data[4:8] == b"\x7f\x00\x00\x01"
assert data[8:13] == b"socks"
assert data[13] == 0
@pytest.mark.parametrize("request_reply_code", list(SOCKS4ReplyCode))
def test_socks4_receive_data(request_reply_code: bytes) -> None:
conn = SOCKS4Connection(user_id=b"socks")
reply = conn.receive_data(
b"".join(
[
b"\x00",
request_reply_code,
(8080).to_bytes(2, byteorder="big"),
b"\x7f\x00\x00\x01",
]
)
)
assert reply == SOCKS4Reply(
reply_code=SOCKS4ReplyCode(request_reply_code), port=8080, addr="127.0.0.1"
)
@pytest.mark.parametrize(
"data",
[
b"\x00Z\x1f\x90\x7f\x00\x00", # missing one byte
b"\x0FZ\x1f\x90\x7f\x00\x00\x01", # not starting with 0
b"\x00\xFF\x1f\x90\x7f\x00\x00\x01", # incorrect reply code
],
)
def test_socks4_receive_malformed_data(data: bytes) -> None:
conn = SOCKS4Connection(user_id=b"socks")
with pytest.raises(ProtocolError):
conn.receive_data(data)
@pytest.mark.parametrize("command", [SOCKS4Command.BIND, SOCKS4Command.CONNECT])
def test_SOCKS4A_connection_request(command: SOCKS4Command) -> None:
conn = SOCKS4Connection(user_id=b"socks")
request = SOCKS4ARequest.from_address(
command=command, address=("proxy.example.com", 8080)
)
conn.send(request)
data = conn.data_to_send()
assert len(data) == 32
assert data[0:1] == b"\x04"
assert data[1:2] == command
assert data[2:4] == (8080).to_bytes(2, byteorder="big")
assert data[4:8] == b"\x00\x00\x00\xFF"
assert data[8:14] == b"socks\x00"
assert data[14:] == b"proxy.example.com\x00"
socksio-1.0.0/tests/test_socks5.py 0000664 0000000 0000000 00000026647 13646347054 0017177 0 ustar 00root root 0000000 0000000 import pytest
from socksio import (
ProtocolError,
SOCKS5AType,
SOCKS5AuthMethod,
SOCKS5AuthMethodsRequest,
SOCKS5AuthReply,
SOCKS5Command,
SOCKS5CommandRequest,
SOCKS5Connection,
SOCKS5Reply,
SOCKS5ReplyCode,
SOCKS5UsernamePasswordRequest,
)
from socksio.socks5 import SOCKS5State
from socksio.utils import AddressType
@pytest.mark.parametrize(
"atype,expected",
[
(AddressType.IPV4, SOCKS5AType.IPV4_ADDRESS),
(AddressType.IPV6, SOCKS5AType.IPV6_ADDRESS),
(AddressType.DN, SOCKS5AType.DOMAIN_NAME),
],
)
def test_socks5atype_from_address_type(
atype: AddressType, expected: SOCKS5AType
) -> None:
assert SOCKS5AType.from_atype(atype) == expected
def test_socks5atype_unknown_address_type_raises() -> None:
with pytest.raises(ValueError):
SOCKS5AType.from_atype("FOOBAR") # type: ignore
@pytest.mark.parametrize(
"address,expected_atype,expected_address,expected_port",
[
(("127.0.0.1", 3080), SOCKS5AType.IPV4_ADDRESS, b"\x7f\x00\x00\x01", 3080),
(("127.0.0.1", "3080"), SOCKS5AType.IPV4_ADDRESS, b"\x7f\x00\x00\x01", 3080),
("127.0.0.1:8080", SOCKS5AType.IPV4_ADDRESS, b"\x7f\x00\x00\x01", 8080),
(
"[0:0:0:0:0:0:0:1]:3080",
SOCKS5AType.IPV6_ADDRESS,
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01",
3080,
),
((b"127.0.0.1", 3080), SOCKS5AType.IPV4_ADDRESS, b"\x7f\x00\x00\x01", 3080),
((b"127.0.0.1", b"3080"), SOCKS5AType.IPV4_ADDRESS, b"\x7f\x00\x00\x01", 3080),
(b"127.0.0.1:8080", SOCKS5AType.IPV4_ADDRESS, b"\x7f\x00\x00\x01", 8080),
(
b"[0:0:0:0:0:0:0:1]:3080",
SOCKS5AType.IPV6_ADDRESS,
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01",
3080,
),
],
)
def test_socks5commandrequest_from_address(
address, expected_atype, expected_address, expected_port
) -> None:
cmd = SOCKS5CommandRequest.from_address(SOCKS5Command.CONNECT, address)
assert cmd.command == SOCKS5Command.CONNECT
assert cmd.atype == expected_atype
assert cmd.addr == expected_address
assert cmd.port == expected_port
def test_socks5_auth_request() -> None:
conn = SOCKS5Connection()
auth_methods = [SOCKS5AuthMethod.GSSAPI, SOCKS5AuthMethod.USERNAME_PASSWORD]
auth_request = SOCKS5AuthMethodsRequest(auth_methods)
conn.send(auth_request)
data = conn.data_to_send()
assert len(data) == 4
assert data[0:1] == b"\x05"
assert data[1:2] == len(auth_methods).to_bytes(1, byteorder="big")
assert data[2:3] == SOCKS5AuthMethod.GSSAPI
assert data[3:] == SOCKS5AuthMethod.USERNAME_PASSWORD
@pytest.mark.parametrize(
"auth_method",
[
SOCKS5AuthMethod.NO_AUTH_REQUIRED,
SOCKS5AuthMethod.USERNAME_PASSWORD,
SOCKS5AuthMethod.GSSAPI,
],
)
def test_socks5_auth_reply_accepted(auth_method: SOCKS5AuthMethod) -> None:
conn = SOCKS5Connection()
auth_methods = [
SOCKS5AuthMethod.NO_AUTH_REQUIRED,
SOCKS5AuthMethod.USERNAME_PASSWORD,
SOCKS5AuthMethod.GSSAPI,
]
auth_request = SOCKS5AuthMethodsRequest(auth_methods)
conn.send(auth_request)
reply = conn.receive_data(b"\x05" + auth_method)
assert reply == SOCKS5AuthReply(method=auth_method)
def test_socks5_auth_reply_no_acceptable_auth_method() -> None:
conn = SOCKS5Connection()
auth_request = SOCKS5AuthMethodsRequest([SOCKS5AuthMethod.USERNAME_PASSWORD])
conn.send(auth_request)
reply = conn.receive_data(b"\x05\xFF")
assert reply == SOCKS5AuthReply(method=SOCKS5AuthMethod.NO_ACCEPTABLE_METHODS)
@pytest.mark.parametrize(
"data", [b"\x05", b"\x05\x10"] # missing method byte , incorrect method value
)
def test_socks5_auth_reply_malformed(data: bytes) -> None:
conn = SOCKS5Connection()
auth_request = SOCKS5AuthMethodsRequest([SOCKS5AuthMethod.USERNAME_PASSWORD])
conn.send(auth_request)
with pytest.raises(ProtocolError):
conn.receive_data(data)
def test_socks5_no_auth_required_reply_sets_client_authenticated_state() -> None:
conn = SOCKS5Connection()
auth_methods = [
SOCKS5AuthMethod.NO_AUTH_REQUIRED,
SOCKS5AuthMethod.USERNAME_PASSWORD,
SOCKS5AuthMethod.GSSAPI,
]
auth_request = SOCKS5AuthMethodsRequest(auth_methods)
conn.send(auth_request)
_ = conn.receive_data(b"\x05" + SOCKS5AuthMethod.NO_AUTH_REQUIRED)
assert conn.state == SOCKS5State.CLIENT_AUTHENTICATED
def test_socks5_auth_username_password_requires_connect_waiting() -> None:
conn = SOCKS5Connection()
auth_request = SOCKS5UsernamePasswordRequest(username=b"user", password=b"pass")
with pytest.raises(ProtocolError):
conn.send(auth_request)
def test_socks5_auth_username_password_success() -> None:
conn = SOCKS5Connection()
auth_methods_request = SOCKS5AuthMethodsRequest(
[SOCKS5AuthMethod.USERNAME_PASSWORD]
)
conn.send(auth_methods_request)
conn.data_to_send()
conn.receive_data(b"\x05" + SOCKS5AuthMethod.USERNAME_PASSWORD)
auth_request = SOCKS5UsernamePasswordRequest(
username=b"username", password=b"password"
)
conn.send(auth_request)
assert conn.data_to_send() == b"\x01\x08username\x08password"
conn.receive_data(b"\x01\x00")
assert conn.state == SOCKS5State.CLIENT_AUTHENTICATED
def test_socks5_auth_username_password_fail() -> None:
conn = SOCKS5Connection()
auth_methods_request = SOCKS5AuthMethodsRequest(
[SOCKS5AuthMethod.USERNAME_PASSWORD]
)
conn.send(auth_methods_request)
conn.data_to_send()
conn.receive_data(b"\x05" + SOCKS5AuthMethod.USERNAME_PASSWORD)
auth_request = SOCKS5UsernamePasswordRequest(
username=b"username", password=b"password"
)
conn.send(auth_request)
assert conn.data_to_send() == b"\x01\x08username\x08password"
conn.receive_data(b"\x01")
assert conn.state == SOCKS5State.MUST_CLOSE
def test_socks5_request_require_authentication() -> None:
conn = SOCKS5Connection()
cmd_request = SOCKS5CommandRequest.from_address(
SOCKS5Command.CONNECT, "127.0.0.1:1080"
)
with pytest.raises(ProtocolError):
conn.send(cmd_request)
@pytest.fixture
def authenticated_conn() -> SOCKS5Connection:
conn = SOCKS5Connection()
auth_methods_request = SOCKS5AuthMethodsRequest(
[SOCKS5AuthMethod.USERNAME_PASSWORD]
)
conn.send(auth_methods_request)
conn.data_to_send()
conn.receive_data(b"\x05" + SOCKS5AuthMethod.USERNAME_PASSWORD)
auth_request = SOCKS5UsernamePasswordRequest(
username=b"username", password=b"password"
)
conn.send(auth_request)
conn.receive_data(b"\x01\x00")
_ = conn.data_to_send() # purge the buffer for further tests
return conn
@pytest.mark.parametrize("command", (SOCKS5Command.CONNECT, SOCKS5Command.BIND))
def test_socks5_request_ipv4(
authenticated_conn: SOCKS5Connection, command: SOCKS5Command
) -> None:
cmd_request = SOCKS5CommandRequest.from_address(command, "127.0.0.1:1080")
authenticated_conn.send(cmd_request)
data = authenticated_conn.data_to_send()
assert len(data) == 10
assert data[0:1] == b"\x05"
assert data[1:2] == command
assert data[2:3] == b"\x00"
assert data[3:4] == b"\x01"
assert data[4:8] == b"\x7f\x00\x00\x01"
assert data[8:] == (1080).to_bytes(2, byteorder="big")
@pytest.mark.parametrize("command", (SOCKS5Command.CONNECT, SOCKS5Command.BIND))
def test_socks5_request_domain_name(
authenticated_conn: SOCKS5Connection, command: SOCKS5Command
) -> None:
cmd_request = SOCKS5CommandRequest.from_address(command, "localhost:1080")
authenticated_conn.send(cmd_request)
data = authenticated_conn.data_to_send()
assert len(data) == 16
assert data[0:1] == b"\x05"
assert data[1:2] == command
assert data[2:3] == b"\x00"
assert data[3:4] == b"\x03"
assert data[4:5] == (9).to_bytes(1, byteorder="big")
assert data[5:14] == b"localhost"
assert data[14:] == (1080).to_bytes(2, byteorder="big")
@pytest.mark.parametrize("command", (SOCKS5Command.CONNECT, SOCKS5Command.BIND))
def test_socks5_request_ipv6(
authenticated_conn: SOCKS5Connection, command: SOCKS5Command
) -> None:
cmd_request = SOCKS5CommandRequest.from_address(
command, address="[0:0:0:0:0:0:0:1]:1080"
)
authenticated_conn.send(cmd_request)
data = authenticated_conn.data_to_send()
assert len(data) == 22
assert data[0:1] == b"\x05"
assert data[1:2] == command
assert data[2:3] == b"\x00"
assert data[3:4] == b"\x04"
assert (
data[4:20]
== b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
)
assert data[20:] == (1080).to_bytes(2, byteorder="big")
@pytest.mark.parametrize(
"atype,addr,expected_atype,expected_addr",
[
(b"\x01", b"\x7f\x00\x00\x01", SOCKS5AType.IPV4_ADDRESS, "127.0.0.1"),
(b"\x03", b"localhost", SOCKS5AType.DOMAIN_NAME, "localhost"),
(
b"\x04",
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01",
SOCKS5AType.IPV6_ADDRESS,
"::1",
),
],
)
def test_socks5_reply_success(
authenticated_conn: SOCKS5Connection,
atype: bytes,
addr: bytes,
expected_atype: SOCKS5AType,
expected_addr: str,
) -> None:
data = b"".join(
[
b"\x05", # protocol version
b"\x00", # reply
b"\x00", # reserved
atype,
addr,
(1080).to_bytes(2, byteorder="big"), # port
]
)
reply = authenticated_conn.receive_data(data)
assert authenticated_conn.state == SOCKS5State.TUNNEL_READY
assert reply == SOCKS5Reply(
reply_code=SOCKS5ReplyCode.SUCCEEDED,
atype=expected_atype,
addr=expected_addr,
port=1080,
)
@pytest.mark.parametrize(
"data",
[
b"\x00\x00\x00\x01\x7f\x00\x00\x01\x048", # incorrect protocol version
b"\x05\x00\x00\x01\x7f\x00\x00\x01\x04", # missing one byte of port number
b"\x05\x00\x00\x01\x7f\x00\x00\x048", # missing one byte of address
],
)
def test_socks5_receive_malformed_data(
authenticated_conn: SOCKS5Connection, data: bytes
) -> None:
with pytest.raises(ProtocolError):
authenticated_conn.receive_data(data)
@pytest.mark.parametrize("error_code", list(SOCKS5ReplyCode)[1:])
@pytest.mark.parametrize(
"atype,addr,expected_atype,expected_addr",
[
(b"\x01", b"\x7f\x00\x00\x01", SOCKS5AType.IPV4_ADDRESS, "127.0.0.1"),
(b"\x03", b"localhost", SOCKS5AType.DOMAIN_NAME, "localhost"),
(
b"\x04",
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01",
SOCKS5AType.IPV6_ADDRESS,
"::1",
),
],
)
def test_socks5_reply_error(
error_code: SOCKS5ReplyCode,
authenticated_conn: SOCKS5Connection,
atype: bytes,
addr: bytes,
expected_atype: SOCKS5AType,
expected_addr: str,
) -> None:
data = b"".join(
[
b"\x05", # protocol version
error_code,
b"\x00", # reserved
atype,
addr,
(1080).to_bytes(2, byteorder="big"), # port
]
)
reply = authenticated_conn.receive_data(data)
assert authenticated_conn.state == SOCKS5State.MUST_CLOSE
assert reply == SOCKS5Reply(
reply_code=error_code, atype=expected_atype, addr=expected_addr, port=1080
)
socksio-1.0.0/tests/test_utils.py 0000664 0000000 0000000 00000002502 13646347054 0017110 0 ustar 00root root 0000000 0000000 import pytest
from socksio.socks5 import SOCKS5AType
from socksio.utils import AddressType, split_address_port_from_string
@pytest.mark.parametrize(
"socks5_atype,expected",
[
(SOCKS5AType.IPV4_ADDRESS, AddressType.IPV4),
(SOCKS5AType.IPV6_ADDRESS, AddressType.IPV6),
(SOCKS5AType.DOMAIN_NAME, AddressType.DN),
],
)
def test_address_type_from_socks5atype(
socks5_atype: SOCKS5AType, expected: AddressType
) -> None:
assert AddressType.from_socks5_atype(socks5_atype) == expected
def test_socks5atype_unknown_address_type_raises() -> None:
with pytest.raises(ValueError):
AddressType.from_socks5_atype("FOOBAR")
@pytest.mark.parametrize(
"address_str,expected_address,expected_port",
[
("127.0.0.1:8080", "127.0.0.1", 8080),
("[0:0:0:0:0:0:0:1]:3080", "0:0:0:0:0:0:0:1", 3080),
],
)
def test_split_address_port_from_string(
address_str, expected_address, expected_port
) -> None:
assert split_address_port_from_string(address_str) == (
expected_address,
expected_port,
)
@pytest.mark.parametrize(
"address_str", ["127.0.0.1", "::1", "127.0.0.1:", "[::1]:foobar"]
)
def test_split_address_port_from_string_errors(address_str) -> None:
with pytest.raises(ValueError):
split_address_port_from_string(address_str)