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

* Add `proxy.http.client` utility and base SSH classes

* py_class_role
This commit is contained in:
Abhinav Singh 2024-04-23 18:29:50 +05:30 committed by GitHub
parent c24862ba85
commit 78248474bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 201 additions and 4 deletions

View File

@ -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
@ -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/*

View File

@ -318,6 +318,7 @@ nitpick_ignore = [
(_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'),

15
proxy.service Normal file
View File

@ -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

View File

@ -49,12 +49,23 @@ SLASH = b'/'
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',
@ -135,6 +146,7 @@ DEFAULT_VERSION = False
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.

View File

@ -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
@ -127,3 +130,19 @@ class Plugins:
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
proxy/core/ssh/base.py Normal file
View File

@ -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
proxy/http/client.py Normal file
View File

@ -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