add command bar to mitmweb

This commit is contained in:
zokutyou2@gmail.com 2021-06-25 22:06:35 +09:00
parent e8fd816b97
commit feb77ed7ea
8 changed files with 240 additions and 44 deletions

View file

@ -20,7 +20,6 @@ from mitmproxy import io
from mitmproxy import log
from mitmproxy import optmanager
from mitmproxy import version
from mitmproxy.addons import export
from mitmproxy.utils.strutils import always_str
@ -30,8 +29,6 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
Args:
flow: The original flow.
Sync with web/src/flow.ts.
"""
f = {
"id": flow.id,
@ -45,42 +42,31 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
if flow.client_conn:
f["client_conn"] = {
"id": flow.client_conn.id,
"peername": flow.client_conn.peername,
"sockname": flow.client_conn.sockname,
"address": flow.client_conn.peername,
"tls_established": flow.client_conn.tls_established,
"sni": flow.client_conn.sni,
"cipher": flow.client_conn.cipher,
"alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
"tls_version": flow.client_conn.tls_version,
"timestamp_start": flow.client_conn.timestamp_start,
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
"timestamp_end": flow.client_conn.timestamp_end,
# Legacy properties
"address": flow.client_conn.peername,
"sni": flow.client_conn.sni,
"cipher_name": flow.client_conn.cipher,
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
"tls_version": flow.client_conn.tls_version,
}
if flow.server_conn:
f["server_conn"] = {
"id": flow.server_conn.id,
"peername": flow.server_conn.peername,
"sockname": flow.server_conn.sockname,
"address": flow.server_conn.address,
"ip_address": flow.server_conn.peername,
"source_address": flow.server_conn.sockname,
"tls_established": flow.server_conn.tls_established,
"sni": flow.server_conn.sni,
"cipher": flow.server_conn.cipher,
"alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
"tls_version": flow.server_conn.tls_version,
"timestamp_start": flow.server_conn.timestamp_start,
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
"timestamp_end": flow.server_conn.timestamp_end,
# Legacy properties
"ip_address": flow.server_conn.peername,
"source_address": flow.server_conn.sockname,
"alpn_proto_negotiated": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
}
if flow.error:
f["error"] = flow.error.get_state()
@ -219,7 +205,7 @@ class IndexHandler(RequestHandler):
def get(self):
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
assert token
self.render("index.html", static=False, version=version.VERSION)
self.render("index.html")
class FilterHelp(RequestHandler):
@ -280,14 +266,6 @@ class DumpFlows(RequestHandler):
bio.close()
class ExportFlow(RequestHandler):
def post(self, flow_id, format):
out = export.formats[format](self.flow)
self.write({
"export": always_str(out, "utf8", "backslashreplace")
})
class ClearAll(RequestHandler):
def post(self):
self.view.clear()
@ -453,11 +431,56 @@ class FlowContentView(RequestHandler):
))
class Commands(RequestHandler):
def post(self):
result = self.master.commands.execute(self.json["command"])
if result is None:
self.write({"result": ""})
return
self.write({"result": str(result)})
class Events(RequestHandler):
def get(self):
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,
mode=str(self.master.options.mode),
intercept_active=self.master.options.intercept_active,
intercept=self.master.options.intercept,
showhost=self.master.options.showhost,
upstream_cert=self.master.options.upstream_cert,
rawtcp=self.master.options.rawtcp,
http2=self.master.options.http2,
websocket=self.master.options.websocket,
anticache=self.master.options.anticache,
anticomp=self.master.options.anticomp,
stickyauth=self.master.options.stickyauth,
stickycookie=self.master.options.stickycookie,
stream=self.master.options.stream_large_bodies,
contentViews=[v.name.replace(' ', '_') for v in contentviews.views],
listen_host=self.master.options.listen_host,
listen_port=self.master.options.listen_port,
server=self.master.options.server,
))
def put(self):
update = self.json
allowed_options = {
"intercept", "showhost", "upstream_cert", "ssl_insecure",
"rawtcp", "http2", "websocket", "anticache", "anticomp",
"stickycookie", "stickyauth", "stream_large_bodies"
}
for k in update:
if k not in allowed_options:
raise APIError(400, f"Unknown setting {k}")
self.master.options.update(**update)
class Options(RequestHandler):
def get(self):
self.write(optmanager.dump_dicts(self.master.options))
@ -470,6 +493,16 @@ class Options(RequestHandler):
raise APIError(400, f"{err}")
class CommandArguments(RequestHandler):
def get(self):
arguments = {}
for (name, command) in self.master.commands.commands.items():
arguments[name] = []
for parameter in command.parameters:
arguments[name].append(parameter.name)
self.write(arguments)
class SaveOptions(RequestHandler):
def post(self):
# try:
@ -488,17 +521,6 @@ class DnsRebind(RequestHandler):
)
class Conf(RequestHandler):
def get(self):
conf = {
"static": False,
"version": version.VERSION,
"contentViews": [v.name for v in contentviews.views]
}
self.write(f"MITMWEB_CONF = {json.dumps(conf)};")
self.set_header("content-type", "application/javascript")
class Application(tornado.web.Application):
master: "mitmproxy.tools.web.master.WebMaster"
@ -522,6 +544,7 @@ class Application(tornado.web.Application):
(r"/", IndexHandler),
(r"/filter-help(?:\.json)?", FilterHelp),
(r"/updates", ClientConnection),
(r"/commands", Commands),
(r"/events(?:\.json)?", Events),
(r"/flows(?:\.json)?", Flows),
(r"/flows/dump", DumpFlows),
@ -533,14 +556,14 @@ class Application(tornado.web.Application):
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
(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\-]+)/export/(?P<format>[a-z][a-z_]+).json", ExportFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content.data", FlowContent),
(
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
FlowContentView),
(r"/settings(?:\.json)?", Settings),
(r"/arguments(?:\.json)?", CommandArguments),
(r"/clear", ClearAll),
(r"/options(?:\.json)?", Options),
(r"/options/save", SaveOptions),
(r"/conf\.js", Conf),
(r"/options/save", SaveOptions)
]
)

View file

@ -78,7 +78,7 @@ function templates() {
.pipe(gulp.dest("../mitmproxy/tools/web"));
}
const peg_src = "src/js/filt/filt.peg";
const peg_src = "src/js/filt/*.peg";
function peg() {
return gulp.src(peg_src, {base: "src/"})

View file

@ -20,3 +20,4 @@ html {
@import (less) "contentview.less";
@import (less) "modal.less";
@import (less) "dropdown.less";
@import (less) "command.less";

17
web/src/css/command.less Normal file
View file

@ -0,0 +1,17 @@
.command {
background-color: #F2F2F2;
border: 1px solid #aaa;
}
.command-result {
display: block;
margin: 0px;
background-color: #fcfcfc;
height: 100px;
max-height: 100px;
overflow: auto;
}
.command-suggestion {
background-color: #9c9c9c;
}

View file

@ -0,0 +1,13 @@
import React from 'react'
import renderer from 'react-test-renderer'
import CommandBar from '../../../components/CommandBar'
describe('CommandBar Component', () => {
let commandBar = renderer.create(
<CommandBar />),
tree = commandBar.toJSON()
it('should render correctly', () => {
expect(tree).toMatchSnapshot()
})
})

View file

@ -0,0 +1,108 @@
import React, { useState, useEffect, Component } from 'react'
import classnames from 'classnames'
import { Key, fetchApi } from '../utils.js'
import Filt from '../filt/command'
export default function CommandBar() {
const [command, setCommand] = useState("")
const [results, setResults] = useState([])
const [history, setHistory] = useState([])
const [currentPos, setCurrentPos] = useState(0)
const [args, setArgs] = useState({})
const [nextArgs, setNextArgs] = useState([])
useEffect(() => {
fetchApi('/arguments')
.then(response => response.json())
.then(data => setArgs(data))
}, [])
const parseCommand = (input) => {
const parts = Filt.parse(input)
const nextArgs = args[parts[0]]
if (nextArgs) {
setNextArgs([parts[0], ...nextArgs])
}
}
const onChange = (e) => {
setCommand(e.target.value)
}
const onKeyDown = (e) => {
if (e.keyCode === Key.ENTER) {
const body = {"command": command}
const newHistory = Object.assign([], history)
newHistory.splice(currentPos, 0, command)
fetchApi(`/commands`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
setHistory(newHistory)
setCurrentPos(currentPos + 1)
if (data.result == "") return
setResults([...results, {"id": results.length, "result": data.result}])
})
setCommand("")
}
if (e.keyCode === Key.UP) {
if (currentPos > 0) {
setCommand(history[currentPos - 1])
setCurrentPos(currentPos - 1)
}
}
if (e.keyCode === Key.DOWN) {
setCommand(history[currentPos])
if (currentPos < history.length -1) {
setCurrentPos(currentPos + 1)
}
}
e.stopPropagation()
}
const onKeyUp = (e) => {
if (command == "") return
parseCommand(command)
e.stopPropagation()
}
return (
<>
<div className="command">
Command Result
</div>
<div className="command-result">
{results.map(result => (
<div key={result.id}>
{result.result}
</div>
))}
</div>
{ nextArgs ? <div className="command-suggestion">Argument suggestion: {nextArgs.join(" ")}</div> : null }
<div className={classnames('command-input input-group')}>
<span className="input-group-addon">
<i className={'fa fa-fw fa-terminal'}/>
</span>
<input
type="text"
placeholder="Enter command"
className="form-control"
value={command}
onChange={onChange}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
</div>
</>
)
}

View file

@ -5,6 +5,7 @@ import { connect } from 'react-redux'
import { onKeyDown } from '../ducks/ui/keyboard'
import MainView from './MainView'
import Header from './Header'
import CommandBar from './CommandBar'
import EventLog from './EventLog'
import Footer from './Footer'
import Modal from './Modal/Modal'
@ -25,6 +26,7 @@ class ProxyAppMain extends Component {
<div id="container" tabIndex="0">
<Header/>
<MainView />
<CommandBar />
{showEventLog && (
<EventLog key="eventlog"/>
)}

View file

@ -0,0 +1,32 @@
Expr
= s:StringLiteral ws+ rest:Expr { return [s, ...rest]; }
/ s:StringLiteral { return [s]; }
/ ws* { return ""; }
StringLiteral "string"
= '"' chars:DoubleStringChar* '"' { return chars.join(""); }
/ "'" chars:SingleStringChar* "'" { return chars.join(""); }
/ !cc chars:UnquotedStringChar+ { return chars.join(""); }
/ '"' chars:DoubleStringChar* { return chars.join(""); }
/ "'" chars:SingleStringChar* { return chars.join(""); }
DoubleStringChar
= !["\\] char:. { return char; }
/ "\\" char:EscapeSequence { return char; }
SingleStringChar
= !['\\] char:. { return char; }
/ "\\" char:EscapeSequence { return char; }
UnquotedStringChar
= !ws char:. { return char; }
EscapeSequence
= ['"\\]
/ "n" { return "\n"; }
/ "r" { return "\r"; }
/ "t" { return "\t"; }
ws "whitespace" = [ \t\n\r]
cc "control character" = [|&!()~"]
__ "optional whitespace" = ws*