diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index 76d0a3f7d..3ac354f0b 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -27,10 +27,8 @@ from mitmproxy.utils.strutils import always_str def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: """ Remove flow message content and cert to save transmission space. - Args: flow: The original flow. - Sync with web/src/flow.ts. """ f = { @@ -454,12 +452,20 @@ class FlowContentView(RequestHandler): class Commands(RequestHandler): + def get(self): + commands = {} + for (name, command) in self.master.commands.commands.items(): + commands[name] = [] + for parameter in command.parameters: + commands[name].append({"name": parameter.name}) + self.write({"commands": commands}) + def post(self): result = self.master.commands.execute(self.json["command"]) if result is None: self.write({"result": ""}) return - self.write({"result": str(result)}) + self.write({ "result": result, "type": type(result).__name__ }) class Events(RequestHandler): @@ -479,16 +485,6 @@ 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: @@ -541,7 +537,7 @@ class Application(tornado.web.Application): (r"/", IndexHandler), (r"/filter-help(?:\.json)?", FilterHelp), (r"/updates", ClientConnection), - (r"/commands", Commands), + (r"/commands(?:\.json)?", Commands), (r"/events(?:\.json)?", Events), (r"/flows(?:\.json)?", Flows), (r"/flows/dump", DumpFlows), @@ -559,7 +555,6 @@ class Application(tornado.web.Application): r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response)/content/(?P[0-9a-zA-Z\-\_]+)(?:\.json)?", FlowContentView), (r"/clear", ClearAll), - (r"/arguments(?:\.json)?", CommandArguments), (r"/options(?:\.json)?", Options), (r"/options/save", SaveOptions), (r"/conf\.js", Conf), diff --git a/web/src/css/command.less b/web/src/css/command.less index aeee32b4d..9701e76b2 100644 --- a/web/src/css/command.less +++ b/web/src/css/command.less @@ -1,4 +1,4 @@ -.command { +.command-title { background-color: #F2F2F2; border: 1px solid #aaa; } @@ -14,4 +14,18 @@ .command-suggestion { background-color: #9c9c9c; +} + +.argument-suggestion { + background-color: hsla(209, 52%, 84%, 0.5) !important; +} + +.command > .popover { + display: block; + position: relative; + max-width: none; +} + +.available-commands { + overflow: auto; } \ 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 index 46db151c2..5f24639b7 100644 --- a/web/src/js/__tests__/components/Command/CommandSpec.js +++ b/web/src/js/__tests__/components/Command/CommandSpec.js @@ -1,13 +1,10 @@ import React from 'react' -import renderer from 'react-test-renderer' import CommandBar from '../../../components/CommandBar' +import { render } from "../../test-utils" -describe('CommandBar Component', () => { - let commandBar = renderer.create( - ), - tree = commandBar.toJSON() - - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) +test('CommandBar Component', async () => { + const {asFragment, store} = render( + + ); + expect(asFragment()).toMatchSnapshot(); }) \ No newline at end of file diff --git a/web/src/js/components/CommandBar.tsx b/web/src/js/components/CommandBar.tsx index 52f4ca252..70a0fda41 100644 --- a/web/src/js/components/CommandBar.tsx +++ b/web/src/js/components/CommandBar.tsx @@ -3,39 +3,76 @@ import classnames from 'classnames' import { Key, fetchApi } from '../utils' import Filt from '../filt/command' +export function AvailableCommands({input, commands}) { + if (!commands) return null + let availableCommands = [] + for (const [command, args] of Object.entries(commands)) { + if (command.startsWith(input)) { + availableCommands.push(command) + } + } + return
Available Commands: {JSON.stringify(availableCommands)}
+} + +export function ArgumentSuggestion({nextArgs, currentArg}){ + let results = [] + for (let i = 0; i < nextArgs.length; i++) { + if (i==currentArg) { + results.push({nextArgs[i]}) + continue + } + results.push({nextArgs[i]} ) + } + return (
+
+
+ Argument suggestion: {results} +
+
) +} + export default function CommandBar() { + const [input, setInput] = useState("") const [command, setCommand] = useState("") const [results, setResults] = useState([]) const [history, setHistory] = useState([]) const [currentPos, setCurrentPos] = useState(0) - const [args, setArgs] = useState({}) + const [allCommands, setAllCommands] = useState({}) const [nextArgs, setNextArgs] = useState([]) + const [currentArg, setCurrentArg] = useState(0) + const [commandHelp, setCommandHelp] = useState("") useEffect(() => { - fetchApi('/arguments') + fetchApi('/commands', { method: 'GET' }) .then(response => response.json()) - .then(data => setArgs(data)) + .then(data => setAllCommands(data)) }, []) const parseCommand = (input) => { const parts = Filt.parse(input) + if (allCommands["commands"].hasOwnProperty(parts[0])){ + setCommand(parts[0]) + } else { + setCommand("") + } - const nextArgs = args[parts[0]] + const nextArgs = allCommands["commands"][parts[0]]?.map(arg => arg.name) if (nextArgs) { setNextArgs([parts[0], ...nextArgs]) + setCurrentArg(parts.length-1) } } const onChange = (e) => { - setCommand(e.target.value) + setInput(e.target.value) } const onKeyDown = (e) => { if (e.keyCode === Key.ENTER) { - const body = {"command": command} + const body = {"command": input} const newHistory = Object.assign([], history) - newHistory.splice(currentPos, 0, command) + newHistory.splice(currentPos, 0, input) fetchApi(`/commands`, { method: 'POST', @@ -48,21 +85,21 @@ export default function CommandBar() { .then(data => { setHistory(newHistory) setCurrentPos(currentPos + 1) + setNextArgs([]) - if (data.result == "") return setResults([...results, {"id": results.length, "result": data.result}]) }) - setCommand("") + setInput("") } if (e.keyCode === Key.UP) { if (currentPos > 0) { - setCommand(history[currentPos - 1]) + setInput(history[currentPos - 1]) setCurrentPos(currentPos - 1) } } if (e.keyCode === Key.DOWN) { - setCommand(history[currentPos]) + setInput(history[currentPos]) if (currentPos < history.length -1) { setCurrentPos(currentPos + 1) } @@ -71,14 +108,14 @@ export default function CommandBar() { } const onKeyUp = (e) => { - if (command == "") return - parseCommand(command) + if (input == "") return + parseCommand(input) e.stopPropagation() } return ( - <> -
+
+
Command Result
@@ -88,7 +125,7 @@ export default function CommandBar() {
))}
- { nextArgs ?
Argument suggestion: {nextArgs.join(" ")}
: null } + { nextArgs.length > 0 && }
@@ -97,12 +134,13 @@ export default function CommandBar() { type="text" placeholder="Enter command" className="form-control" - value={command} + value={input} onChange={onChange} onKeyDown={onKeyDown} onKeyUp={onKeyUp} />
- + { !command && } +
) } \ No newline at end of file