mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-26 02:10:59 +00:00
mitmweb: cleanup, reimplement eventlog
This commit is contained in:
parent
a6c608e085
commit
fea6041cde
19
mitmproxy/addons/eventstore.py
Normal file
19
mitmproxy/addons/eventstore.py
Normal file
@ -0,0 +1,19 @@
|
||||
from typing import List # noqa
|
||||
|
||||
import blinker
|
||||
from mitmproxy import log
|
||||
|
||||
|
||||
class EventStore:
|
||||
def __init__(self):
|
||||
self.data = [] # type: List[log.LogEntry]
|
||||
self.sig_add = blinker.Signal()
|
||||
self.sig_refresh = blinker.Signal()
|
||||
|
||||
def log(self, entry: log.LogEntry):
|
||||
self.data.append(entry)
|
||||
self.sig_add.send(self, entry=entry)
|
||||
|
||||
def clear(self):
|
||||
self.data.clear()
|
||||
self.sig_refresh.send(self)
|
@ -135,7 +135,6 @@ def mitmweb(args=None): # pragma: no cover
|
||||
web_options.wdebug = args.wdebug
|
||||
web_options.wiface = args.wiface
|
||||
web_options.wport = args.wport
|
||||
web_options.process_web_options(parser)
|
||||
|
||||
server = process_options(parser, web_options, args)
|
||||
m = web.master.WebMaster(web_options, server)
|
||||
|
@ -1,5 +1,3 @@
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
@ -7,19 +5,20 @@ import os.path
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
import mitmproxy.addons.view
|
||||
import mitmproxy.flow
|
||||
import tornado.escape
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
import tornado.escape
|
||||
from mitmproxy import contentviews
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy import http
|
||||
from mitmproxy import io
|
||||
from mitmproxy import log
|
||||
from mitmproxy import version
|
||||
import mitmproxy.addons.view
|
||||
import mitmproxy.flow
|
||||
|
||||
|
||||
def convert_flow_to_json_dict(flow: mitmproxy.flow.Flow) -> dict:
|
||||
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"""
|
||||
Remove flow message content and cert to save transmission space.
|
||||
|
||||
@ -46,8 +45,10 @@ def convert_flow_to_json_dict(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"path": flow.request.path,
|
||||
"http_version": flow.request.http_version,
|
||||
"headers": tuple(flow.request.headers.items(True)),
|
||||
"contentLength": len(flow.request.raw_content) if flow.request.raw_content is not None else None,
|
||||
"contentHash": hashlib.sha256(flow.request.raw_content).hexdigest() if flow.request.raw_content is not None else None,
|
||||
"contentLength": len(
|
||||
flow.request.raw_content) if flow.request.raw_content is not None else None,
|
||||
"contentHash": hashlib.sha256(
|
||||
flow.request.raw_content).hexdigest() if flow.request.raw_content is not None else None,
|
||||
"timestamp_start": flow.request.timestamp_start,
|
||||
"timestamp_end": flow.request.timestamp_end,
|
||||
"is_replay": flow.request.is_replay,
|
||||
@ -58,8 +59,10 @@ def convert_flow_to_json_dict(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"status_code": flow.response.status_code,
|
||||
"reason": flow.response.reason,
|
||||
"headers": tuple(flow.response.headers.items(True)),
|
||||
"contentLength": len(flow.response.raw_content) if flow.response.raw_content is not None else None,
|
||||
"contentHash": hashlib.sha256(flow.response.raw_content).hexdigest() if flow.response.raw_content is not None else None,
|
||||
"contentLength": len(
|
||||
flow.response.raw_content) if flow.response.raw_content is not None else None,
|
||||
"contentHash": hashlib.sha256(
|
||||
flow.response.raw_content).hexdigest() if flow.response.raw_content is not None else None,
|
||||
"timestamp_start": flow.response.timestamp_start,
|
||||
"timestamp_end": flow.response.timestamp_end,
|
||||
"is_replay": flow.response.is_replay,
|
||||
@ -69,34 +72,18 @@ def convert_flow_to_json_dict(flow: mitmproxy.flow.Flow) -> dict:
|
||||
return f
|
||||
|
||||
|
||||
def logentry_to_json(e: log.LogEntry):
|
||||
return {
|
||||
"message": e.msg,
|
||||
"level": e.level
|
||||
}
|
||||
|
||||
|
||||
class APIError(tornado.web.HTTPError):
|
||||
pass
|
||||
|
||||
|
||||
class BasicAuth:
|
||||
|
||||
def set_auth_headers(self):
|
||||
self.set_status(401)
|
||||
self.set_header('WWW-Authenticate', 'Basic realm=MITMWeb')
|
||||
self._transforms = []
|
||||
self.finish()
|
||||
|
||||
def prepare(self):
|
||||
wauthenticator = self.application.settings['wauthenticator']
|
||||
if wauthenticator:
|
||||
auth_header = self.request.headers.get('Authorization')
|
||||
if auth_header is None or not auth_header.startswith('Basic '):
|
||||
self.set_auth_headers()
|
||||
else:
|
||||
auth_decoded = base64.decodebytes(auth_header[6:])
|
||||
username, password = auth_decoded.split(':', 2)
|
||||
if not wauthenticator.test(username, password):
|
||||
self.set_auth_headers()
|
||||
raise APIError(401, "Invalid username or password.")
|
||||
|
||||
|
||||
class RequestHandler(BasicAuth, tornado.web.RequestHandler):
|
||||
|
||||
class RequestHandler(tornado.web.RequestHandler):
|
||||
def write(self, chunk):
|
||||
# Writing arrays on the top level is ok nowadays.
|
||||
# http://flask.pocoo.org/docs/0.11/security/#json-security
|
||||
@ -150,7 +137,6 @@ class RequestHandler(BasicAuth, tornado.web.RequestHandler):
|
||||
|
||||
|
||||
class IndexHandler(RequestHandler):
|
||||
|
||||
def get(self):
|
||||
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
|
||||
assert token
|
||||
@ -158,14 +144,13 @@ class IndexHandler(RequestHandler):
|
||||
|
||||
|
||||
class FilterHelp(RequestHandler):
|
||||
|
||||
def get(self):
|
||||
self.write(dict(
|
||||
commands=flowfilter.help
|
||||
))
|
||||
|
||||
|
||||
class WebSocketEventBroadcaster(BasicAuth, tornado.websocket.WebSocketHandler):
|
||||
class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler):
|
||||
# raise an error if inherited class doesn't specify its own instance.
|
||||
connections = None # type: set
|
||||
|
||||
@ -191,9 +176,8 @@ class ClientConnection(WebSocketEventBroadcaster):
|
||||
|
||||
|
||||
class Flows(RequestHandler):
|
||||
|
||||
def get(self):
|
||||
self.write([convert_flow_to_json_dict(f) for f in self.view])
|
||||
self.write([flow_to_json(f) for f in self.view])
|
||||
|
||||
|
||||
class DumpFlows(RequestHandler):
|
||||
@ -219,25 +203,22 @@ class DumpFlows(RequestHandler):
|
||||
|
||||
|
||||
class ClearAll(RequestHandler):
|
||||
|
||||
def post(self):
|
||||
self.view.clear()
|
||||
self.master.events.clear()
|
||||
|
||||
|
||||
class AcceptFlows(RequestHandler):
|
||||
|
||||
def post(self):
|
||||
self.master.accept_all(self.master)
|
||||
|
||||
|
||||
class AcceptFlow(RequestHandler):
|
||||
|
||||
def post(self, flow_id):
|
||||
self.flow.resume(self.master)
|
||||
|
||||
|
||||
class FlowHandler(RequestHandler):
|
||||
|
||||
def delete(self, flow_id):
|
||||
if self.flow.killable:
|
||||
self.flow.kill(self.master)
|
||||
@ -286,19 +267,16 @@ class FlowHandler(RequestHandler):
|
||||
|
||||
|
||||
class DuplicateFlow(RequestHandler):
|
||||
|
||||
def post(self, flow_id):
|
||||
self.master.view.duplicate_flow(self.flow)
|
||||
|
||||
|
||||
class RevertFlow(RequestHandler):
|
||||
|
||||
def post(self, flow_id):
|
||||
self.flow.revert()
|
||||
|
||||
|
||||
class ReplayFlow(RequestHandler):
|
||||
|
||||
def post(self, flow_id):
|
||||
self.flow.backup()
|
||||
self.flow.response = None
|
||||
@ -310,7 +288,6 @@ class ReplayFlow(RequestHandler):
|
||||
|
||||
|
||||
class FlowContent(RequestHandler):
|
||||
|
||||
def post(self, flow_id, message):
|
||||
self.flow.backup()
|
||||
message = getattr(self.flow, message)
|
||||
@ -347,15 +324,14 @@ class FlowContent(RequestHandler):
|
||||
|
||||
|
||||
class FlowContentView(RequestHandler):
|
||||
|
||||
def get(self, flow_id, message, content_view):
|
||||
message = getattr(self.flow, message)
|
||||
|
||||
description, lines, error = contentviews.get_message_content_view(
|
||||
content_view.replace('_', ' '), message
|
||||
)
|
||||
# if error:
|
||||
# add event log
|
||||
# if error:
|
||||
# add event log
|
||||
|
||||
self.write(dict(
|
||||
lines=list(lines),
|
||||
@ -364,13 +340,11 @@ class FlowContentView(RequestHandler):
|
||||
|
||||
|
||||
class Events(RequestHandler):
|
||||
|
||||
def get(self):
|
||||
self.write([]) # FIXME
|
||||
self.write([logentry_to_json(e) for e in self.master.events.data])
|
||||
|
||||
|
||||
class Settings(RequestHandler):
|
||||
|
||||
def get(self):
|
||||
self.write(dict(
|
||||
version=version.VERSION,
|
||||
@ -432,8 +406,7 @@ class Settings(RequestHandler):
|
||||
|
||||
|
||||
class Application(tornado.web.Application):
|
||||
|
||||
def __init__(self, master, debug, wauthenticator):
|
||||
def __init__(self, master, debug):
|
||||
self.master = master
|
||||
handlers = [
|
||||
(r"/", IndexHandler),
|
||||
@ -449,7 +422,9 @@ class Application(tornado.web.Application):
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content", FlowContent),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)", FlowContentView),
|
||||
(
|
||||
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)",
|
||||
FlowContentView),
|
||||
(r"/settings", Settings),
|
||||
(r"/clear", ClearAll),
|
||||
]
|
||||
@ -460,6 +435,5 @@ class Application(tornado.web.Application):
|
||||
cookie_secret=os.urandom(256),
|
||||
debug=debug,
|
||||
autoreload=False,
|
||||
wauthenticator=wauthenticator,
|
||||
)
|
||||
super().__init__(handlers, **settings)
|
||||
|
@ -1,49 +1,20 @@
|
||||
import sys
|
||||
import webbrowser
|
||||
from typing import Optional
|
||||
|
||||
import tornado.httpserver
|
||||
import tornado.ioloop
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from mitmproxy import addons
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy.addons import view
|
||||
from mitmproxy.addons import intercept
|
||||
from mitmproxy import options
|
||||
from mitmproxy import log
|
||||
from mitmproxy import master
|
||||
from mitmproxy import options
|
||||
from mitmproxy.addons import eventstore
|
||||
from mitmproxy.addons import intercept
|
||||
from mitmproxy.addons import view
|
||||
from mitmproxy.tools.web import app
|
||||
|
||||
|
||||
class Stop(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _WebState():
|
||||
def add_log(self, e, level):
|
||||
# server-side log ids are odd
|
||||
self._last_event_id += 2
|
||||
entry = {
|
||||
"id": self._last_event_id,
|
||||
"message": e,
|
||||
"level": level
|
||||
}
|
||||
self.events.append(entry)
|
||||
app.ClientConnection.broadcast(
|
||||
resource="events",
|
||||
cmd="add",
|
||||
data=entry
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
super().clear()
|
||||
self.events.clear()
|
||||
app.ClientConnection.broadcast(
|
||||
resource="events",
|
||||
cmd="reset"
|
||||
)
|
||||
|
||||
|
||||
class Options(options.Options):
|
||||
def __init__(
|
||||
self,
|
||||
@ -52,54 +23,32 @@ class Options(options.Options):
|
||||
wdebug: bool = False,
|
||||
wport: int = 8081,
|
||||
wiface: str = "127.0.0.1",
|
||||
# wauthenticator: Optional[authentication.PassMan] = None,
|
||||
wsingleuser: Optional[str] = None,
|
||||
whtpasswd: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> None:
|
||||
self.intercept = intercept
|
||||
self.wdebug = wdebug
|
||||
self.wport = wport
|
||||
self.wiface = wiface
|
||||
# self.wauthenticator = wauthenticator
|
||||
# self.wsingleuser = wsingleuser
|
||||
# self.whtpasswd = whtpasswd
|
||||
self.intercept = intercept
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# TODO: This doesn't belong here.
|
||||
def process_web_options(self, parser):
|
||||
# if self.wsingleuser or self.whtpasswd:
|
||||
# if self.wsingleuser:
|
||||
# if len(self.wsingleuser.split(':')) != 2:
|
||||
# return parser.error(
|
||||
# "Invalid single-user specification. Please use the format username:password"
|
||||
# )
|
||||
# username, password = self.wsingleuser.split(':')
|
||||
# # self.wauthenticator = authentication.PassManSingleUser(username, password)
|
||||
# elif self.whtpasswd:
|
||||
# try:
|
||||
# self.wauthenticator = authentication.PassManHtpasswd(self.whtpasswd)
|
||||
# except ValueError as v:
|
||||
# return parser.error(v.message)
|
||||
# else:
|
||||
# self.wauthenticator = None
|
||||
pass
|
||||
|
||||
|
||||
class WebMaster(master.Master):
|
||||
|
||||
def __init__(self, options, server):
|
||||
super().__init__(options, server)
|
||||
self.view = view.View()
|
||||
self.view.sig_view_add.connect(self._sig_add)
|
||||
self.view.sig_view_remove.connect(self._sig_remove)
|
||||
self.view.sig_view_update.connect(self._sig_update)
|
||||
self.view.sig_view_refresh.connect(self._sig_refresh)
|
||||
self.view.sig_view_add.connect(self._sig_view_add)
|
||||
self.view.sig_view_remove.connect(self._sig_view_remove)
|
||||
self.view.sig_view_update.connect(self._sig_view_update)
|
||||
self.view.sig_view_refresh.connect(self._sig_view_refresh)
|
||||
|
||||
self.events = eventstore.EventStore()
|
||||
self.events.sig_add.connect(self._sig_events_add)
|
||||
self.events.sig_refresh.connect(self._sig_events_refresh)
|
||||
|
||||
self.addons.add(*addons.default_addons())
|
||||
self.addons.add(self.view, intercept.Intercept())
|
||||
self.addons.add(self.view, self.events, intercept.Intercept())
|
||||
self.app = app.Application(
|
||||
self, self.options.wdebug, False
|
||||
self, self.options.wdebug
|
||||
)
|
||||
# This line is just for type hinting
|
||||
self.options = self.options # type: Options
|
||||
@ -112,33 +61,46 @@ class WebMaster(master.Master):
|
||||
"error"
|
||||
)
|
||||
|
||||
def _sig_add(self, view, flow):
|
||||
def _sig_view_add(self, view, flow):
|
||||
app.ClientConnection.broadcast(
|
||||
resource="flows",
|
||||
cmd="add",
|
||||
data=app.convert_flow_to_json_dict(flow)
|
||||
data=app.flow_to_json(flow)
|
||||
)
|
||||
|
||||
def _sig_update(self, view, flow):
|
||||
def _sig_view_update(self, view, flow):
|
||||
app.ClientConnection.broadcast(
|
||||
resource="flows",
|
||||
cmd="update",
|
||||
data=app.convert_flow_to_json_dict(flow)
|
||||
data=app.flow_to_json(flow)
|
||||
)
|
||||
|
||||
def _sig_remove(self, view, flow):
|
||||
def _sig_view_remove(self, view, flow):
|
||||
app.ClientConnection.broadcast(
|
||||
resource="flows",
|
||||
cmd="remove",
|
||||
data=dict(id=flow.id)
|
||||
)
|
||||
|
||||
def _sig_refresh(self, view):
|
||||
def _sig_view_refresh(self, view):
|
||||
app.ClientConnection.broadcast(
|
||||
resource="flows",
|
||||
cmd="reset"
|
||||
)
|
||||
|
||||
def _sig_events_add(self, event_store, entry: log.LogEntry):
|
||||
app.ClientConnection.broadcast(
|
||||
resource="events",
|
||||
cmd="add",
|
||||
data=app.logentry_to_json(entry)
|
||||
)
|
||||
|
||||
def _sig_events_refresh(self, event_store):
|
||||
app.ClientConnection.broadcast(
|
||||
resource="events",
|
||||
cmd="reset"
|
||||
)
|
||||
|
||||
def run(self): # pragma: no cover
|
||||
|
||||
iol = tornado.ioloop.IOLoop.instance()
|
||||
@ -155,13 +117,9 @@ class WebMaster(master.Master):
|
||||
print("No webbrowser found. Please open a browser and point it to {}".format(url))
|
||||
|
||||
iol.start()
|
||||
except (Stop, KeyboardInterrupt):
|
||||
except (KeyboardInterrupt):
|
||||
self.shutdown()
|
||||
|
||||
# def add_log(self, e, level="info"):
|
||||
# super().add_log(e, level)
|
||||
# return self.state.add_log(e, level)
|
||||
|
||||
|
||||
def open_browser(url: str) -> bool:
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user