Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Devtools Protocol #174

Merged
merged 24 commits into from
Nov 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7b045b6
Refine docs
abhinavsingh Nov 14, 2019
4f0beec
Decouple relay from dashboard.
abhinavsingh Nov 14, 2019
4fa5ba7
Just have a single manager for all eventing
abhinavsingh Nov 14, 2019
fa24834
Ofcourse managers cant be shared across processes
abhinavsingh Nov 14, 2019
b2957d7
Remove unused
abhinavsingh Nov 14, 2019
a99f6b6
Add DevtoolsProtocolPlugin
abhinavsingh Nov 14, 2019
8d3bbb8
Emit REQUEST_COMPLETE core event
abhinavsingh Nov 14, 2019
3cf9859
Emit only if --enable-events used
abhinavsingh Nov 14, 2019
2258679
Add event emitter for response cycle
abhinavsingh Nov 14, 2019
ce85dfc
Fill up core events to devtools protocol expectations
abhinavsingh Nov 14, 2019
40f7d4d
Serve static content with Cache-Control header and gzip compression
abhinavsingh Nov 14, 2019
82471ec
Add PWA manifest.json and icons from sample PWA apps (replace later)
abhinavsingh Nov 14, 2019
e8a8dbb
Catch any exception and be ssl agnostic
abhinavsingh Nov 14, 2019
fd887f9
Add CSP headers and avoid inline scripts
abhinavsingh Nov 14, 2019
a477832
Re-enable iframe and deobfuscation
abhinavsingh Nov 14, 2019
51a33e0
Embed plugins within <section/> block
abhinavsingh Nov 14, 2019
14ead49
Make tab switching agnostic of block name
abhinavsingh Nov 14, 2019
8517055
Add support for browser history on tab change
abhinavsingh Nov 15, 2019
14db4c6
Default hash to #home
abhinavsingh Nov 15, 2019
a9841cd
Switch to tab if hash is already set
abhinavsingh Nov 15, 2019
9177eae
Expand canvas to fill screen even without content
abhinavsingh Nov 15, 2019
b3dc7d3
Remove inline css for embedded devtools
abhinavsingh Nov 15, 2019
e068fc9
Make dashboard backend websocket API pluggable
abhinavsingh Nov 15, 2019
3df07a0
doc
abhinavsingh Nov 15, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ Embed proxy.py
## Blocking Mode

Start `proxy.py` in embedded mode with default configuration
by using the `main` method. Example:
by using `proxy.main` method. Example:

