mirror of
https://github.com/vee1e/mitmproxy.git
synced 2026-09-01 18:27:18 +00:00
add command bar to mitmweb
This commit is contained in:
parent
e8fd816b97
commit
feb77ed7ea
8 changed files with 240 additions and 44 deletions
|
|
@ -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)
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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/"})
|
||||
|
|
|
|||
|
|
@ -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
17
web/src/css/command.less
Normal 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;
|
||||
}
|
||||
13
web/src/js/__tests__/components/Command/CommandSpec.js
Normal file
13
web/src/js/__tests__/components/Command/CommandSpec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
108
web/src/js/components/CommandBar.tsx
Normal file
108
web/src/js/components/CommandBar.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"/>
|
||||
)}
|
||||
|
|
|
|||
32
web/src/js/filt/command.peg
Normal file
32
web/src/js/filt/command.peg
Normal 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*
|
||||
Loading…
Add table
Add a link
Reference in a new issue