From 30e7590ba3a6834a3d5103706cd9783462756858 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 14 Aug 2024 16:54:25 +0200 Subject: [PATCH 01/96] Capture tab nits (#7102) * capture tab: style 'add' button * capture tab: improve message during mode startup * capture tab: make first reverse proxy irremovable * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- web/src/css/mode.less | 7 +++++++ web/src/js/components/Modes/CaptureSetup.tsx | 5 +---- web/src/js/components/Modes/Reverse.tsx | 3 ++- web/src/js/components/Modes/ReverseToggleRow.tsx | 14 +++++++++----- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/web/src/css/mode.less b/web/src/css/mode.less index c9073d1c0..e6ca52f05 100644 --- a/web/src/css/mode.less +++ b/web/src/css/mode.less @@ -98,6 +98,13 @@ cursor: pointer; font-weight: 500; + opacity: 0.5; + transition: all 50ms ease-in-out; + + &:hover { + opacity: 1; + } + i { font-size: 2rem; margin-right: 5px; diff --git a/web/src/js/components/Modes/CaptureSetup.tsx b/web/src/js/components/Modes/CaptureSetup.tsx index fae840592..aa72e02b7 100644 --- a/web/src/js/components/Modes/CaptureSetup.tsx +++ b/web/src/js/components/Modes/CaptureSetup.tsx @@ -22,7 +22,6 @@ function ServerDescription({ description, listen_addrs, is_running, - full_spec, wireguard_conf, type, }: ServerInfo) { @@ -53,9 +52,7 @@ function ServerDescription({ if (!is_running) { desc = ( <> -
- {description} ({full_spec}) -
+
{description} starting...
); } else { diff --git a/web/src/js/components/Modes/Reverse.tsx b/web/src/js/components/Modes/Reverse.tsx index c6bd28c6f..e1992e361 100644 --- a/web/src/js/components/Modes/Reverse.tsx +++ b/web/src/js/components/Modes/Reverse.tsx @@ -17,9 +17,10 @@ export default function Reverse() { Requests are forwarded to a preconfigured destination.

- {servers.map((server) => ( + {servers.map((server, i) => ( 0} server={server} backendState={backendState[getSpec(server)]} /> diff --git a/web/src/js/components/Modes/ReverseToggleRow.tsx b/web/src/js/components/Modes/ReverseToggleRow.tsx index e2ccc8644..2d582ad24 100644 --- a/web/src/js/components/Modes/ReverseToggleRow.tsx +++ b/web/src/js/components/Modes/ReverseToggleRow.tsx @@ -17,11 +17,13 @@ import { ServerStatus } from "./CaptureSetup"; import { ServerInfo } from "../../ducks/backendState"; interface ReverseToggleRowProps { + removable: boolean; server: ReverseState; backendState?: ServerInfo; } export default function ReverseToggleRow({ + removable, server, backendState, }: ReverseToggleRowProps) { @@ -101,11 +103,13 @@ export default function ReverseToggleRow({ } placeholder="example.com" /> - + {removable && ( + + )}
From 0f0c5ee250369476a689eea83e82b042c223ead0 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 14 Aug 2024 17:02:14 +0200 Subject: [PATCH 02/96] Fix urwid deprecation warnings (#7098) * migrate to new urwid apis * AttrWrap -> AttrMap --- mitmproxy/tools/console/commands.py | 7 +++-- mitmproxy/tools/console/common.py | 2 +- mitmproxy/tools/console/flowview.py | 4 +-- mitmproxy/tools/console/grideditor/base.py | 26 +++++++++---------- .../tools/console/grideditor/col_bytes.py | 2 +- mitmproxy/tools/console/keybindings.py | 16 +++++------- mitmproxy/tools/console/options.py | 12 ++++----- mitmproxy/tools/console/overlay.py | 8 +++--- mitmproxy/tools/console/statusbar.py | 2 +- mitmproxy/tools/console/tabs.py | 7 +++-- mitmproxy/tools/console/window.py | 6 ++--- test/mitmproxy/tools/console/conftest.py | 2 +- 12 files changed, 43 insertions(+), 51 deletions(-) diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py index ee1049254..67c1f1899 100644 --- a/mitmproxy/tools/console/commands.py +++ b/mitmproxy/tools/console/commands.py @@ -15,8 +15,7 @@ command_focus_change = utils_signals.SyncSignal(lambda text: None) class CommandItem(urwid.WidgetWrap): def __init__(self, walker, cmd: command.Command, focused: bool): self.walker, self.cmd, self.focused = walker, cmd, focused - super().__init__(None) - self._w = self.get_widget() + super().__init__(self.get_widget()) def get_widget(self): parts = [("focus", ">> " if self.focused else " "), ("title", self.cmd.name)] @@ -112,14 +111,14 @@ class CommandHelp(urwid.Frame): def set_active(self, val): h = urwid.Text("Command Help") style = "heading" if val else "heading_inactive" - self.header = urwid.AttrWrap(h, style) + self.header = urwid.AttrMap(h, style) def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def sig_mod(self, txt): - self.set_body(self.widget(txt)) + self.body = self.widget(txt) class Commands(urwid.Pile, layoutwidget.LayoutWidget): diff --git a/mitmproxy/tools/console/common.py b/mitmproxy/tools/console/common.py index 66534ef94..651e22317 100644 --- a/mitmproxy/tools/console/common.py +++ b/mitmproxy/tools/console/common.py @@ -188,7 +188,7 @@ class TruncatedText(urwid.Widget): text = text[::-1] attr = attr[::-1] - text_len = urwid.util.calc_width(text, 0, len(text)) + text_len = urwid.calc_width(text, 0, len(text)) if size is not None and len(size) > 0: width = size[0] else: diff --git a/mitmproxy/tools/console/flowview.py b/mitmproxy/tools/console/flowview.py index 1ae54cdc6..a57ead740 100644 --- a/mitmproxy/tools/console/flowview.py +++ b/mitmproxy/tools/console/flowview.py @@ -201,7 +201,7 @@ class FlowDetails(tabs.Tabs): align="right", ), ] - contentview_status_bar = urwid.AttrWrap(urwid.Columns(cols), "heading") + contentview_status_bar = urwid.AttrMap(urwid.Columns(cols), "heading") return contentview_status_bar FROM_CLIENT_MARKER = ("from_client", f"{common.SYMBOL_FROM_CLIENT} ") @@ -412,7 +412,7 @@ class FlowDetails(tabs.Tabs): align="right", ), ] - title = urwid.AttrWrap(urwid.Columns(cols), "heading") + title = urwid.AttrMap(urwid.Columns(cols), "heading") txt.append(title) txt.extend(body) diff --git a/mitmproxy/tools/console/grideditor/base.py b/mitmproxy/tools/console/grideditor/base.py index 46d4a2eec..f03be6327 100644 --- a/mitmproxy/tools/console/grideditor/base.py +++ b/mitmproxy/tools/console/grideditor/base.py @@ -99,11 +99,11 @@ class GridRow(urwid.WidgetWrap): w = self.editor.columns[i].Display(v) if focused == i: if i in errors: - w = urwid.AttrWrap(w, "focusfield_error") + w = urwid.AttrMap(w, "focusfield_error") else: - w = urwid.AttrWrap(w, "focusfield") + w = urwid.AttrMap(w, "focusfield") elif i in errors: - w = urwid.AttrWrap(w, "field_error") + w = urwid.AttrMap(w, "field_error") self.fields.append(w) fspecs = self.fields[:] @@ -111,7 +111,7 @@ class GridRow(urwid.WidgetWrap): fspecs[0] = ("fixed", self.editor.first_width + 2, fspecs[0]) w = urwid.Columns(fspecs, dividechars=2) if focused is not None: - w.set_focus_column(focused) + w.focus_position = focused super().__init__(w) def keypress(self, s, k): @@ -295,7 +295,7 @@ class BaseGridEditor(urwid.WidgetWrap): else: headings.append(c) h = urwid.Columns(headings, dividechars=2) - h = urwid.AttrWrap(h, "heading") + h = urwid.AttrMap(h, "heading") self.walker = GridWalker(self.value, self) self.lb = GridListBox(self.walker) @@ -313,16 +313,14 @@ class BaseGridEditor(urwid.WidgetWrap): def show_empty_msg(self): if self.walker.lst: - self._w.set_footer(None) + self._w.footer = None else: - self._w.set_footer( - urwid.Text( - [ - ("highlight", "No values - you should add some. Press "), - ("key", "?"), - ("highlight", " for help."), - ] - ) + self._w.footer = urwid.Text( + [ + ("highlight", "No values - you should add some. Press "), + ("key", "?"), + ("highlight", " for help."), + ] ) def set_subeditor_value(self, val, focus, focus_col): diff --git a/mitmproxy/tools/console/grideditor/col_bytes.py b/mitmproxy/tools/console/grideditor/col_bytes.py index 9af1a3544..7ab97ea68 100644 --- a/mitmproxy/tools/console/grideditor/col_bytes.py +++ b/mitmproxy/tools/console/grideditor/col_bytes.py @@ -37,7 +37,7 @@ class Edit(base.Cell): def __init__(self, data: bytes) -> None: d = strutils.bytes_to_escaped_str(data) w = urwid.Edit(edit_text=d, wrap="any", multiline=True) - w = urwid.AttrWrap(w, "editfield") + w = urwid.AttrMap(w, "editfield") super().__init__(w) def get_data(self) -> bytes: diff --git a/mitmproxy/tools/console/keybindings.py b/mitmproxy/tools/console/keybindings.py index 4a200833f..a9d723765 100644 --- a/mitmproxy/tools/console/keybindings.py +++ b/mitmproxy/tools/console/keybindings.py @@ -12,8 +12,7 @@ HELP_HEIGHT = 5 class KeyItem(urwid.WidgetWrap): def __init__(self, walker, binding, focused): self.walker, self.binding, self.focused = walker, binding, focused - super().__init__(None) - self._w = self.get_widget() + super().__init__(self.get_widget()) def get_widget(self): cmd = textwrap.dedent(self.binding.command).strip() @@ -116,14 +115,14 @@ class KeyHelp(urwid.Frame): def set_active(self, val): h = urwid.Text("Key Binding Help") style = "heading" if val else "heading_inactive" - self.header = urwid.AttrWrap(h, style) + self.header = urwid.AttrMap(h, style) def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def sig_mod(self, txt): - self.set_body(self.widget(txt)) + self.body = self.widget(txt) class KeyBindings(urwid.Pile, layoutwidget.LayoutWidget): @@ -146,13 +145,13 @@ class KeyBindings(urwid.Pile, layoutwidget.LayoutWidget): def get_focused_binding(self): if self.focus_position != 0: return None - f = self.widget_list[0] + f = self.contents[0][0] return f.walker.get_focus()[0].binding def keypress(self, size, key): if key == "m_next": self.focus_position = (self.focus_position + 1) % len(self.widget_list) - self.widget_list[1].set_active(self.focus_position == 1) + self.contents[1][0].set_active(self.focus_position == 1) key = None # This is essentially a copypasta from urwid.Pile's keypress handler. @@ -160,6 +159,5 @@ class KeyBindings(urwid.Pile, layoutwidget.LayoutWidget): item_rows = None if len(size) == 2: item_rows = self.get_item_rows(size, focus=True) - i = self.widget_list.index(self.focus_item) - tsize = self.get_item_size(size, i, True, item_rows) - return self.focus_item.keypress(tsize, key) + tsize = self.get_item_size(size, self.focus_position, True, item_rows) + return self.focus.keypress(tsize, key) diff --git a/mitmproxy/tools/console/options.py b/mitmproxy/tools/console/options.py index e54b3a588..1e52cca25 100644 --- a/mitmproxy/tools/console/options.py +++ b/mitmproxy/tools/console/options.py @@ -34,8 +34,7 @@ class OptionItem(urwid.WidgetWrap): self.walker, self.opt, self.focused = walker, opt, focused self.namewidth = namewidth self.editing = editing - super().__init__(None) - self._w = self.get_widget() + super().__init__(self.get_widget()) def get_widget(self): val = self.opt.current() @@ -232,14 +231,14 @@ class OptionHelp(urwid.Frame): def set_active(self, val): h = urwid.Text("Option Help") style = "heading" if val else "heading_inactive" - self.header = urwid.AttrWrap(h, style) + self.header = urwid.AttrMap(h, style) def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def update_help_text(self, txt: str) -> None: - self.set_body(self.widget(txt)) + self.body = self.widget(txt) class Options(urwid.Pile, layoutwidget.LayoutWidget): @@ -274,6 +273,5 @@ class Options(urwid.Pile, layoutwidget.LayoutWidget): item_rows = None if len(size) == 2: item_rows = self.get_item_rows(size, focus=True) - i = self.widget_list.index(self.focus_item) - tsize = self.get_item_size(size, i, True, item_rows) - return self.focus_item.keypress(tsize, key) + tsize = self.get_item_size(size, self.focus_position, True, item_rows) + return self.focus.keypress(tsize, key) diff --git a/mitmproxy/tools/console/overlay.py b/mitmproxy/tools/console/overlay.py index 17b55bc10..fad1dd444 100644 --- a/mitmproxy/tools/console/overlay.py +++ b/mitmproxy/tools/console/overlay.py @@ -45,7 +45,7 @@ class Choice(urwid.WidgetWrap): else: s = "option_selected" if focus else "text" super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.Padding(urwid.Text(txt)), s, ) @@ -107,7 +107,7 @@ class Chooser(urwid.WidgetWrap, layoutwidget.LayoutWidget): self.walker = ChooserListWalker(choices, current) super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.LineBox( urwid.BoxAdapter(urwid.ListBox(self.walker), len(choices)), title=title, @@ -152,7 +152,7 @@ class OptionsOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget): cols, rows = master.ui.get_cols_rows() self.ge = grideditor.OptionsEditor(master, name, vals) super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.LineBox(urwid.BoxAdapter(self.ge, rows - vspace), title=name), "background", ) @@ -176,7 +176,7 @@ class DataViewerOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget): cols, rows = master.ui.get_cols_rows() self.ge = grideditor.DataViewer(master, vals) super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.LineBox(urwid.BoxAdapter(self.ge, rows - 5), title="Data viewer"), "background", ) diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index 71b5d41d9..705427647 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -332,7 +332,7 @@ class StatusBar(urwid.WidgetWrap): else: boundaddr = "" t.extend(self.get_status()) - status = urwid.AttrWrap( + status = urwid.AttrMap( urwid.Columns( [ urwid.Text(t), diff --git a/mitmproxy/tools/console/tabs.py b/mitmproxy/tools/console/tabs.py index a3ba09a10..adf9c481f 100644 --- a/mitmproxy/tools/console/tabs.py +++ b/mitmproxy/tools/console/tabs.py @@ -8,7 +8,7 @@ class Tab(urwid.WidgetWrap): """ p = urwid.Text(content, align="center") p = urwid.Padding(p, align="center", width=("relative", 100)) - p = urwid.AttrWrap(p, attr) + p = urwid.AttrMap(p, attr) urwid.WidgetWrap.__init__(self, p) self.offset = offset self.onclick = onclick @@ -21,11 +21,10 @@ class Tab(urwid.WidgetWrap): class Tabs(urwid.WidgetWrap): def __init__(self, tabs, tab_offset=0): - super().__init__("") + super().__init__(urwid.Pile([])) self.tab_offset = tab_offset self.tabs = tabs self.show() - self._w = urwid.Pile([]) def change_tab(self, offset): self.tab_offset = offset @@ -56,4 +55,4 @@ class Tabs(urwid.WidgetWrap): self._w = urwid.Frame( body=self.tabs[self.tab_offset % len(self.tabs)][1](), header=headers ) - self._w.set_focus("body") + self._w.focus_position = "body" diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py index 6c2e24926..8c3a6de1f 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -23,7 +23,7 @@ class StackWidget(urwid.Frame): self.window = window if title: - header = urwid.AttrWrap( + header = urwid.AttrMap( urwid.Text(title), "heading" if focus else "heading_inactive" ) else: @@ -129,7 +129,7 @@ class Window(urwid.Frame): def __init__(self, master): self.statusbar = statusbar.StatusBar(master) super().__init__( - None, header=None, footer=urwid.AttrWrap(self.statusbar, "background") + None, header=None, footer=urwid.AttrMap(self.statusbar, "background") ) self.master = master self.master.view.sig_view_refresh.connect(self.view_changed) @@ -185,7 +185,7 @@ class Window(urwid.Frame): focus_column=self.pane, ) - self.body = urwid.AttrWrap(w, "background") + self.body = urwid.AttrMap(w, "background") signals.window_refresh.send() def flow_changed(self, flow: flow.Flow) -> None: diff --git a/test/mitmproxy/tools/console/conftest.py b/test/mitmproxy/tools/console/conftest.py index f17301f5f..9a5fe85b8 100644 --- a/test/mitmproxy/tools/console/conftest.py +++ b/test/mitmproxy/tools/console/conftest.py @@ -30,7 +30,7 @@ class ConsoleTestMaster(ConsoleMaster): self.window.keypress(self.ui.get_cols_rows(), key) def screen_contents(self) -> str: - return b"\n".join(self.window.render((80, 24), True)._text_content()).decode() + return b"\n".join(self.window.render((80, 24), True).text).decode() @pytest.fixture From c8cbb71f5665b6b5c857fe5dc56f930ef44dde11 Mon Sep 17 00:00:00 2001 From: Matteo Luppi <100372313+lups2000@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:18:23 +0200 Subject: [PATCH 03/96] Feature/socks and transparent modes (#7100) * add transparent mode * add tests transparent * add socks mode * update tests socks * [autofix.ci] apply automated fixes * review changes * adapt socks and transparent to new server status logic * review changes * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- web/gen/state_js.py | 2 +- web/src/js/__tests__/ducks/_tbackendstate.ts | 2 +- .../js/__tests__/ducks/modes/socksSpec.tsx | 116 +++++++++++++++++ .../__tests__/ducks/modes/transparentSpec.tsx | 122 ++++++++++++++++++ web/src/js/__tests__/ducks/tutils.ts | 10 ++ web/src/js/__tests__/modes/socksSpec.ts | 31 +++++ web/src/js/__tests__/modes/transparentSpec.ts | 31 +++++ web/src/js/components/Modes.tsx | 7 + web/src/js/components/Modes/Socks.tsx | 62 +++++++++ web/src/js/components/Modes/Transparent.tsx | 68 ++++++++++ web/src/js/ducks/modes.ts | 4 + web/src/js/ducks/modes/socks.ts | 39 ++++++ web/src/js/ducks/modes/transparent.ts | 39 ++++++ web/src/js/ducks/modes/utils.ts | 4 + web/src/js/modes/socks.ts | 17 +++ web/src/js/modes/transparent.ts | 17 +++ 16 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 web/src/js/__tests__/ducks/modes/socksSpec.tsx create mode 100644 web/src/js/__tests__/ducks/modes/transparentSpec.tsx create mode 100644 web/src/js/__tests__/modes/socksSpec.ts create mode 100644 web/src/js/__tests__/modes/transparentSpec.ts create mode 100644 web/src/js/components/Modes/Socks.tsx create mode 100644 web/src/js/components/Modes/Transparent.tsx create mode 100644 web/src/js/ducks/modes/socks.ts create mode 100644 web/src/js/ducks/modes/transparent.ts create mode 100644 web/src/js/modes/socks.ts create mode 100644 web/src/js/modes/transparent.ts diff --git a/web/gen/state_js.py b/web/gen/state_js.py index c1ec8b3f3..187b53ea7 100755 --- a/web/gen/state_js.py +++ b/web/gen/state_js.py @@ -45,7 +45,7 @@ async def make() -> str: data.update(available=True) data["contentViews"] = ["Auto", "Raw"] data["version"] = "1.2.3" - data["platform"] = "win32" + data["platform"] = "darwin" # language=TypeScript content = ( diff --git a/web/src/js/__tests__/ducks/_tbackendstate.ts b/web/src/js/__tests__/ducks/_tbackendstate.ts index c99ec4bb9..2cedb31d6 100644 --- a/web/src/js/__tests__/ducks/_tbackendstate.ts +++ b/web/src/js/__tests__/ducks/_tbackendstate.ts @@ -7,7 +7,7 @@ export function TBackendState(): Required { "Auto", "Raw" ], - "platform": "win32", + "platform": "darwin", "servers": { "regular": { "description": "HTTP(S) proxy", diff --git a/web/src/js/__tests__/ducks/modes/socksSpec.tsx b/web/src/js/__tests__/ducks/modes/socksSpec.tsx new file mode 100644 index 000000000..dda061c0b --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/socksSpec.tsx @@ -0,0 +1,116 @@ +import socksReducer, { + initialState, + setListenHost, + setListenPort, + setActive, +} from "./../../../ducks/modes/socks"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("socksSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.socks[0]).toEqual({ + active: false, + }); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setActive({ value: true, server })); + await store.dispatch(setListenHost({ value: "127.0.0.1", server })); + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(store.getState().modes.socks[0]).toEqual({ + active: true, + listen_host: "127.0.0.1", + listen_port: 4444, + }); + + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("should handle error when setting socks mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setActive({ value: false, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.socks[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.socks[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.socks[0].error).toBe("invalid spec"); + }); + + it("should handle RECEIVE_STATE with an active socks proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "socks5@localhost:8081": { + description: "SOCKS v5 proxy", + full_spec: "socks5@localhost:8081", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8081], + ["::1", 8081], + ], + type: "socks5", + }, + }, + }, + } as PayloadAction>; + const newState = socksReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + listen_host: "localhost", + listen_port: 8081, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active socks proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = socksReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/transparentSpec.tsx b/web/src/js/__tests__/ducks/modes/transparentSpec.tsx new file mode 100644 index 000000000..d2b3c701c --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/transparentSpec.tsx @@ -0,0 +1,122 @@ +import transparentReducer, { + initialState, + setListenHost, + setListenPort, + setActive, +} from "./../../../ducks/modes/transparent"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("transparentSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.transparent[0]).toEqual({ + active: false, + }); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setActive({ value: true, server })); + await store.dispatch(setListenHost({ value: "127.0.0.1", server })); + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(store.getState().modes.transparent[0]).toEqual({ + active: true, + listen_host: "127.0.0.1", + listen_port: 4444, + }); + + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("should handle error when setting transparent mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setActive({ value: false, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.transparent[0].error).toBe( + "invalid spec", + ); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.transparent[0].error).toBe( + "invalid spec", + ); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.transparent[0].error).toBe( + "invalid spec", + ); + }); + + it("should handle RECEIVE_STATE with an active transparent proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "transparent@localhost:8081": { + description: "Transparent Proxy", + full_spec: "transparent@localhost:8081", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8081], + ["::1", 8081], + ], + type: "transparent", + }, + }, + }, + } as PayloadAction>; + const newState = transparentReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + listen_host: "localhost", + listen_port: 8081, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active transparent proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = transparentReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/tutils.ts b/web/src/js/__tests__/ducks/tutils.ts index 90e2501a2..25a5cad7b 100644 --- a/web/src/js/__tests__/ducks/tutils.ts +++ b/web/src/js/__tests__/ducks/tutils.ts @@ -157,6 +157,16 @@ export const testState: RootState = { }, defaultReverseState(), ], + transparent: [ + { + active: false, + }, + ], + socks: [ + { + active: false, + }, + ], }, }; diff --git a/web/src/js/__tests__/modes/socksSpec.ts b/web/src/js/__tests__/modes/socksSpec.ts new file mode 100644 index 000000000..f0b4f79fa --- /dev/null +++ b/web/src/js/__tests__/modes/socksSpec.ts @@ -0,0 +1,31 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, SocksState } from "../../modes/socks"; + +describe("getSpec socks mode", () => { + it("should return the correct mode config", () => { + const modes = { + socks: [ + { + active: true, + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.socks[0]); + expect(mode).toBe("socks5@localhost:8082"); + }); +}); + +describe("parseRaw socks mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("socks5@localhost:8082")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + listen_host: "localhost", + listen_port: 8082, + } as SocksState); + }); +}); diff --git a/web/src/js/__tests__/modes/transparentSpec.ts b/web/src/js/__tests__/modes/transparentSpec.ts new file mode 100644 index 000000000..1a9815d1a --- /dev/null +++ b/web/src/js/__tests__/modes/transparentSpec.ts @@ -0,0 +1,31 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, TransparentState } from "../../modes/transparent"; + +describe("getSpec transparent mode", () => { + it("should return the correct mode config", () => { + const modes = { + transparent: [ + { + active: true, + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.transparent[0]); + expect(mode).toBe("transparent@localhost:8082"); + }); +}); + +describe("parseRaw transparent mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("transparent@localhost:8082")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + listen_host: "localhost", + listen_port: 8082, + } as TransparentState); + }); +}); diff --git a/web/src/js/components/Modes.tsx b/web/src/js/components/Modes.tsx index 51d86e7bf..112713b76 100644 --- a/web/src/js/components/Modes.tsx +++ b/web/src/js/components/Modes.tsx @@ -4,6 +4,8 @@ import Regular from "./Modes/Regular"; import Wireguard from "./Modes/Wireguard"; import Reverse from "./Modes/Reverse"; import { useAppSelector } from "../ducks"; +import Transparent from "./Modes/Transparent"; +import Socks from "./Modes/Socks"; export default function Modes() { const platform = useAppSelector((state) => state.backendState.platform); @@ -18,6 +20,11 @@ export default function Modes() { {!platform.startsWith("linux") ? : undefined} + +

Advanced

+
+ + {!platform.startsWith("win32") ? : undefined} Remaining modes are coming soon...
diff --git a/web/src/js/components/Modes/Socks.tsx b/web/src/js/components/Modes/Socks.tsx new file mode 100644 index 000000000..da7f07051 --- /dev/null +++ b/web/src/js/components/Modes/Socks.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; +import { useAppDispatch, useAppSelector } from "../../ducks"; +import { getSpec, SocksState } from "../../modes/socks"; +import { ServerInfo } from "../../ducks/backendState"; +import { setActive } from "../../ducks/modes/socks"; + +import { ModeToggle } from "./ModeToggle"; +import { ServerStatus } from "./CaptureSetup"; + +export default function Socks() { + const serverState = useAppSelector((state) => state.modes.socks); + const backendState = useAppSelector((state) => state.backendState.servers); + + const servers = serverState.map((server) => { + return ( + + ); + }); + + return ( +
+

SOCKS Proxy

+

+ You manually configure your client application or device to use + a SOCKSv5 proxy. +

+ + {servers} +
+ ); +} + +function SocksRow({ + server, + backendState, +}: { + server: SocksState; + backendState?: ServerInfo; +}) { + const dispatch = useAppDispatch(); + + const error = server.error || backendState?.last_exception || undefined; + + return ( +
+ + dispatch(setActive({ server, value: !server.active })) + } + > + Run SOCKS Proxy + {/** Add here popover to set listen_host and listen_port */} + + +
+ ); +} diff --git a/web/src/js/components/Modes/Transparent.tsx b/web/src/js/components/Modes/Transparent.tsx new file mode 100644 index 000000000..d2ac5c567 --- /dev/null +++ b/web/src/js/components/Modes/Transparent.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; +import { useAppDispatch, useAppSelector } from "../../ducks"; +import { getSpec, TransparentState } from "../../modes/transparent"; +import { ServerInfo } from "../../ducks/backendState"; +import { setActive } from "../../ducks/modes/transparent"; + +import { ModeToggle } from "./ModeToggle"; +import { ServerStatus } from "./CaptureSetup"; + +export default function Transparent() { + const serverState = useAppSelector((state) => state.modes.transparent); + const backendState = useAppSelector((state) => state.backendState.servers); + + const servers = serverState.map((server) => { + return ( + + ); + }); + + return ( +
+

Transparent Proxy

+

+ You{" "} + + configure your routing table + {" "} + to send traffic through mitmproxy. +

+ + {servers} +
+ ); +} + +function TransparentRow({ + server, + backendState, +}: { + server: TransparentState; + backendState?: ServerInfo; +}) { + const dispatch = useAppDispatch(); + + const error = server.error || backendState?.last_exception || undefined; + + return ( +
+ + dispatch(setActive({ server, value: !server.active })) + } + > + Run Transparent Proxy + {/** Add here popover to set listen_host and listen_port */} + + +
+ ); +} diff --git a/web/src/js/ducks/modes.ts b/web/src/js/ducks/modes.ts index e65106b34..227f798a6 100644 --- a/web/src/js/ducks/modes.ts +++ b/web/src/js/ducks/modes.ts @@ -3,12 +3,16 @@ import regularReducer from "./modes/regular"; import localReducer from "./modes/local"; import wireguardReducer from "./modes/wireguard"; import reverseReducer from "./modes/reverse"; +import transparentReducer from "./modes/transparent"; +import socksReducer from "./modes/socks"; const modes = combineReducers({ regular: regularReducer, local: localReducer, wireguard: wireguardReducer, reverse: reverseReducer, + transparent: transparentReducer, + socks: socksReducer, //add new modes here }); diff --git a/web/src/js/ducks/modes/socks.ts b/web/src/js/ducks/modes/socks.ts new file mode 100644 index 000000000..5729d4a32 --- /dev/null +++ b/web/src/js/ducks/modes/socks.ts @@ -0,0 +1,39 @@ +import { parseRaw, SocksState } from "../../modes/socks"; +import { + RECEIVE as RECEIVE_STATE, + UPDATE as UPDATE_STATE, +} from "../backendState"; +import { addSetter, createModeUpdateThunk, updateState } from "./utils"; +import { createSlice } from "@reduxjs/toolkit"; + +export const setActive = createModeUpdateThunk( + "modes/socks5/setActive", +); +export const setListenHost = createModeUpdateThunk( + "modes/socks5/setListenHost", +); +export const setListenPort = createModeUpdateThunk( + "modes/socks5/setListenPort", +); + +export const initialState: SocksState[] = [ + { + active: false, + ui_id: Math.random(), + }, +]; + +export const socksSlice = createSlice({ + name: "modes/socks5", + initialState, + reducers: {}, + extraReducers: (builder) => { + addSetter(builder, "active", setActive); + addSetter(builder, "listen_host", setListenHost); + addSetter(builder, "listen_port", setListenPort); + builder.addCase(RECEIVE_STATE, updateState("socks5", parseRaw)); + builder.addCase(UPDATE_STATE, updateState("socks5", parseRaw)); + }, +}); + +export default socksSlice.reducer; diff --git a/web/src/js/ducks/modes/transparent.ts b/web/src/js/ducks/modes/transparent.ts new file mode 100644 index 000000000..3321d9a76 --- /dev/null +++ b/web/src/js/ducks/modes/transparent.ts @@ -0,0 +1,39 @@ +import { parseRaw, TransparentState } from "../../modes/transparent"; +import { + RECEIVE as RECEIVE_STATE, + UPDATE as UPDATE_STATE, +} from "../backendState"; +import { addSetter, createModeUpdateThunk, updateState } from "./utils"; +import { createSlice } from "@reduxjs/toolkit"; + +export const setActive = createModeUpdateThunk( + "modes/transparent/setActive", +); +export const setListenHost = createModeUpdateThunk( + "modes/transparent/setListenHost", +); +export const setListenPort = createModeUpdateThunk( + "modes/transparent/setListenPort", +); + +export const initialState: TransparentState[] = [ + { + active: false, + ui_id: Math.random(), + }, +]; + +export const transparentSlice = createSlice({ + name: "modes/transparent", + initialState, + reducers: {}, + extraReducers: (builder) => { + addSetter(builder, "active", setActive); + addSetter(builder, "listen_host", setListenHost); + addSetter(builder, "listen_port", setListenPort); + builder.addCase(RECEIVE_STATE, updateState("transparent", parseRaw)); + builder.addCase(UPDATE_STATE, updateState("transparent", parseRaw)); + }, +}); + +export default transparentSlice.reducer; diff --git a/web/src/js/ducks/modes/utils.ts b/web/src/js/ducks/modes/utils.ts index 9d535a769..9b3608221 100644 --- a/web/src/js/ducks/modes/utils.ts +++ b/web/src/js/ducks/modes/utils.ts @@ -2,6 +2,8 @@ import { getSpec as getRegularSpec } from "../../modes/regular"; import { getSpec as getLocalSpec } from "../../modes/local"; import { getSpec as getWireguardSpec } from "../../modes/wireguard"; import { getSpec as getReverseSpec } from "../../modes/reverse"; +import { getSpec as getTransparentSpec } from "../../modes/transparent"; +import { getSpec as getSocksSpec } from "../../modes/socks"; import { fetchApi } from "../../utils"; import { BackendState } from "../backendState"; import { @@ -27,6 +29,8 @@ export async function updateModes(_, thunkAPI) { ...modes.local.filter(isActiveMode).map(getLocalSpec), ...modes.wireguard.filter(isActiveMode).map(getWireguardSpec), ...modes.reverse.filter(isActiveMode).map(getReverseSpec), + ...modes.transparent.filter(isActiveMode).map(getTransparentSpec), + ...modes.socks.filter(isActiveMode).map(getSocksSpec), //add new modes here ]; const response = await fetchApi.put("/options", { diff --git a/web/src/js/modes/socks.ts b/web/src/js/modes/socks.ts new file mode 100644 index 000000000..ed9a45ead --- /dev/null +++ b/web/src/js/modes/socks.ts @@ -0,0 +1,17 @@ +import { includeListenAddress, ModeState, RawSpecParts } from "."; + +export interface SocksState extends ModeState {} + +export const getSpec = (s: SocksState): string => { + return includeListenAddress("socks5", s); +}; + +export const parseRaw = ({ + listen_host, + listen_port, +}: RawSpecParts): SocksState => ({ + ui_id: Math.random(), + active: true, + listen_host, + listen_port, +}); diff --git a/web/src/js/modes/transparent.ts b/web/src/js/modes/transparent.ts new file mode 100644 index 000000000..083689948 --- /dev/null +++ b/web/src/js/modes/transparent.ts @@ -0,0 +1,17 @@ +import { includeListenAddress, ModeState, RawSpecParts } from "."; + +export interface TransparentState extends ModeState {} + +export const getSpec = (s: TransparentState): string => { + return includeListenAddress("transparent", s); +}; + +export const parseRaw = ({ + listen_host, + listen_port, +}: RawSpecParts): TransparentState => ({ + ui_id: Math.random(), + active: true, + listen_host, + listen_port, +}); From aa7a912d898dc68fed2216e294cefb9fd1761efb Mon Sep 17 00:00:00 2001 From: Matteo Luppi <100372313+lups2000@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:30:41 +0200 Subject: [PATCH 04/96] Feature/popover modes (#7078) * create UI popover for mode configurations * add listen_host support to regular mode * add listen_host, port and path to wireguard mode * leftover * [autofix.ci] apply automated fixes * adapt popover to new rtk logic * update snapshots * use popover api * [autofix.ci] apply automated fixes * popovers: simplify css, make popover render ok on browsers without anchor support * fix tests * [autofix.ci] apply automated fixes * coverage++ * doc++ * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Maximilian Hils Co-authored-by: Maximilian Hils --- web/src/css/mode.less | 48 ++++++++++- .../Modes/__snapshots__/RegularSpec.tsx.snap | 84 ++++++++++++++++--- .../__tests__/ducks/modes/wireguardSpec.tsx | 2 - web/src/js/__tests__/modes/wireguardSpec.ts | 8 +- web/src/js/components/Modes/Local.tsx | 1 + web/src/js/components/Modes/Popover.tsx | 42 ++++++++++ web/src/js/components/Modes/Regular.tsx | 51 +++++++---- web/src/js/components/Modes/Wireguard.tsx | 47 ++++++++++- web/src/js/ducks/modes/wireguard.ts | 2 - web/src/js/modes/wireguard.ts | 5 +- 10 files changed, 253 insertions(+), 37 deletions(-) create mode 100644 web/src/js/components/Modes/Popover.tsx diff --git a/web/src/css/mode.less b/web/src/css/mode.less index e6ca52f05..cc74ae3b7 100644 --- a/web/src/css/mode.less +++ b/web/src/css/mode.less @@ -64,11 +64,11 @@ height: 25px; } - .mode-regular-input { + .mode-input { border: 1px solid #ccc; margin-left: 10px; border-radius: 4px; - min-width: 70px; + min-width: 120px; height: 25px; } @@ -110,4 +110,48 @@ margin-right: 5px; } } + + .mode-popover { + > button { + background-color: transparent; + border: none; + cursor: pointer; + font-size: 2rem; + } + + @supports (anchor-name: --test) { + > div { + margin: 0; + position: absolute; + left: calc(anchor(right) + 5px); + align-self: anchor-center; + } + } + + > div { + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px 15px; + background-color: #f9f9f9; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + + &:popover-open { + display: grid; + } + grid-template-columns: 9em 1fr; + grid-auto-rows: min-content; + gap: 0.5rem; + align-items: center; + + > h4 { + text-align: center; + grid-column: 1/-1; + } + > .mode-input { + text-align: right; + background-color: white; + margin: 0; + } + } + } } diff --git a/web/src/js/__tests__/components/Modes/__snapshots__/RegularSpec.tsx.snap b/web/src/js/__tests__/components/Modes/__snapshots__/RegularSpec.tsx.snap index 3f75142f6..943d5622a 100644 --- a/web/src/js/__tests__/components/Modes/__snapshots__/RegularSpec.tsx.snap +++ b/web/src/js/__tests__/components/Modes/__snapshots__/RegularSpec.tsx.snap @@ -21,12 +21,42 @@ exports[`RegularSpec 1`] = ` checked="" type="checkbox" /> - Run HTTP/S Proxy on port - + Run HTTP/S Proxy +
+