Move dashboard backend within proxy module, now ships via same pip package (#177)

* Allow resources to load from http and ws when running w/o https

* Move dashboard backend (dashboard.py) within proxy module. Now shipped with pip install proxy.py

* Update ref to dashboard backend in github workflows

* Add git-pre-commit hook file.

Enable it by symlinking as .git/hooks/pre-commit

* Also enable static server for dashboard serving
This commit is contained in:
Abhinav Singh 2019-11-15 13:29:48 -08:00 committed by GitHub
parent 439d58fdc2
commit 148c260472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 263 additions and 299 deletions

View File

@ -4,11 +4,11 @@ on: [push]
jobs:
build:
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
name: Python ${{ matrix.python }} on ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
os: [ubuntu]
python: [3.7]
max-parallel: 1
fail-fast: false

View File

@ -25,8 +25,8 @@ jobs:
pip install -r requirements-testing.txt
- name: Quality Check
run: |
flake8 --ignore=W504 --max-line-length=127 proxy/ tests/ benchmark/ plugin_examples/ dashboard/dashboard.py setup.py
mypy --strict --ignore-missing-imports proxy/ tests/ benchmark/ plugin_examples/ dashboard/dashboard.py setup.py
flake8 --ignore=W504 --max-line-length=127 proxy/ tests/ benchmark/ plugin_examples/ setup.py
mypy --strict --ignore-missing-imports proxy/ tests/ benchmark/ plugin_examples/ setup.py
- name: Run Tests
run: pytest --cov=proxy tests/
- name: Upload coverage to Codecov

View File

@ -16,8 +16,7 @@ CA_SIGNING_KEY_FILE_PATH := ca-signing-key.pem
.PHONY: all https-certificates ca-certificates autopep8 devtools
.PHONY: lib-clean lib-test lib-package lib-release-test lib-release lib-coverage lib-lint lib-profile
.PHONY: container container-run container-release
.PHONY: dashboard dashboard-clean dashboard-package
.PHONY: plugin-package-clean plugin-package
.PHONY: dashboard dashboard-clean
all: lib-clean lib-test
@ -30,7 +29,6 @@ autopep8:
autopep8 --recursive --in-place --aggressive tests/*.py
autopep8 --recursive --in-place --aggressive plugin_examples/*.py
autopep8 --recursive --in-place --aggressive benchmark/*.py
autopep8 --recursive --in-place --aggressive dashboard/*.py
autopep8 --recursive --in-place --aggressive setup.py
https-certificates:
@ -61,8 +59,8 @@ lib-clean:
rm -rf .hypothesis
lib-lint:
flake8 --ignore=W504 --max-line-length=127 proxy/ tests/ benchmark/ plugin_examples/ dashboard/dashboard.py setup.py
mypy --strict --ignore-missing-imports proxy/ tests/ benchmark/ plugin_examples/ dashboard/dashboard.py setup.py
flake8 --ignore=W504 --max-line-length=127 proxy/ tests/ benchmark/ plugin_examples/ setup.py
mypy --strict --ignore-missing-imports proxy/ tests/ benchmark/ plugin_examples/ setup.py
lib-test: lib-lint
python -m unittest discover
@ -89,18 +87,6 @@ dashboard:
dashboard-clean:
if [[ -d public/dashboard ]]; then rm -rf public/dashboard; fi
dashboard-package-clean:
pushd dashboard && rm -rf build && rm -rf dist && popd
dashboard-package: dashboard-package-clean
pushd dashboard && npm test && PYTHONPATH=.. python setup.py sdist bdist_wheel && popd
plugin-package-clean:
pushd plugin_examples && rm -rf build && rm -rf dist && popd
plugin-package: plugin-package-clean
pushd plugin_examples && PYTHONPATH=.. python setup.py sdist bdist_wheel && popd
container:
docker build -t $(LATEST_TAG) -t $(IMAGE_TAG) .

View File

@ -1,176 +0,0 @@
"""
proxy.py
~~~~~~~~
Fast, Lightweight, Programmable, TLS interception capable
proxy server for Application debugging, testing and development.
:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import os
import json
import logging
from abc import ABC, abstractmethod
from typing import List, Tuple, Any, Dict
from proxy.common.flags import Flags
from proxy.core.event import EventSubscriber
from proxy.http.server import HttpWebServerPlugin, HttpWebServerBasePlugin, httpProtocolTypes
from proxy.http.parser import HttpParser
from proxy.http.websocket import WebsocketFrame
from proxy.http.codes import httpStatusCodes
from proxy.common.utils import build_http_response, bytes_
from proxy.core.connection import TcpClientConnection
logger = logging.getLogger(__name__)
class ProxyDashboardWebsocketPlugin(ABC):
"""Abstract class for plugins extending dashboard websocket API."""
def __init__(
self,
flags: Flags,
client: TcpClientConnection,
subscriber: EventSubscriber) -> None:
self.flags = flags
self.client = client
self.subscriber = subscriber
@abstractmethod
def methods(self) -> List[str]:
"""Return list of methods that this plugin will handle."""
pass
@abstractmethod
def handle_message(self, message: Dict[str, Any]) -> None:
"""Handle messages for registered methods."""
pass
def reply(self, data: Dict[str, Any]) -> None:
self.client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(data))))
class InspectTrafficPlugin(ProxyDashboardWebsocketPlugin):
"""Websocket API for inspect_traffic.ts frontend plugin."""
def methods(self) -> List[str]:
return [
'enable_inspection',
'disable_inspection',
]
def handle_message(self, message: Dict[str, Any]) -> None:
if message['method'] == 'enable_inspection':
# inspection can only be enabled if --enable-events is used
if not self.flags.enable_events:
self.client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(
{'id': message['id'], 'response': 'not enabled'})
)
)
)
else:
self.subscriber.subscribe(
lambda event: ProxyDashboard.callback(
self.client, event))
self.reply(
{'id': message['id'], 'response': 'inspection_enabled'})
elif message['method'] == 'disable_inspection':
self.subscriber.unsubscribe()
self.reply({'id': message['id'],
'response': 'inspection_disabled'})
else:
raise NotImplementedError()
class ProxyDashboard(HttpWebServerBasePlugin):
"""Proxy Dashboard."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.subscriber = EventSubscriber(self.event_queue)
# Initialize Websocket API plugins
self.plugins: Dict[str, ProxyDashboardWebsocketPlugin] = {}
plugins = [InspectTrafficPlugin]
for plugin in plugins:
p = plugin(self.flags, self.client, self.subscriber)
for method in p.methods():
self.plugins[method] = p
def routes(self) -> List[Tuple[int, bytes]]:
return [
# Redirects to /dashboard/
(httpProtocolTypes.HTTP, b'/dashboard'),
# Redirects to /dashboard/
(httpProtocolTypes.HTTPS, b'/dashboard'),
# Redirects to /dashboard/
(httpProtocolTypes.HTTP, b'/dashboard/proxy.html'),
# Redirects to /dashboard/
(httpProtocolTypes.HTTPS, b'/dashboard/proxy.html'),
(httpProtocolTypes.HTTP, b'/dashboard/'),
(httpProtocolTypes.HTTPS, b'/dashboard/'),
(httpProtocolTypes.WEBSOCKET, b'/dashboard'),
]
def handle_request(self, request: HttpParser) -> None:
if request.path == b'/dashboard/':
self.client.queue(
HttpWebServerPlugin.read_and_build_static_file_response(
os.path.join(self.flags.static_server_dir, 'dashboard', 'proxy.html')))
elif request.path in (
b'/dashboard',
b'/dashboard/proxy.html'):
self.client.queue(build_http_response(
httpStatusCodes.PERMANENT_REDIRECT, reason=b'Permanent Redirect',
headers={
b'Location': b'/dashboard/',
b'Content-Length': b'0',
b'Connection': b'close',
}
))
def on_websocket_open(self) -> None:
logger.info('app ws opened')
def on_websocket_message(self, frame: WebsocketFrame) -> None:
try:
assert frame.data
message = json.loads(frame.data)
except UnicodeDecodeError:
logger.error(frame.data)
logger.info(frame.opcode)
return
method = message['method']
if method == 'ping':
self.reply({'id': message['id'], 'response': 'pong'})
elif method in self.plugins:
self.plugins[method].handle_message(message)
else:
logger.info(frame.data)
logger.info(frame.opcode)
self.reply({'id': message['id'], 'response': 'not_implemented'})
def on_websocket_close(self) -> None:
logger.info('app ws closed')
# unsubscribe
def reply(self, data: Dict[str, Any]) -> None:
self.client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(data))))
@staticmethod
def callback(client: TcpClientConnection, event: Dict[str, Any]) -> None:
event['push'] = 'inspect_traffic'
client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(event))))

View File

@ -6,7 +6,7 @@ export const input = 'src/proxy.ts';
export const output = {
file: '../public/dashboard/proxy.js',
format: 'umd',
name: 'projectbundle',
name: 'proxy',
sourcemap: true
};
export const plugins = [

View File

@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
"""
proxy.py
~~~~~~~~
Fast, Lightweight, Programmable, TLS interception capable
proxy server for Application debugging, testing and development.
:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
from setuptools import setup, find_packages
VERSION = (0, 1, 0)
__version__ = '.'.join(map(str, VERSION[0:3]))
__description__ = '⚡⚡⚡ Fast, Lightweight, Programmable Proxy Server in a single Python file.'
__author__ = 'Abhinav Singh'
__author_email__ = 'mailsforabhinav@gmail.com'
__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
__download_url__ = '%s/archive/master.zip' % __homepage__
__license__ = 'BSD'
setup(
name='proxy.py-dashboard',
version=__version__,
author=__author__,
author_email=__author_email__,
url=__homepage__,
description=__description__,
long_description=open('README.md').read().strip(),
long_description_content_type='text/markdown',
download_url=__download_url__,
license=__license__,
packages=find_packages(),
install_requires=['proxy.py'],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Environment :: No Input/Output (Daemon)',
'Environment :: Web Environment',
'Environment :: MacOS X',
'Environment :: Plugins',
'Environment :: Win32 (MS Windows)',
'Framework :: Robot Framework',
'Framework :: Robot Framework :: Library',
'Intended Audience :: Developers',
'Intended Audience :: Education',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: System Administrators',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Operating System :: MacOS',
'Operating System :: MacOS :: MacOS 9',
'Operating System :: MacOS :: MacOS X',
'Operating System :: POSIX',
'Operating System :: POSIX :: Linux',
'Operating System :: Unix',
'Operating System :: Microsoft',
'Operating System :: Microsoft :: Windows',
'Operating System :: Microsoft :: Windows :: Windows 10',
'Operating System :: Android',
'Operating System :: OS Independent',
'Programming Language :: Python :: Implementation',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet',
'Topic :: Internet :: Proxy Servers',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries',
'Topic :: Internet :: WWW/HTTP :: HTTP Servers',
'Topic :: Scientific/Engineering :: Information Analysis',
'Topic :: Software Development :: Debuggers',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Monitoring',
'Topic :: System :: Networking',
'Topic :: System :: Networking :: Firewalls',
'Topic :: System :: Networking :: Monitoring',
'Topic :: Utilities',
'Typing :: Typed',
],
)

View File

@ -14,7 +14,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Security-Policy"
content="default-src https: wss: data:; object-src 'none'; script-src 'self'; style-src 'self'">
content="default-src http: https: ws: wss: data:; object-src 'none'; script-src 'self'; style-src 'self'">
<meta name="referrer" content="same-origin">
<meta name="theme-color" content="#2F3BA2" />
<meta name="apple-mobile-web-app-capable" content="yes">

3
git-pre-commit Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
make

View File

@ -15,9 +15,11 @@ from typing import List
from .version import __version__
PROXY_PY_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
PROXY_PY_START_TIME = time.time()
# /path/to/proxy.py/proxy folder
PROXY_PY_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
CRLF = b'\r\n'
COLON = b':'
WHITESPACE = b' '
@ -45,6 +47,7 @@ DEFAULT_CLIENT_RECVBUF_SIZE = DEFAULT_BUFFER_SIZE
DEFAULT_DEVTOOLS_WS_PATH = b'/devtools'
DEFAULT_DISABLE_HEADERS: List[bytes] = []
DEFAULT_DISABLE_HTTP_PROXY = False
DEFAULT_ENABLE_DASHBOARD = False
DEFAULT_ENABLE_DEVTOOLS = False
DEFAULT_ENABLE_EVENTS = False
DEFAULT_EVENTS_QUEUE = None

View File

@ -9,6 +9,7 @@
"""
import logging
import importlib
import collections
import argparse
import base64
import ipaddress
@ -19,7 +20,7 @@ import sys
import inspect
import pathlib
from typing import Optional, Union, Dict, List, TypeVar, Type, cast, Any
from typing import Optional, Union, Dict, List, TypeVar, Type, cast, Any, Tuple
from .utils import text_, bytes_
from .constants import DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_BACKLOG, DEFAULT_BASIC_AUTH
@ -30,7 +31,7 @@ from .constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY
from .constants import DEFAULT_PAC_FILE_URL_PATH, DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT
from .constants import DEFAULT_NUM_WORKERS, DEFAULT_VERSION, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME
from .constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_STATIC_SERVER_DIR
from .constants import COMMA, DOT
from .constants import DEFAULT_ENABLE_DASHBOARD, COMMA, DOT
from .version import __version__
__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
@ -152,19 +153,32 @@ class Flags:
Flags.setup_logger(args.log_file, args.log_level, args.log_format)
Flags.set_open_file_limit(args.open_file_limit)
default_plugins = ''
http_proxy_plugin = 'proxy.http.proxy.HttpProxyPlugin'
web_server_plugin = 'proxy.http.server.HttpWebServerPlugin'
pac_file_plugin = 'proxy.http.server.HttpWebServerPacFilePlugin'
devtools_protocol_plugin = 'proxy.http.inspector.DevtoolsProtocolPlugin'
dashboard_plugin = 'proxy.dashboard.dashboard.ProxyDashboard'
inspect_traffic_plugin = 'proxy.dashboard.inspect_traffic.InspectTrafficPlugin'
default_plugins: List[Tuple[str, bool]] = []
if args.enable_dashboard:
default_plugins.append((web_server_plugin, True))
args.enable_static_server = True
default_plugins.append((dashboard_plugin, True))
default_plugins.append((inspect_traffic_plugin, True))
args.enable_events = True
args.enable_devtools = True
if args.enable_devtools:
default_plugins += 'proxy.http.inspector.DevtoolsProtocolPlugin,'
default_plugins += 'proxy.http.server.HttpWebServerPlugin,'
default_plugins.append((devtools_protocol_plugin, True))
default_plugins.append((web_server_plugin, True))
if not args.disable_http_proxy:
default_plugins += 'proxy.http.proxy.HttpProxyPlugin,'
default_plugins.append((http_proxy_plugin, True))
if args.enable_web_server or \
args.pac_file is not None or \
args.enable_static_server and \
'proxy.http.server.HttpWebServerPlugin' not in default_plugins:
default_plugins += 'proxy.http.server.HttpWebServerPlugin,'
args.enable_static_server:
default_plugins.append((web_server_plugin, True))
if args.pac_file is not None:
default_plugins += 'proxy.http.server.HttpWebServerPacFilePlugin,'
default_plugins.append((pac_file_plugin, True))
return cls(
auth_code=cast(Optional[bytes], opts.get('auth_code', auth_code)),
@ -238,7 +252,8 @@ class Flags:
plugins=Flags.load_plugins(
bytes_(
'%s%s' %
(default_plugins, opts.get('plugins', args.plugins)))),
(text_(COMMA).join(collections.OrderedDict(default_plugins).keys()),
opts.get('plugins', args.plugins)))),
pid_file=cast(Optional[str], opts.get('pid_file', args.pid_file)))
def tls_interception_enabled(self) -> bool:
@ -331,6 +346,12 @@ class Flags:
action='store_true',
default=DEFAULT_DISABLE_HTTP_PROXY,
help='Default: False. Whether to disable proxy.HttpProxyPlugin.')
parser.add_argument(
'--enable-dashboard',
action='store_true',
default=DEFAULT_ENABLE_DASHBOARD,
help='Default: False. Enables proxy.py dashboard.'
)
parser.add_argument(
'--enable-devtools',
action='store_true',
@ -474,6 +495,7 @@ class Flags:
b'HttpProtocolHandlerPlugin': [],
b'HttpProxyBasePlugin': [],
b'HttpWebServerBasePlugin': [],
b'ProxyDashboardWebsocketPlugin': []
}
for plugin_ in plugins.split(COMMA):
plugin = text_(plugin_.strip())

View File

@ -110,6 +110,7 @@ class AcceptorPool:
"""Listen on port, setup workers and pass server socket to workers."""
self.listen()
if self.flags.enable_events:
logger.info('Core Event enabled')
self.start_event_dispatcher()
self.start_workers()

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
proxy.py
~~~~~~~~
Fast, Lightweight, Programmable Proxy Server in a single Python file.
:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""

View File

@ -0,0 +1,100 @@
"""
proxy.py
~~~~~~~~
Fast, Lightweight, Programmable, TLS interception capable
proxy server for Application debugging, testing and development.
:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import os
import json
import logging
from typing import List, Tuple, Any, Dict
from .plugin import ProxyDashboardWebsocketPlugin
from ..common.utils import build_http_response, bytes_
from ..http.server import HttpWebServerPlugin, HttpWebServerBasePlugin, httpProtocolTypes
from ..http.parser import HttpParser
from ..http.websocket import WebsocketFrame
from ..http.codes import httpStatusCodes
logger = logging.getLogger(__name__)
class ProxyDashboard(HttpWebServerBasePlugin):
"""Proxy Dashboard."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.plugins: Dict[str, ProxyDashboardWebsocketPlugin] = {}
if b'ProxyDashboardWebsocketPlugin' in self.flags.plugins:
for klass in self.flags.plugins[b'ProxyDashboardWebsocketPlugin']:
p = klass(self.flags, self.client, self.event_queue)
for method in p.methods():
self.plugins[method] = p
def routes(self) -> List[Tuple[int, bytes]]:
return [
# Redirects to /dashboard/
(httpProtocolTypes.HTTP, b'/dashboard'),
# Redirects to /dashboard/
(httpProtocolTypes.HTTPS, b'/dashboard'),
# Redirects to /dashboard/
(httpProtocolTypes.HTTP, b'/dashboard/proxy.html'),
# Redirects to /dashboard/
(httpProtocolTypes.HTTPS, b'/dashboard/proxy.html'),
(httpProtocolTypes.HTTP, b'/dashboard/'),
(httpProtocolTypes.HTTPS, b'/dashboard/'),
(httpProtocolTypes.WEBSOCKET, b'/dashboard'),
]
def handle_request(self, request: HttpParser) -> None:
if request.path == b'/dashboard/':
self.client.queue(
HttpWebServerPlugin.read_and_build_static_file_response(
os.path.join(self.flags.static_server_dir, 'dashboard', 'proxy.html')))
elif request.path in (
b'/dashboard',
b'/dashboard/proxy.html'):
self.client.queue(build_http_response(
httpStatusCodes.PERMANENT_REDIRECT, reason=b'Permanent Redirect',
headers={
b'Location': b'/dashboard/',
b'Content-Length': b'0',
b'Connection': b'close',
}
))
def on_websocket_open(self) -> None:
logger.info('app ws opened')
def on_websocket_message(self, frame: WebsocketFrame) -> None:
try:
assert frame.data
message = json.loads(frame.data)
except UnicodeDecodeError:
logger.error(frame.data)
logger.info(frame.opcode)
return
method = message['method']
if method == 'ping':
self.reply({'id': message['id'], 'response': 'pong'})
elif method in self.plugins:
self.plugins[method].handle_message(message)
else:
logger.info(frame.data)
logger.info(frame.opcode)
self.reply({'id': message['id'], 'response': 'not_implemented'})
def on_websocket_close(self) -> None:
logger.info('app ws closed')
# unsubscribe
def reply(self, data: Dict[str, Any]) -> None:
self.client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(data))))

View File

@ -0,0 +1,56 @@
import json
from typing import List, Dict, Any
from .plugin import ProxyDashboardWebsocketPlugin
from ..common.utils import bytes_
from ..core.event import EventSubscriber
from ..core.connection import TcpClientConnection
from ..http.websocket import WebsocketFrame
class InspectTrafficPlugin(ProxyDashboardWebsocketPlugin):
"""Websocket API for inspect_traffic.ts frontend plugin."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.subscriber = EventSubscriber(self.event_queue)
def methods(self) -> List[str]:
return [
'enable_inspection',
'disable_inspection',
]
def handle_message(self, message: Dict[str, Any]) -> None:
if message['method'] == 'enable_inspection':
# inspection can only be enabled if --enable-events is used
if not self.flags.enable_events:
self.client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(
{'id': message['id'], 'response': 'not enabled'})
)
)
)
else:
self.subscriber.subscribe(
lambda event: InspectTrafficPlugin.callback(
self.client, event))
self.reply(
{'id': message['id'], 'response': 'inspection_enabled'})
elif message['method'] == 'disable_inspection':
self.subscriber.unsubscribe()
self.reply({'id': message['id'],
'response': 'inspection_disabled'})
else:
raise NotImplementedError()
@staticmethod
def callback(client: TcpClientConnection, event: Dict[str, Any]) -> None:
event['push'] = 'inspect_traffic'
client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(event))))

47
proxy/dashboard/plugin.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
proxy.py
~~~~~~~~
Fast, Lightweight, Programmable Proxy Server in a single Python file.
:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import json
from abc import ABC, abstractmethod
from typing import List, Dict, Any
from ..common.utils import bytes_
from ..common.flags import Flags
from ..http.websocket import WebsocketFrame
from ..core.connection import TcpClientConnection
from ..core.event import EventQueue
class ProxyDashboardWebsocketPlugin(ABC):
"""Abstract class for plugins extending dashboard websocket API."""
def __init__(
self,
flags: Flags,
client: TcpClientConnection,
event_queue: EventQueue) -> None:
self.flags = flags
self.client = client
self.event_queue = event_queue
@abstractmethod
def methods(self) -> List[str]:
"""Return list of methods that this plugin will handle."""
pass
@abstractmethod
def handle_message(self, message: Dict[str, Any]) -> None:
"""Handle messages for registered methods."""
pass
def reply(self, data: Dict[str, Any]) -> None:
self.client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(data))))

View File

@ -1,7 +1,7 @@
python-coveralls==2.9.3
coverage==4.5.4
flake8==3.7.9
pytest==5.2.2
pytest==5.2.3
pytest-cov==2.8.1
autopep8==1.4.4
mypy==0.740