Skip to content

Commit 7824847

Browse files
authored
Add proxy.http.client utility and base SSH classes (#1395)
* Add `proxy.http.client` utility and base SSH classes * py_class_role
1 parent c24862b commit 7824847

File tree

7 files changed

+201
-4
lines changed

7 files changed

+201
-4
lines changed

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ OPEN=$(shell which xdg-open)
3131
endif
3232

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

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

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

130+
lib-package: lib-clean lib-check lib-build
131+
130132
lib-release-test: lib-package
131133
twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*
132134

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@
318318
(_py_class_role, 'connection.Connection'),
319319
(_py_class_role, 'EventQueue'),
320320
(_py_class_role, 'T'),
321+
(_py_class_role, 'module'),
321322
(_py_class_role, 'HostPort'),
322323
(_py_class_role, 'TcpOrTlsSocket'),
323324
(_py_class_role, 're.Pattern'),

proxy.service

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[Unit]
2+
Description=ProxyPy Server
3+
After=network.target
4+
5+
[Service]
6+
Type=simple
7+
User=proxypy
8+
Group=proxypy
9+
ExecStart=proxy --hostname 0.0.0.0
10+
Restart=always
11+
SyslogIdentifier=proxypy
12+
LimitNOFILE=65536
13+
14+
[Install]
15+
WantedBy=multi-user.target

proxy/common/constants.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,23 @@ def _env_threadless_compliant() -> bool:
4949
AT = b'@'
5050
AND = b'&'
5151
EQUAL = b'='
52+
TCP_PROTO = b"tcp"
53+
UDP_PROTO = b"udp"
5254
HTTP_PROTO = b'http'
5355
HTTPS_PROTO = HTTP_PROTO + b's'
56+
WEBSOCKET_PROTO = b"ws"
57+
WEBSOCKETS_PROTO = WEBSOCKET_PROTO + b"s"
58+
HTTP_PROTOS = [HTTP_PROTO, HTTPS_PROTO, WEBSOCKET_PROTO, WEBSOCKETS_PROTO]
59+
SSL_PROTOS = [HTTPS_PROTO, WEBSOCKETS_PROTO]
5460
HTTP_1_0 = HTTP_PROTO.upper() + SLASH + b'1.0'
5561
HTTP_1_1 = HTTP_PROTO.upper() + SLASH + b'1.1'
56-
HTTP_URL_PREFIX = HTTP_PROTO + COLON + SLASH + SLASH
57-
HTTPS_URL_PREFIX = HTTPS_PROTO + COLON + SLASH + SLASH
62+
COLON_SLASH_SLASH = COLON + SLASH + SLASH
63+
TCP_URL_PREFIX = TCP_PROTO + COLON_SLASH_SLASH
64+
UDP_URL_PREFIX = UDP_PROTO + COLON_SLASH_SLASH
65+
HTTP_URL_PREFIX = HTTP_PROTO + COLON_SLASH_SLASH
66+
HTTPS_URL_PREFIX = HTTPS_PROTO + COLON_SLASH_SLASH
67+
WEBSOCKET_URL_PREFIX = WEBSOCKET_PROTO + COLON_SLASH_SLASH
68+
WEBSOCKETS_URL_PREFIX = WEBSOCKETS_PROTO + COLON_SLASH_SLASH
5869

5970
LOCAL_INTERFACE_HOSTNAMES = (
6071
b'localhost',
@@ -135,6 +146,7 @@ def _env_threadless_compliant() -> bool:
135146
DEFAULT_HTTP_PORT = 80
136147
DEFAULT_HTTPS_PORT = 443
137148
DEFAULT_WORK_KLASS = 'proxy.http.HttpProtocolHandler'
149+
DEFAULT_SSH_LISTENER_KLASS = "proxy.core.ssh.listener.SshTunnelListener"
138150
DEFAULT_ENABLE_PROXY_PROTOCOL = False
139151
# 25 milliseconds to keep the loops hot
140152
# Will consume ~0.3-0.6% CPU when idle.

proxy/common/plugins.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
import io
1112
import os
1213
import inspect
1314
import logging
1415
import importlib
1516
import itertools
17+
# pylint: disable=ungrouped-imports
18+
import importlib.util
1619
from types import ModuleType
1720
from typing import Any, Dict, List, Tuple, Union, Optional
1821

@@ -127,3 +130,19 @@ def locate_klass(klass_module_name: str, klass_path: List[str]) -> Union[type, N
127130
if klass is None or module_name is None:
128131
raise ValueError('%s is not resolvable as a plugin class' % text_(plugin))
129132
return (klass, module_name)
133+
134+
@staticmethod
135+
def from_bytes(pyc: bytes, name: str) -> ModuleType:
136+
code_stream = io.BytesIO(pyc)
137+
spec = importlib.util.spec_from_loader(
138+
name,
139+
loader=None,
140+
origin='dynamic',
141+
is_package=True,
142+
)
143+
assert spec is not None
144+
mod = importlib.util.module_from_spec(spec)
145+
code_stream.seek(0)
146+
# pylint: disable=exec-used
147+
exec(code_stream.read(), mod.__dict__) # noqa: S102
148+
return mod

proxy/core/ssh/base.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import logging
12+
import argparse
13+
from abc import abstractmethod
14+
from typing import TYPE_CHECKING, Any
15+
16+
17+
try:
18+
if TYPE_CHECKING: # pragma: no cover
19+
from paramiko.channel import Channel
20+
21+
from ...common.types import HostPort
22+
except ImportError: # pragma: no cover
23+
pass
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
class BaseSshTunnelHandler:
29+
30+
def __init__(self, flags: argparse.Namespace) -> None:
31+
self.flags = flags
32+
33+
@abstractmethod
34+
def on_connection(
35+
self,
36+
chan: 'Channel',
37+
origin: 'HostPort',
38+
server: 'HostPort',
39+
) -> None:
40+
raise NotImplementedError()
41+
42+
@abstractmethod
43+
def shutdown(self) -> None:
44+
raise NotImplementedError()
45+
46+
47+
class BaseSshTunnelListener:
48+
49+
def __init__(
50+
self,
51+
flags: argparse.Namespace,
52+
handler: BaseSshTunnelHandler,
53+
*args: Any,
54+
**kwargs: Any,
55+
) -> None:
56+
self.flags = flags
57+
self.handler = handler
58+
59+
def __enter__(self) -> 'BaseSshTunnelListener':
60+
self.setup()
61+
return self
62+
63+
def __exit__(self, *args: Any) -> None:
64+
self.shutdown()
65+
66+
@abstractmethod
67+
def is_alive(self) -> bool:
68+
raise NotImplementedError()
69+
70+
@abstractmethod
71+
def is_active(self) -> bool:
72+
raise NotImplementedError()
73+
74+
@abstractmethod
75+
def setup(self) -> None:
76+
raise NotImplementedError()
77+
78+
@abstractmethod
79+
def shutdown(self) -> None:
80+
raise NotImplementedError()

proxy/http/client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import ssl
12+
from typing import Optional
13+
14+
from .parser import HttpParser, httpParserTypes
15+
from ..common.utils import build_http_request, new_socket_connection
16+
from ..common.constants import HTTPS_PROTO, DEFAULT_TIMEOUT
17+
18+
19+
def client(
20+
host: bytes,
21+
port: int,
22+
path: bytes,
23+
method: bytes,
24+
body: Optional[bytes] = None,
25+
conn_close: bool = True,
26+
scheme: bytes = HTTPS_PROTO,
27+
timeout: float = DEFAULT_TIMEOUT,
28+
) -> Optional[HttpParser]:
29+
"""Makes a request to remote registry endpoint"""
30+
request = build_http_request(
31+
method=method,
32+
url=path,
33+
headers={
34+
b'Host': host,
35+
b'Content-Type': b'application/x-www-form-urlencoded',
36+
},
37+
body=body,
38+
conn_close=conn_close,
39+
)
40+
try:
41+
conn = new_socket_connection((host.decode(), port))
42+
except ConnectionRefusedError:
43+
return None
44+
try:
45+
sock = (
46+
ssl.wrap_socket(sock=conn, ssl_version=ssl.PROTOCOL_TLSv1_2)
47+
if scheme == HTTPS_PROTO
48+
else conn
49+
)
50+
except Exception:
51+
conn.close()
52+
return None
53+
parser = HttpParser(
54+
httpParserTypes.RESPONSE_PARSER,
55+
)
56+
sock.settimeout(timeout)
57+
try:
58+
sock.sendall(request)
59+
while True:
60+
chunk = sock.recv(1024)
61+
if not chunk:
62+
break
63+
parser.parse(memoryview(chunk))
64+
if parser.is_complete:
65+
break
66+
finally:
67+
sock.close()
68+
return parser

0 commit comments

Comments
 (0)