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:
parent
c24862ba85
commit
78248474bc
6
Makefile
6
Makefile
|
@ -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/*
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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…
Reference in New Issue