Skip to content

Commit db8da4f

Browse files
authored
Fix --enable-dashboard flags (#707)
* Expose within __all__ * Enable `--numprocesses=auto` for `pytest.ini` * make lib-lint * Also consider `--plugins` flag when bootstrapping plugins * Add `from .dashboard import ProxyDashboard` in top-level `__init__.py` to make `ProxyDashboard` flags auto discoverable * Move `--enable-dashboard` to top-level * Move logging utility within `Logger` class * Consider comma separated --plugin and --plugins during discover_plugins * Refactor plugin related utilities in Plugins module * mypy and lint * Fix unused import * Safe to use tempdir on Github actions to avoid race conditions??? * pki (generically disk based file) based tests are flaky on macOS under parallel execution
1 parent 78748f5 commit db8da4f

18 files changed

+223
-134
lines changed

proxy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
:license: BSD, see LICENSE for more details.
1010
"""
1111
from .proxy import entry_point, main, Proxy
12-
from .testing.test_case import TestCase
12+
from .testing import TestCase
1313

1414
__all__ = [
1515
# PyPi package entry_point. See

proxy/common/constants.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,18 @@
100100
DEFAULT_DATA_DIRECTORY_PATH = os.path.join(str(pathlib.Path.home()), '.proxy')
101101

102102
# Cor plugins enabled by default or via flags
103+
DEFAULT_ABC_PLUGINS = [
104+
'HttpProtocolHandlerPlugin',
105+
'HttpProxyBasePlugin',
106+
'HttpWebServerBasePlugin',
107+
'ProxyDashboardWebsocketPlugin',
108+
]
103109
PLUGIN_HTTP_PROXY = 'proxy.http.proxy.HttpProxyPlugin'
104110
PLUGIN_WEB_SERVER = 'proxy.http.server.HttpWebServerPlugin'
105111
PLUGIN_PAC_FILE = 'proxy.http.server.HttpWebServerPacFilePlugin'
106112
PLUGIN_DEVTOOLS_PROTOCOL = 'proxy.http.inspector.DevtoolsProtocolPlugin'
107-
PLUGIN_DASHBOARD = 'proxy.dashboard.dashboard.ProxyDashboard'
108-
PLUGIN_INSPECT_TRAFFIC = 'proxy.dashboard.inspect_traffic.InspectTrafficPlugin'
113+
PLUGIN_DASHBOARD = 'proxy.dashboard.ProxyDashboard'
114+
PLUGIN_INSPECT_TRAFFIC = 'proxy.dashboard.InspectTrafficPlugin'
109115
PLUGIN_PROXY_AUTH = 'proxy.http.proxy.AuthPlugin'
110116

111117
PY2_DEPRECATION_MESSAGE = '''DEPRECATION: proxy.py no longer supports Python 2.7. Kindly upgrade to Python 3+. '

proxy/common/flag.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919

2020
from typing import Optional, List, Any, cast
2121

22+
from .plugins import Plugins
2223
from .types import IpAddress
23-
from .utils import text_, bytes_, setup_logger, is_py2, set_open_file_limit
24-
from .utils import import_plugin, load_plugins
24+
from .utils import text_, bytes_, is_py2, set_open_file_limit
2525
from .constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_NUM_WORKERS
2626
from .constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE
2727
from .constants import PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL
2828
from .constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE
2929
from .constants import PLUGIN_WEB_SERVER, PLUGIN_PROXY_AUTH
30+
from .logger import Logger
3031

3132
from .version import __version__
3233

@@ -94,10 +95,9 @@ def initialize(
9495
sys.exit(1)
9596

9697
# Discover flags from requested plugin.
97-
# This also surface external plugin flags under --help
98-
for i, f in enumerate(input_args):
99-
if f == '--plugin':
100-
import_plugin(bytes_(input_args[i + 1]))
98+
# This will also surface external plugin flags
99+
# under --help.
100+
Plugins.discover(input_args)
101101

102102
# Parse flags
103103
args = flags.parse_args(input_args)
@@ -108,7 +108,7 @@ def initialize(
108108
sys.exit(0)
109109

110110
# Setup logging module
111-
setup_logger(args.log_file, args.log_level, args.log_format)
111+
Logger.setup_logger(args.log_file, args.log_level, args.log_format)
112112

113113
# Setup limits
114114
set_open_file_limit(args.open_file_limit)
@@ -125,7 +125,7 @@ def initialize(
125125
]
126126

127127
# Load default plugins along with user provided --plugins
128-
plugins = load_plugins(default_plugins + extra_plugins)
128+
plugins = Plugins.load(default_plugins + extra_plugins)
129129

130130
# proxy.py currently cannot serve over HTTPS and also perform TLS interception
131131
# at the same time. Check if user is trying to enable both feature

proxy/common/logger.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
13+
from typing import Optional
14+
15+
from .constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL
16+
17+
SINGLE_CHAR_TO_LEVEL = {
18+
'D': 'DEBUG',
19+
'I': 'INFO',
20+
'W': 'WARNING',
21+
'E': 'ERROR',
22+
'C': 'CRITICAL',
23+
}
24+
25+
26+
class Logger:
27+
"""Common logging utilities and setup."""
28+
29+
@staticmethod
30+
def setup_logger(
31+
log_file: Optional[str] = DEFAULT_LOG_FILE,
32+
log_level: str = DEFAULT_LOG_LEVEL,
33+
log_format: str = DEFAULT_LOG_FORMAT,
34+
) -> None:
35+
ll = getattr(logging, SINGLE_CHAR_TO_LEVEL[log_level.upper()[0]])
36+
if log_file:
37+
logging.basicConfig(
38+
filename=log_file,
39+
filemode='a',
40+
level=ll,
41+
format=log_format,
42+
)
43+
else:
44+
logging.basicConfig(level=ll, format=log_format)

proxy/common/plugins.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 os
12+
import abc
13+
import logging
14+
import inspect
15+
import importlib
16+
17+
from typing import Any, List, Dict, Optional, Union
18+
19+
from .utils import bytes_, text_
20+
from .constants import DOT, DEFAULT_ABC_PLUGINS
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class Plugins:
26+
"""Common utilities for plugin discovery."""
27+
28+
@staticmethod
29+
def discover(input_args: List[str]) -> None:
30+
"""Search for plugin and plugins flag in command line arguments,
31+
then iterates over each value and discovers the plugin.
32+
"""
33+
for i, f in enumerate(input_args):
34+
if f in ('--plugin', '--plugins'):
35+
v = input_args[i + 1]
36+
parts = v.split(',')
37+
for part in parts:
38+
Plugins.importer(bytes_(part))
39+
40+
@staticmethod
41+
def load(
42+
plugins: List[Union[bytes, type]],
43+
abc_plugins: Optional[List[str]] = None,
44+
) -> Dict[bytes, List[type]]:
45+
"""Accepts a list Python modules, scans them to identify
46+
if they are an implementation of abstract plugin classes and
47+
returns a dictionary of matching plugins for each abstract class.
48+
"""
49+
p: Dict[bytes, List[type]] = {}
50+
for abc_plugin in (abc_plugins or DEFAULT_ABC_PLUGINS):
51+
p[bytes_(abc_plugin)] = []
52+
for plugin_ in plugins:
53+
klass, module_name = Plugins.importer(plugin_)
54+
assert klass and module_name
55+
mro = list(inspect.getmro(klass))
56+
mro.reverse()
57+
iterator = iter(mro)
58+
while next(iterator) is not abc.ABC:
59+
pass
60+
base_klass = next(iterator)
61+
if klass not in p[bytes_(base_klass.__name__)]:
62+
p[bytes_(base_klass.__name__)].append(klass)
63+
logger.info('Loaded plugin %s.%s', module_name, klass.__name__)
64+
return p
65+
66+
@staticmethod
67+
def importer(plugin: Union[bytes, type]) -> Any:
68+
"""Import and returns the plugin."""
69+
if isinstance(plugin, type):
70+
return (plugin, '__main__')
71+
plugin_ = text_(plugin.strip())
72+
assert plugin_ != ''
73+
module_name, klass_name = plugin_.rsplit(text_(DOT), 1)
74+
klass = getattr(
75+
importlib.import_module(
76+
module_name.replace(
77+
os.path.sep, text_(DOT),
78+
),
79+
),
80+
klass_name,
81+
)
82+
return (klass, module_name)

proxy/common/utils.py

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,18 @@
99
:license: BSD, see LICENSE for more details.
1010
"""
1111
import os
12-
import abc
1312
import sys
1413
import ssl
1514
import socket
1615
import logging
17-
import inspect
18-
import importlib
1916
import functools
2017
import ipaddress
2118
import contextlib
2219

2320
from types import TracebackType
24-
from typing import Optional, Dict, Any, List, Tuple, Type, Callable, Union
21+
from typing import Optional, Dict, Any, List, Tuple, Type, Callable
2522

2623
from .constants import HTTP_1_1, COLON, WHITESPACE, CRLF, DEFAULT_TIMEOUT
27-
from .constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL
28-
from .constants import DOT
2924

3025
if os.name != 'nt':
3126
import resource
@@ -263,77 +258,6 @@ def get_available_port() -> int:
263258
return int(port)
264259

265260

266-
def setup_logger(
267-
log_file: Optional[str] = DEFAULT_LOG_FILE,
268-
log_level: str = DEFAULT_LOG_LEVEL,
269-
log_format: str = DEFAULT_LOG_FORMAT,
270-
) -> None:
271-
ll = getattr(
272-
logging,
273-
{
274-
'D': 'DEBUG',
275-
'I': 'INFO',
276-
'W': 'WARNING',
277-
'E': 'ERROR',
278-
'C': 'CRITICAL',
279-
}[log_level.upper()[0]],
280-
)
281-
if log_file:
282-
logging.basicConfig(
283-
filename=log_file,
284-
filemode='a',
285-
level=ll,
286-
format=log_format,
287-
)
288-
else:
289-
logging.basicConfig(level=ll, format=log_format)
290-
291-
292-
def load_plugins(
293-
plugins: List[Union[bytes, type]],
294-
) -> Dict[bytes, List[type]]:
295-
"""Accepts a comma separated list of Python modules and returns
296-
a list of respective Python classes."""
297-
p: Dict[bytes, List[type]] = {
298-
b'HttpProtocolHandlerPlugin': [],
299-
b'HttpProxyBasePlugin': [],
300-
b'HttpWebServerBasePlugin': [],
301-
b'ProxyDashboardWebsocketPlugin': [],
302-
}
303-
for plugin_ in plugins:
304-
klass, module_name = import_plugin(plugin_)
305-
assert klass and module_name
306-
mro = list(inspect.getmro(klass))
307-
mro.reverse()
308-
iterator = iter(mro)
309-
while next(iterator) is not abc.ABC:
310-
pass
311-
base_klass = next(iterator)
312-
if klass not in p[bytes_(base_klass.__name__)]:
313-
p[bytes_(base_klass.__name__)].append(klass)
314-
logger.info('Loaded plugin %s.%s', module_name, klass.__name__)
315-
return p
316-
317-
318-
def import_plugin(plugin: Union[bytes, type]) -> Any:
319-
if isinstance(plugin, type):
320-
module_name = '__main__'
321-
klass = plugin
322-
else:
323-
plugin_ = text_(plugin.strip())
324-
assert plugin_ != ''
325-
module_name, klass_name = plugin_.rsplit(text_(DOT), 1)
326-
klass = getattr(
327-
importlib.import_module(
328-
module_name.replace(
329-
os.path.sep, text_(DOT),
330-
),
331-
),
332-
klass_name,
333-
)
334-
return (klass, module_name)
335-
336-
337261
def set_open_file_limit(soft_limit: int) -> None:
338262
"""Configure open file description soft limit on supported OS."""
339263
if os.name != 'nt': # resource module not available on Windows OS

proxy/core/acceptor/acceptor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from ..event import EventQueue, eventNames
2828
from ...common.constants import DEFAULT_THREADLESS
2929
from ...common.flag import flags
30-
from ...common.utils import setup_logger
30+
from ...common.logger import Logger
3131

3232
logger = logging.getLogger(__name__)
3333

@@ -159,7 +159,7 @@ def run_once(self) -> None:
159159
self._start_threaded_work(conn, addr)
160160

161161
def run(self) -> None:
162-
setup_logger(
162+
Logger.setup_logger(
163163
self.flags.log_file, self.flags.log_level,
164164
self.flags.log_format,
165165
)

proxy/core/acceptor/threadless.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from ..connection import TcpClientConnection
2727
from ..event import EventQueue, eventNames
2828

29-
from ...common.utils import setup_logger
29+
from ...common.logger import Logger
3030
from ...common.types import Readables, Writables
3131
from ...common.constants import DEFAULT_TIMEOUT
3232

@@ -204,7 +204,7 @@ def run_once(self) -> None:
204204
self.cleanup_inactive()
205205

206206
def run(self) -> None:
207-
setup_logger(
207+
Logger.setup_logger(
208208
self.flags.log_file, self.flags.log_level,
209209
self.flags.log_format,
210210
)

proxy/core/ssh/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
from .client import SshClient
12+
from .tunnel import Tunnel
13+
14+
__all__ = [
15+
'SshClient',
16+
'Tunnel',
17+
]

proxy/dashboard/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,12 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
from .dashboard import ProxyDashboard
12+
from .inspect_traffic import InspectTrafficPlugin
13+
from .plugin import ProxyDashboardWebsocketPlugin
14+
15+
__all__ = [
16+
'ProxyDashboard',
17+
'InspectTrafficPlugin',
18+
'ProxyDashboardWebsocketPlugin',
19+
]

proxy/dashboard/dashboard.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414

1515
from .plugin import ProxyDashboardWebsocketPlugin
1616

17-
from ..common.flag import flags
1817
from ..common.utils import build_http_response, bytes_
19-
from ..common.constants import DEFAULT_ENABLE_DASHBOARD
2018
from ..http.server import HttpWebServerPlugin, HttpWebServerBasePlugin, httpProtocolTypes
2119
from ..http.parser import HttpParser
2220
from ..http.websocket import WebsocketFrame
@@ -25,14 +23,6 @@
2523
logger = logging.getLogger(__name__)
2624

2725

28-
flags.add_argument(
29-
'--enable-dashboard',
30-
action='store_true',
31-
default=DEFAULT_ENABLE_DASHBOARD,
32-
help='Default: False. Enables proxy.py dashboard.',
33-
)
34-
35-
3626
class ProxyDashboard(HttpWebServerBasePlugin):
3727
"""Proxy Dashboard."""
3828

0 commit comments

Comments
 (0)