From f481d964baf4aef39526265b206dd83f02ef308d Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Sat, 10 Sep 2022 02:19:24 +0930 Subject: [PATCH] [web] fix query editing fix #4565 --- CHANGELOG.md | 2 + mitmproxy/tools/web/app.py | 22 ++ test/mitmproxy/tools/web/test_app.py | 15 ++ .../contentviews/HttpMessageSpec.tsx | 65 +++++- .../__snapshots__/HttpMessageSpec.tsx.snap | 189 +++++++++++++++++- .../components/contentviews/HttpMessage.tsx | 26 ++- .../js/components/contentviews/useContent.tsx | 4 +- web/src/js/ducks/flows.ts | 7 + web/src/js/flow/utils.ts | 12 ++ 9 files changed, 330 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a102d6996..ac4bc27493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,8 @@ ([#5658](https://github.com/mitmproxy/mitmproxy/issues/5658), [#5661](https://github.com/mitmproxy/mitmproxy/issues/5661), @LIU-shuyi, @mhils) * Added Docs for Transparent Mode on Windows. ([#5402](https://github.com/mitmproxy/mitmproxy/issues/5402), @stephenspol) +* Fix query editing on mitmweb. + ([#5574](https://github.com/mitmproxy/mitmproxy/pull/5574), @yesmeck) ## 28 June 2022: mitmproxy 8.1.1 diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index 0c7e6677cb..83855dcd48 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -24,6 +24,7 @@ from mitmproxy import version from mitmproxy.dns import DNSFlow from mitmproxy.http import HTTPFlow +from mitmproxy.net.http import url from mitmproxy.tcp import TCPFlow, TCPMessage from mitmproxy.udp import UDPFlow, UDPMessage from mitmproxy.utils.emoji import emoji @@ -403,6 +404,8 @@ def put(self, flow_id): request.trailers.add(*trailer) elif k == "content": request.text = v + elif k == "query": + request.query = [tuple(i) for i in v] else: raise APIError(400, f"Unknown update request.{k}: {v}") @@ -485,6 +488,21 @@ def get(self, flow_id, message): self.write(message.get_content(strict=False)) +class FlowQuery(RequestHandler): + def post(self, flow_id, message): + self.flow.backup() + message = getattr(self.flow, message) + message.query = url.decode(b"&".join(self.filecontents.strip().splitlines())) + self.view.update([self.flow]) + + def get(self, flow_id, message): + message = getattr(self.flow, message) + self.set_header("Content-Type", "application/text") + self.set_header("X-Content-Type-Options", "nosniff") + self.set_header("X-Frame-Options", "DENY") + self.write("\n".join("=".join(field) for field in message.query.fields)) + + class FlowContentView(RequestHandler): def message_to_json( self, @@ -674,6 +692,10 @@ def __init__( r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response|messages)/content.data", FlowContent, ), + ( + r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response|messages)/query.data", + FlowQuery, + ), ( r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response|messages)/" r"content/(?P[0-9a-zA-Z\-\_%]+)(?:\.json)?", diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index 9a8ece62a0..38dc76a589 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -306,6 +306,7 @@ def test_flow_update(self): upd = { "request": { "trailers": [("foo", "baz")], + "query": [("foo", "2")] }, "response": { "trailers": [("foo", "baz")], @@ -313,6 +314,7 @@ def test_flow_update(self): } assert self.put_json("/flows/42", upd).code == 200 assert f.request.trailers["foo"] == "baz" + assert f.request.query["foo"] == "2" f.revert() @@ -390,6 +392,19 @@ def test_flow_content_returns_raw_content_when_decoding_fails(self): f.revert() + def test_flow_query(self): + f = self.view.get_by_id("42") + f.backup() + + f.request.query = (("foo", "1"), ("bar", "2")) + + r = self.fetch("/flows/42/request/query.data") + + assert r.code == 200 + assert r.body == b"foo=1\nbar=2" + + f.revert() + def test_update_flow_content(self): assert ( self.fetch("/flows/42/request/content.data", method="POST", body="new").code diff --git a/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx b/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx index fbb9d5ee84..6bdedfdd53 100644 --- a/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx +++ b/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx @@ -4,7 +4,20 @@ import HttpMessage, {ViewImage} from '../../../components/contentviews/HttpMessa import {fireEvent, render, screen, waitFor} from "../../test-utils" import fetchMock, {enableFetchMocks} from "jest-fetch-mock"; -jest.mock("../../../contrib/CodeMirror") +jest.mock("../../../contrib/CodeMirror", () => { + const React = require("react"); + return { + __esModule: true, + default: React.forwardRef((props, ref) => { + React.useImperativeHandle(ref, () => ({ + codeMirror: { + getValue: () => props.value + } + })); + return
{props.value}
+ }) + } +}) enableFetchMocks(); @@ -25,6 +38,11 @@ test("HttpMessage", async () => { description: "Raw", }), "raw content", + JSON.stringify({ + lines: Array(5).fill([["text", "rawdata"]]), + description: "Raw", + }), + "", JSON.stringify({ lines: Array(5).fill([["text", "rawdata"]]), description: "Raw", @@ -32,6 +50,7 @@ test("HttpMessage", async () => { ); const tflow = TFlow(); + tflow.request.method = "POST"; const {asFragment} = render(); await waitFor(() => screen.getAllByText("data")); expect(screen.queryByText('additional')).toBeNull(); @@ -50,6 +69,50 @@ test("HttpMessage", async () => { await waitFor(() => screen.getAllByText("rawdata")); expect(asFragment()).toMatchSnapshot(); + + fireEvent.click(screen.getByText("Edit")); + fireEvent.click(screen.getByText("Done")); + await waitFor(() => screen.getAllByText("rawdata")); + expect(asFragment()).toMatchSnapshot(); +}); + +test("HttpMessage edit query string", async () => { + const lines = [ + [ + ["header", "foo"], + ["text", "1"], + ], + [ + ["header", "bar"], + ["text", "2"], + ], + ]; + + fetchMock.mockResponses( + JSON.stringify({ + lines: lines, + description: "Query", + }), + "foo=1\nbar=2", + '', + JSON.stringify({ + lines, + description: "Query", + }) + ); + + const tflow = TFlow(); + tflow.request.path = "/path?foo=1&bar=2"; + const { asFragment, debug } = render( + + ); + fireEvent.click(screen.getByText("Edit")); + await waitFor(() => screen.getAllByText(/foo/)); + expect(asFragment()).toMatchSnapshot(); + fireEvent.click(screen.getByText("Done")); + + await waitFor(() => screen.getAllByText("foo")); + expect(asFragment()).toMatchSnapshot(); }); test("ViewImage", async () => { diff --git a/web/src/js/__tests__/components/contentviews/__snapshots__/HttpMessageSpec.tsx.snap b/web/src/js/__tests__/components/contentviews/__snapshots__/HttpMessageSpec.tsx.snap index b79c50132b..e5014d4098 100644 --- a/web/src/js/__tests__/components/contentviews/__snapshots__/HttpMessageSpec.tsx.snap +++ b/web/src/js/__tests__/components/contentviews/__snapshots__/HttpMessageSpec.tsx.snap @@ -126,7 +126,11 @@ exports[`HttpMessage 2`] = `
+ > +
+ {"lines":[[["text","rawdata"]],[["text","rawdata"]],[["text","rawdata"]],[["text","rawdata"]],[["text","rawdata"]]],"description":"Raw"} +
+
`; @@ -226,6 +230,189 @@ exports[`HttpMessage 3`] = ` `; +exports[`HttpMessage 4`] = ` + +
+
+
+ Loading... +
+ +   + + + Replace + + +   + + + +   + + View: + + raw + + + +
+
+
+`; + +exports[`HttpMessage edit query string 1`] = ` + +
+
+
+ [Editing] +
+ +   + +
+
+
+ foo=1 +bar=2 +
+
+
+
+`; + +exports[`HttpMessage edit query string 2`] = ` + +
+
+
+ Query +
+ +   + + + Replace + + +   + + + +   + + View: + + auto + + + +
+
+      
+ + foo + + + 1 + +
+
+ + bar + + + 2 + +
+
+
+
+`; + exports[`ViewImage 1`] = `
(useAppSelector(state => state.options.content_view_lines_cutoff)); const showMore = useCallback(() => setMaxLines(Math.max(1024, maxLines * 2)), [maxLines]); const [edit, setEdit] = useState(false); + const isGETRequest = flow.request.method === 'GET' && part === 'request'; let url; if (edit) { - url = MessageUtils.getContentURL(flow, message); + url = isGETRequest ? MessageUtils.getQueryURL(flow, message) : MessageUtils.getContentURL(flow, message); } else { url = MessageUtils.getContentURL(flow, message, contentView, maxLines + 1); } - const content = useContent(url, message.contentHash); + let content = useContent(url, message.contentHash, flow.request.path); + const contentViewData = useMemo(() => { if (content && !edit) { try { @@ -48,8 +50,12 @@ export default function HttpMessage({flow, message}: HttpMessageProps) { if (edit) { const save = async () => { - const content = editorRef.current?.getContent(); - await dispatch(flowActions.update(flow, {[part]: {content}})); + let content = editorRef.current?.getContent(); + await dispatch(flowActions.update(flow, { + [part]: isGETRequest ? { + query: content?.split("\n").map(item => item.split('=')) + } : { content } + })); setEdit(false); } return ( @@ -76,7 +82,11 @@ export default function HttpMessage({flow, message}: HttpMessageProps) { icon="fa-upload" text="Replace" title="Upload a file to replace the content." - onOpenFile={content => dispatch(uploadContent(flow, content, part))} + onOpenFile={content => dispatch( + isGETRequest ? + uploadQuery(flow, content, part) : + uploadContent(flow, content, part) + )} className="btn btn-default btn-xs"/>   (), [abort, setAbort] = useState(); @@ -42,7 +42,7 @@ export function useContent(url: string, hash?: string): string | undefined { controller.abort(); } }, - [url, hash] + [url, hash, path] ); return content; diff --git a/web/src/js/ducks/flows.ts b/web/src/js/ducks/flows.ts index fa4ce9b6cc..924bc178a7 100644 --- a/web/src/js/ducks/flows.ts +++ b/web/src/js/ducks/flows.ts @@ -202,6 +202,13 @@ export function uploadContent(flow: Flow, file, type) { return dispatch => fetchApi(`/flows/${flow.id}/${type}/content.data`, {method: 'POST', body}) } +export function uploadQuery(flow: Flow, file, type) { + const body = new FormData() + file = new window.Blob([file], {type: 'plain/text'}) + body.append('file', file) + return dispatch => fetchApi(`/flows/${flow.id}/${type}/query.data`, {method: 'POST', body}) +} + export function clear() { return dispatch => fetchApi('/clear', {method: 'POST'}) diff --git a/web/src/js/flow/utils.ts b/web/src/js/flow/utils.ts index 9461233a0c..c874a93c21 100644 --- a/web/src/js/flow/utils.ts +++ b/web/src/js/flow/utils.ts @@ -66,6 +66,18 @@ export class MessageUtils { const lineStr = lines ? `?lines=${lines}` : ""; return `./flows/${flow.id}/${part}/` + (view ? `content/${encodeURIComponent(view)}.json${lineStr}` : 'content.data'); } + + static getQueryURL( + flow: Flow, + part: HTTPMessage | "request" | "response" | "messages", + ): string { + if (flow.type === "http" && part === flow.request) { + part = "request"; + } else if (flow.type === "http" && part === flow.response) { + part = "response"; + } + return `./flows/${flow.id}/${part}/query.data`; + } } export class RequestUtils extends MessageUtils {