Skip to content

Commit

Permalink
Add proxy.http.client utility and base SSH classes (#1395)
Browse files Browse the repository at this point in the history
* Add `proxy.http.client` utility and base SSH classes

* py_class_role
  • Loading branch information
abhinavsingh authored Apr 23, 2024
1 parent c24862b commit 7824847
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 4 deletions.
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ OPEN=$(shell which xdg-open)
endif

.PHONY: all https-certificates sign-https-certificates ca-certificates
.PHONY: lib-check lib-clean lib-test lib-package lib-coverage lib-lint lib-pytest
.PHONY: lib-check lib-clean lib-test lib-package lib-coverage lib-lint lib-pytest lib-build
.PHONY: lib-release-test lib-release lib-profile lib-doc lib-pre-commit
.PHONY: lib-dep lib-flake8 lib-mypy lib-speedscope container-buildx-all-platforms
.PHONY: container container-run container-release container-build container-buildx
Expand Down Expand Up @@ -124,9 +124,11 @@ lib-pytest:

lib-test: lib-clean lib-check lib-lint lib-pytest

lib-package: lib-clean lib-check
lib-build:
$(PYTHON) -m tox -e cleanup-dists,build-dists,metadata-validation

lib-package: lib-clean lib-check lib-build

lib-release-test: lib-package
twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*

Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@
(_py_class_role, 'connection.Connection'),
(_py_class_role, 'EventQueue'),
(_py_class_role, 'T'),
(_py_class_role, 'module'),
(_py_class_role, 'HostPort'),
(_py_class_role, 'TcpOrTlsSocket'),
(_py_class_role, 're.Pattern'),
Expand Down
15 changes: 15 additions & 0 deletions proxy.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Unit]
Description=ProxyPy Server
After=network.target

[Service]
Type=simple
User=proxypy
Group=proxypy
ExecStart=proxy --hostname 0.0.0.0
Restart=always
SyslogIdentifier=proxypy
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
16 changes: 14 additions & 2 deletions proxy/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,23 @@ def _env_threadless_compliant() -> bool:
AT = b'@'
AND = b'&'
EQUAL = b'='
TCP_PROTO = b"tcp"
UDP_PROTO = b"udp"
HTTP_PROTO = b'http'
HTTPS_PROTO = HTTP_PROTO + b's'
WEBSOCKET_PROTO = b"ws"
WEBSOCKETS_PROTO = WEBSOCKET_PROTO + b"s"
HTTP_PROTOS = [HTTP_PROTO, HTTPS_PROTO, WEBSOCKET_PROTO, WEBSOCKETS_PROTO]
SSL_PROTOS = [HTTPS_PROTO, WEBSOCKETS_PROTO]
HTTP_1_0 = HTTP_PROTO.upper() + SLASH + b'1.0'
HTTP_1_1 = HTTP_PROTO.upper() + SLASH + b'1.1'
HTTP_URL_PREFIX = HTTP_PROTO + COLON + SLASH + SLASH
HTTPS_URL_PREFIX = HTTPS_PROTO + COLON + SLASH + SLASH
COLON_SLASH_SLASH = COLON + SLASH + SLASH
TCP_URL_PREFIX = TCP_PROTO + COLON_SLASH_SLASH
UDP_URL_PREFIX = UDP_PROTO + COLON_SLASH_SLASH
HTTP_URL_PREFIX = HTTP_PROTO + COLON_SLASH_SLASH
HTTPS_URL_PREFIX = HTTPS_PROTO + COLON_SLASH_SLASH
WEBSOCKET_URL_PREFIX = WEBSOCKET_PROTO + COLON_SLASH_SLASH
WEBSOCKETS_URL_PREFIX = WEBSOCKETS_PROTO + COLON_SLASH_SLASH

