Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proxy.http.client utility and base SSH classes #1395

Merged
merged 2 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading