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:
parent
439d58fdc2
commit
148c260472
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
20
Makefile
20
Makefile
|
@ -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) .
|
||||
|
||||
|
|
|
@ -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))))
|
|
@ -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 = [
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
)
|
|
@ -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">
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
make
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
|
@ -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))))
|
|
@ -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))))
|
|
@ -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))))
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue