diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index c4cdb1448..b2dd4630a 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -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[0-9a-f\-]+)/duplicate", DuplicateFlow), (r"/flows/(?P[0-9a-f\-]+)/replay", ReplayFlow), (r"/flows/(?P[0-9a-f\-]+)/revert", RevertFlow), - (r"/flows/(?P[0-9a-f\-]+)/export/(?P[a-z][a-z_]+).json", ExportFlow), (r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response)/content.data", FlowContent), ( r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response)/content/(?P[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) ] ) diff --git a/web/gulpfile.js b/web/gulpfile.js index 85c960b20..fc12e56cc 100644 --- a/web/gulpfile.js +++ b/web/gulpfile.js @@ -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/"}) diff --git a/web/src/css/app.less b/web/src/css/app.less index 7fbf65044..644777f9a 100644 --- a/web/src/css/app.less +++ b/web/src/css/app.less @@ -20,3 +20,4 @@ html { @import (less) "contentview.less"; @import (less) "modal.less"; @import (less) "dropdown.less"; +@import (less) "command.less"; diff --git a/web/src/css/command.less b/web/src/css/command.less new file mode 100644 index 000000000..aeee32b4d --- /dev/null +++ b/web/src/css/command.less @@ -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; +} \ No newline at end of file diff --git a/web/src/js/__tests__/components/Command/CommandSpec.js b/web/src/js/__tests__/components/Command/CommandSpec.js new file mode 100644 index 000000000..46db151c2 --- /dev/null +++ b/web/src/js/__tests__/components/Command/CommandSpec.js @@ -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( + ), + tree = commandBar.toJSON() + + it('should render correctly', () => { + expect(tree).toMatchSnapshot() + }) +}) \ No newline at end of file diff --git a/web/src/js/components/CommandBar.tsx b/web/src/js/components/CommandBar.tsx new file mode 100644 index 000000000..bd0b1b920 --- /dev/null +++ b/web/src/js/components/CommandBar.tsx @@ -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 ( + <> +
+ Command Result +
+
+ {results.map(result => ( +
+ {result.result} +
+ ))} +
+ { nextArgs ?
Argument suggestion: {nextArgs.join(" ")}
: null } +
+ + + + +
+ + ) +} \ No newline at end of file diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx index f6b138b33..7ea21ee28 100644 --- a/web/src/js/components/ProxyApp.jsx +++ b/web/src/js/components/ProxyApp.jsx @@ -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 {
+ {showEventLog && ( )} diff --git a/web/src/js/filt/command.peg b/web/src/js/filt/command.peg new file mode 100644 index 000000000..0e734e29e --- /dev/null +++ b/web/src/js/filt/command.peg @@ -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* \ No newline at end of file