```
import proxy
Expand Down Expand Up @@ -782,10 +782,10 @@ Note that:
1. `start` is a context manager.
It will start `proxy.py` when called and will shut it down
once scope ends.
3. Just like `main`, startup flags with `start` method too
3. Just like `main`, startup flags with `start` method
can be customized by either passing flags as list of
input arguments `start(['--port', '8899'])` or by using passing
flags as kwargs `start(port=8899)`.
input arguments e.g. `start(['--port', '8899'])` or
by using passing flags as kwargs e.g. `start(port=8899)`.

Unit testing with proxy.py
==========================
Expand Down Expand Up @@ -1131,13 +1131,14 @@ usage: proxy [-h] [--backlog BACKLOG] [--basic-auth BASIC_AUTH]
[--client-recvbuf-size CLIENT_RECVBUF_SIZE]
[--devtools-ws-path DEVTOOLS_WS_PATH]
[--disable-headers DISABLE_HEADERS] [--disable-http-proxy]
[--enable-devtools] [--enable-events] [--enable-static-server]
[--enable-web-server] [--hostname HOSTNAME] [--key-file KEY_FILE]
[--enable-devtools] [--enable-events]
[--enable-static-server] [--enable-web-server]
[--hostname HOSTNAME] [--key-file KEY_FILE]
[--log-level LOG_LEVEL] [--log-file LOG_FILE]
[--log-format LOG_FORMAT] [--num-workers NUM_WORKERS]
[--open-file-limit OPEN_FILE_LIMIT] [--pac-file PAC_FILE]
[--pac-file-url-path PAC_FILE_URL_PATH] [--pid-file PID_FILE]
[--plugins PLUGINS] [--port PORT]
[--pac-file-url-path PAC_FILE_URL_PATH]
[--pid-file PID_FILE] [--plugins PLUGINS] [--port PORT]
[--server-recvbuf-size SERVER_RECVBUF_SIZE]
[--static-server-dir STATIC_SERVER_DIR] [--threadless]
[--timeout TIMEOUT] [--version]
Expand Down Expand Up @@ -1186,7 +1187,7 @@ optional arguments:
--disable-http-proxy Default: False. Whether to disable
proxy.HttpProxyPlugin.
--enable-devtools Default: False. Enables integration with Chrome
Devtool Frontend.
Devtool Frontend. Also see --devtools-ws-path.
--enable-events Default: False. Enables core to dispatch lifecycle
events. Plugins can be used to subscribe for core
events.
Expand Down
11 changes: 11 additions & 0 deletions benchmark/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python
# -*- 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.
"""
170 changes: 88 additions & 82 deletions dashboard/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,99 @@
"""
import os
import json
import queue
import logging
import threading
import multiprocessing
import uuid
from typing import List, Tuple, Optional, Any, Dict
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.common.types import DictQueueType
from proxy.core.connection import TcpClientConnection

logger = logging.getLogger(__name__)


class ProxyDashboard(HttpWebServerBasePlugin):
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."""

RELAY_MANAGER: multiprocessing.managers.SyncManager = multiprocessing.Manager()
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.inspection_enabled: bool = False
self.relay_thread: Optional[threading.Thread] = None
self.relay_shutdown: Optional[threading.Event] = None
self.relay_channel: Optional[DictQueueType] = None
self.relay_sub_id: Optional[str] = None
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 [
Expand Down Expand Up @@ -83,65 +147,19 @@ def on_websocket_message(self, frame: WebsocketFrame) -> None:
logger.info(frame.opcode)
return

if message['method'] == 'ping':
method = message['method']
if method == 'ping':
self.reply({'id': message['id'], 'response': 'pong'})
elif 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.inspection_enabled = True

self.relay_shutdown = threading.Event()
self.relay_channel = ProxyDashboard.RELAY_MANAGER.Queue()
self.relay_thread = threading.Thread(
target=self.relay_events,
args=(self.relay_shutdown, self.relay_channel, self.client))
self.relay_thread.start()

self.relay_sub_id = uuid.uuid4().hex
self.event_queue.subscribe(
self.relay_sub_id, self.relay_channel)

self.reply(
{'id': message['id'], 'response': 'inspection_enabled'})
elif message['method'] == 'disable_inspection':
self.shutdown_relay()
self.inspection_enabled = False
self.reply({'id': message['id'],
'response': 'inspection_disabled'})
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')
self.shutdown_relay()

def shutdown_relay(self) -> None:
if not self.inspection_enabled:
return

assert self.relay_shutdown
assert self.relay_thread
assert self.relay_sub_id

self.event_queue.unsubscribe(self.relay_sub_id)
self.relay_shutdown.set()
self.relay_thread.join()

self.relay_thread = None
self.relay_shutdown = None
self.relay_channel = None
self.relay_sub_id = None
# unsubscribe

def reply(self, data: Dict[str, Any]) -> None:
self.client.queue(
Expand All @@ -150,21 +168,9 @@ def reply(self, data: Dict[str, Any]) -> None:
json.dumps(data))))

@staticmethod
def relay_events(
shutdown: threading.Event,
channel: DictQueueType,
client: TcpClientConnection) -> None:
while not shutdown.is_set():
try:
ev = channel.get(timeout=1)
ev['push'] = 'inspect_traffic'
client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(ev))))
except queue.Empty:
pass
except EOFError:
break
except KeyboardInterrupt:
break
def callback(client: TcpClientConnection, event: Dict[str, Any]) -> None:
event['push'] = 'inspect_traffic'
client.queue(
WebsocketFrame.text(
bytes_(
json.dumps(event))))
9 changes: 6 additions & 3 deletions dashboard/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typescript from 'rollup-plugin-typescript';
import copy from 'rollup-plugin-copy';
// import obfuscatorPlugin from 'rollup-plugin-javascript-obfuscator';
import obfuscatorPlugin from 'rollup-plugin-javascript-obfuscator';

export const input = 'src/proxy.ts';
export const output = {
Expand All @@ -21,6 +21,9 @@ export const plugins = [
}, {
src: 'src/proxy.css',
dest: '../public/dashboard',
}, {
src: 'src/manifest.json',
dest: '../public/dashboard',
}, {
src: 'src/core/plugins/inspect_traffic.json',
dest: '../public/dashboard/devtools'
Expand All @@ -32,7 +35,7 @@ export const plugins = [
dest: '../public/dashboard/devtools'
}],
}),
/* obfuscatorPlugin({
obfuscatorPlugin({
log: false,
sourceMap: true,
compact: true,
Expand All @@ -42,5 +45,5 @@ export const plugins = [
stringArrayThreshold: 1,
stringArrayEncoding: 'rc4',
identifierNamesGenerator: 'mangled',
}) */
})
];
10 changes: 8 additions & 2 deletions dashboard/src/core/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { WebsocketApi } from './ws'
export interface IDashboardPlugin {
name: string
title: string
tabId(): string
initializeTab(): JQuery<HTMLElement>
initializeHeader(): JQuery<HTMLElement>
initializeBody(): JQuery<HTMLElement>
Expand All @@ -30,11 +31,16 @@ export abstract class DashboardPlugin implements IDashboardPlugin {
this.websocketApi = websocketApi
}

public tabId () : string {
return this.name + '_tab'
}

public makeTab (name: string, icon: string) : JQuery<HTMLElement> {
return $('<a/>')
.attr({
href: '#',
plugin_name: this.name
href: '#' + this.name,
plugin_name: this.name,
id: this.tabId()
})
.addClass('nav-link')
.text(name)
Expand Down
17 changes: 17 additions & 0 deletions dashboard/src/core/plugins/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ export class HomePlugin extends DashboardPlugin {
return this.makeHeader(this.title)
}

// Show following metrics on home page:
// 0. Uptime
// 1. Total number of requests served counter
// - Separate numbers for proxy and in-built http server
// 2. Number of active requests counter
// - Click to inspect via inspect traffic tab
// - Will be hard here to focus on exact active request within embedded Devtools
// 3. Requests served per second / minute / hours chart
// 4. Active requests per second / minute / hours chart
// 5. List of all proxy.py processes
// - Threads per process
// - RAM / CPU per process over time charts
// 6. Bandwidth served
// - Total incoming bytes
// - Total outgoing bytes
// - Ingress / Egress bytes per sec / min / hour
// 7. Active plugin list
public initializeBody (): JQuery<HTMLElement> {
return $('<div></div>')
}
Expand Down
11 changes: 6 additions & 5 deletions dashboard/src/core/plugins/inspect_traffic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
*/
import { DashboardPlugin } from '../plugin'

declare const Root: any

export class InspectTrafficPlugin extends DashboardPlugin {
public name: string = 'inspect_traffic'
public title: string = 'Inspect Traffic'
Expand All @@ -28,12 +26,15 @@ export class InspectTrafficPlugin extends DashboardPlugin {
}

public activated (): void {
this.websocketApi.enableInspection(this.handleEvents.bind(this))
this.websocketApi.sendMessage(
{ method: 'enable_inspection' },
this.handleEvents.bind(this))
this.ensureIFrame()
}

public deactivated (): void {
this.websocketApi.disableInspection()
this.websocketApi.sendMessage(
{ method: 'disable_inspection' })
}

public handleEvents (message: Record<string, any>): void {
Expand All @@ -57,7 +58,7 @@ export class InspectTrafficPlugin extends DashboardPlugin {
private getDevtoolsIFrame (): JQuery<HTMLElement> {
return $('<iframe></iframe>')
.attr('id', this.getDevtoolsIFrameID())
.attr('height', '80%')
.attr('height', '100%')
.attr('width', '100%')
.attr('padding', '0')
.attr('margin', '0')
Expand Down
Loading