LOCAL_INTERFACE_HOSTNAMES = (
b'localhost',
Expand Down Expand Up @@ -135,6 +146,7 @@ def _env_threadless_compliant() -> bool:
DEFAULT_HTTP_PORT = 80
DEFAULT_HTTPS_PORT = 443
DEFAULT_WORK_KLASS = 'proxy.http.HttpProtocolHandler'
DEFAULT_SSH_LISTENER_KLASS = "proxy.core.ssh.listener.SshTunnelListener"
DEFAULT_ENABLE_PROXY_PROTOCOL = False
# 25 milliseconds to keep the loops hot
# Will consume ~0.3-0.6% CPU when idle.
Expand Down
19 changes: 19 additions & 0 deletions proxy/common/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import io
import os
import inspect
import logging
import importlib
import itertools
# pylint: disable=ungrouped-imports
import importlib.util
from types import ModuleType
from typing import Any, Dict, List, Tuple, Union, Optional

Expand Down Expand Up @@ -127,3 +130,19 @@ def locate_klass(klass_module_name: str, klass_path: List[str]) -> Union[type, N
if klass is None or module_name is None:
raise ValueError('%s is not resolvable as a plugin class' % text_(plugin))
return (klass, module_name)

@staticmethod
def from_bytes(pyc: bytes, name: str) -> ModuleType:
code_stream = io.BytesIO(pyc)
spec = importlib.util.spec_from_loader(
name,
loader=None,
origin='dynamic',
is_package=True,
)
assert spec is not None
mod = importlib.util.module_from_spec(spec)
code_stream.seek(0)
# pylint: disable=exec-used
exec(code_stream.read(), mod.__dict__) # noqa: S102
return mod
80 changes: 80 additions & 0 deletions proxy/core/ssh/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- 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 logging
import argparse
from abc import abstractmethod
from typing import TYPE_CHECKING, Any


try:
if TYPE_CHECKING: # pragma: no cover
from paramiko.channel import Channel

from ...common.types import HostPort
except ImportError: # pragma: no cover
pass

logger = logging.getLogger(__name__)


class BaseSshTunnelHandler:

def __init__(self, flags: argparse.Namespace) -> None:
self.flags = flags

@abstractmethod
def on_connection(
self,
chan: 'Channel',
origin: 'HostPort',
server: 'HostPort',
) -> None:
raise NotImplementedError()

@abstractmethod
def shutdown(self) -> None:
raise NotImplementedError()


class BaseSshTunnelListener:

def __init__(
self,
flags: argparse.Namespace,
handler: BaseSshTunnelHandler,
*args: Any,
**kwargs: Any,
) -> None:
self.flags = flags
self.handler = handler

def __enter__(self) -> 'BaseSshTunnelListener':
self.setup()
return self

def __exit__(self, *args: Any) -> None:
self.shutdown()

@abstractmethod
def is_alive(self) -> bool:
raise NotImplementedError()

@abstractmethod
def is_active(self) -> bool:
raise NotImplementedError()

@abstractmethod
def setup(self) -> None:
raise NotImplementedError()

@abstractmethod
def shutdown(self) -> None:
raise NotImplementedError()
68 changes: 68 additions & 0 deletions proxy/http/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- 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 ssl
from typing import Optional

from .parser import HttpParser, httpParserTypes
from ..common.utils import build_http_request, new_socket_connection
from ..common.constants import HTTPS_PROTO, DEFAULT_TIMEOUT


def client(
host: bytes,
port: int,
path: bytes,
method: bytes,
body: Optional[bytes] = None,
conn_close: bool = True,
scheme: bytes = HTTPS_PROTO,
timeout: float = DEFAULT_TIMEOUT,
) -> Optional[HttpParser]:
"""Makes a request to remote registry endpoint"""
request = build_http_request(
method=method,
url=path,
headers={
b'Host': host,
b'Content-Type': b'application/x-www-form-urlencoded',
},
body=body,
conn_close=conn_close,
)
try:
conn = new_socket_connection((host.decode(), port))
except ConnectionRefusedError:
return None
try:
sock = (
ssl.wrap_socket(sock=conn, ssl_version=ssl.PROTOCOL_TLSv1_2)
if scheme == HTTPS_PROTO
else conn
)
except Exception:
conn.close()
return None
parser = HttpParser(
httpParserTypes.RESPONSE_PARSER,
)
sock.settimeout(timeout)
try:
sock.sendall(request)
while True:
chunk = sock.recv(1024)
if not chunk:
break
parser.parse(memoryview(chunk))
if parser.is_complete:
break
finally:
sock.close()
return parser

0 comments on commit 7824847

Please sign in to comment.