diff options
-rw-r--r-- | docs/src/content/addons-scripting.md | 17 | ||||
-rw-r--r-- | docs/src/content/howto-transparent.md | 6 | ||||
-rw-r--r-- | examples/addons/scripting-headers.py (renamed from examples/addons/scripting.py) | 0 | ||||
-rw-r--r-- | examples/complex/dns_spoofing.py | 6 | ||||
-rw-r--r-- | mitmproxy/tools/console/commander/commander.py | 49 | ||||
-rw-r--r-- | mitmproxy/tools/console/defaultkeys.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/options.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/statusbar.py | 7 | ||||
-rw-r--r-- | mitmproxy/tools/web/app.py | 67 | ||||
-rw-r--r-- | test/mitmproxy/tools/console/test_commander.py | 62 |
10 files changed, 181 insertions, 37 deletions
diff --git a/docs/src/content/addons-scripting.md b/docs/src/content/addons-scripting.md index e31d291a..4e9916ca 100644 --- a/docs/src/content/addons-scripting.md +++ b/docs/src/content/addons-scripting.md @@ -14,4 +14,19 @@ handler functions in the module scope. For instance, here is a complete script that adds a header to every request. -{{< example src="examples/addons/scripting.py" lang="py" >}}
\ No newline at end of file +{{< example src="examples/addons/scripting-headers.py" lang="py" >}} + + +Here's another example that intercepts requests to a particular URL and sends +an arbitrary response instead: + +{{< example src="examples/simple/send_reply_from_proxy.py" lang="py" >}} + + +You can look at the [http][] module, or the [Request][], and +[Response][] classes for other attributes that you can use when +scripting. + +[http][]: https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/http.py +[Request]: https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/net/http/request.py +[Response]: https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/net/http/response.py diff --git a/docs/src/content/howto-transparent.md b/docs/src/content/howto-transparent.md index 07a21ec9..ae36f579 100644 --- a/docs/src/content/howto-transparent.md +++ b/docs/src/content/howto-transparent.md @@ -163,14 +163,14 @@ for earlier versions of OSX. sudo sysctl -w net.inet.ip.forwarding=1 {{< / highlight >}} -### 2. Place the following two lines in a file called, say, **pf.conf**. +### 2. Place the following line in a file called, say, **pf.conf**. {{< highlight none >}} -rdr on en0 inet proto tcp to any port {80, 443} -> 127.0.0.1 port 8080 +rdr pass on en0 inet proto tcp to any port {80, 443} -> 127.0.0.1 port 8080 {{< / highlight >}} -These rules tell pf to redirect all traffic destined for port 80 or 443 +This rule tells pf to redirect all traffic destined for port 80 or 443 to the local mitmproxy instance running on port 8080. You should replace `en0` with the interface on which your test device will appear. diff --git a/examples/addons/scripting.py b/examples/addons/scripting-headers.py index 8b23680e..8b23680e 100644 --- a/examples/addons/scripting.py +++ b/examples/addons/scripting-headers.py diff --git a/examples/complex/dns_spoofing.py b/examples/complex/dns_spoofing.py index e28934ab..a3c1a017 100644 --- a/examples/complex/dns_spoofing.py +++ b/examples/complex/dns_spoofing.py @@ -13,12 +13,12 @@ Usage: -p 443 -s dns_spoofing.py # Used as the target location if neither SNI nor host header are present. - -R http://example.com/ + --mode reverse:http://example.com/ # To avoid auto rewriting of host header by the reverse proxy target. - --keep-host-header + --set keep-host-header mitmdump -p 80 - -R http://localhost:443/ + --mode reverse:http://localhost:443/ (Setting up a single proxy instance and using iptables to redirect to it works as well) diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index df3eaa5a..e8550f86 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -1,5 +1,7 @@ import abc +import copy import typing +import collections import urwid from urwid.text_layout import calc_coords @@ -156,13 +158,53 @@ class CommandBuffer: self.completion = None +class CommandHistory: + def __init__(self, master: mitmproxy.master.Master, size: int=30) -> None: + self.saved_commands: collections.deque = collections.deque( + [CommandBuffer(master, "")], + maxlen=size + ) + self.index: int = 0 + + @property + def last_index(self): + return len(self.saved_commands) - 1 + + def get_next(self) -> typing.Optional[CommandBuffer]: + if self.index < self.last_index: + self.index = self.index + 1 + return self.saved_commands[self.index] + return None + + def get_prev(self) -> typing.Optional[CommandBuffer]: + if self.index > 0: + self.index = self.index - 1 + return self.saved_commands[self.index] + return None + + def add_command(self, command: CommandBuffer, execution: bool=False) -> None: + if self.index == self.last_index or execution: + last_item = self.saved_commands[-1] + last_item_empty = not last_item.text + if last_item.text == command.text or (last_item_empty and execution): + self.saved_commands[-1] = copy.copy(command) + else: + self.saved_commands.append(command) + if not execution and self.index < self.last_index: + self.index += 1 + if execution: + self.index = self.last_index + + class CommandEdit(urwid.WidgetWrap): leader = ": " - def __init__(self, master: mitmproxy.master.Master, text: str) -> None: + def __init__(self, master: mitmproxy.master.Master, + text: str, history: CommandHistory) -> None: super().__init__(urwid.Text(self.leader)) self.master = master self.cbuf = CommandBuffer(master, text) + self.history = history self.update() def keypress(self, size, key): @@ -172,6 +214,11 @@ class CommandEdit(urwid.WidgetWrap): self.cbuf.left() elif key == "right": self.cbuf.right() + elif key == "up": + self.history.add_command(self.cbuf) + self.cbuf = self.history.get_prev() or self.cbuf + elif key == "down": + self.cbuf = self.history.get_next() or self.cbuf elif key == "tab": self.cbuf.cycle_completion() elif len(key) == 1: diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index 0f2e9072..0a6c5561 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -60,7 +60,7 @@ def map(km): km.add("M", "view.properties.marked.toggle", ["flowlist"], "Toggle viewing marked flows") km.add( "n", - "console.command view.create get https://example.com/", + "console.command view.flows.create get https://example.com/", ["flowlist"], "Create a new flow" ) diff --git a/mitmproxy/tools/console/options.py b/mitmproxy/tools/console/options.py index 6e1399ce..5e5ef2a6 100644 --- a/mitmproxy/tools/console/options.py +++ b/mitmproxy/tools/console/options.py @@ -174,7 +174,7 @@ class OptionsList(urwid.ListBox): foc, idx = self.get_focus() v = self.walker.get_edit_text() try: - d = self.master.options.parse_setval(foc.opt.name, v) + d = self.master.options.parse_setval(foc.opt, v) self.master.options.update(**{foc.opt.name: d}) except exceptions.OptionsError as v: signals.status_message.send(message=str(v)) diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index 215cf500..2d32f487 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -42,6 +42,8 @@ class ActionBar(urwid.WidgetWrap): signals.status_prompt_onekey.connect(self.sig_prompt_onekey) signals.status_prompt_command.connect(self.sig_prompt_command) + self.command_history = commander.CommandHistory(master) + self.prompting = None self.onekey = False @@ -98,7 +100,8 @@ class ActionBar(urwid.WidgetWrap): def sig_prompt_command(self, sender, partial=""): signals.focus.send(self, section="footer") - self._w = commander.CommandEdit(self.master, partial) + self._w = commander.CommandEdit(self.master, partial, + self.command_history) self.prompting = commandexecutor.CommandExecutor(self.master) def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): @@ -125,6 +128,7 @@ class ActionBar(urwid.WidgetWrap): def keypress(self, size, k): if self.prompting: if k == "esc": + self.command_history.index = self.command_history.last_index self.prompt_done() elif self.onekey: if k == "enter": @@ -132,6 +136,7 @@ class ActionBar(urwid.WidgetWrap): elif k in self.onekey: self.prompt_execute(k) elif k == "enter": + self.command_history.add_command(self._w.cbuf, True) self.prompt_execute(self._w.get_edit_text()) else: if common.is_keypress(k): diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index ae2394eb..b72e0d77 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -463,34 +463,20 @@ class SaveOptions(RequestHandler): pass +class DnsRebind(RequestHandler): + def get(self): + raise tornado.web.HTTPError( + 403, + reason="To protect against DNS rebinding, mitmweb can only be accessed by IP at the moment. " + "(https://github.com/mitmproxy/mitmproxy/issues/3234)" + ) + + class Application(tornado.web.Application): def __init__(self, master, debug): self.master = master - handlers = [ - (r"/", IndexHandler), - (r"/filter-help(?:\.json)?", FilterHelp), - (r"/updates", ClientConnection), - (r"/events(?:\.json)?", Events), - (r"/flows(?:\.json)?", Flows), - (r"/flows/dump", DumpFlows), - (r"/flows/resume", ResumeFlows), - (r"/flows/kill", KillFlows), - (r"/flows/(?P<flow_id>[0-9a-f\-]+)", FlowHandler), - (r"/flows/(?P<flow_id>[0-9a-f\-]+)/resume", ResumeFlow), - (r"/flows/(?P<flow_id>[0-9a-f\-]+)/kill", KillFlow), - (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\-]+)/(?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"/clear", ClearAll), - (r"/options(?:\.json)?", Options), - (r"/options/save", SaveOptions) - ] - settings = dict( + super().__init__( + default_host="dns-rebind-protection", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, @@ -498,4 +484,33 @@ class Application(tornado.web.Application): debug=debug, autoreload=False, ) - super().__init__(handlers, **settings) + + self.add_handlers("dns-rebind-protection", [(r"/.*", DnsRebind)]) + self.add_handlers( + # make mitmweb accessible by IP only to prevent DNS rebinding. + r'^(localhost|[0-9.:\[\]]+)$', + [ + (r"/", IndexHandler), + (r"/filter-help(?:\.json)?", FilterHelp), + (r"/updates", ClientConnection), + (r"/events(?:\.json)?", Events), + (r"/flows(?:\.json)?", Flows), + (r"/flows/dump", DumpFlows), + (r"/flows/resume", ResumeFlows), + (r"/flows/kill", KillFlows), + (r"/flows/(?P<flow_id>[0-9a-f\-]+)", FlowHandler), + (r"/flows/(?P<flow_id>[0-9a-f\-]+)/resume", ResumeFlow), + (r"/flows/(?P<flow_id>[0-9a-f\-]+)/kill", KillFlow), + (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\-]+)/(?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"/clear", ClearAll), + (r"/options(?:\.json)?", Options), + (r"/options/save", SaveOptions) + ] + ) diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 2a96995d..b5e226fe 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -28,6 +28,68 @@ class TestListCompleter: assert c.cycle() == expected +class TestCommandHistory: + def fill_history(self, commands): + with taddons.context() as tctx: + history = commander.CommandHistory(tctx.master, size=3) + for c in commands: + cbuf = commander.CommandBuffer(tctx.master, c) + history.add_command(cbuf) + return history, tctx.master + + def test_add_command(self): + commands = ["command1", "command2"] + history, tctx_master = self.fill_history(commands) + + saved_commands = [buf.text for buf in history.saved_commands] + assert saved_commands == [""] + commands + + # The history size is only 3. So, we forget the first + # one command, when adding fourth command + cbuf = commander.CommandBuffer(tctx_master, "command3") + history.add_command(cbuf) + saved_commands = [buf.text for buf in history.saved_commands] + assert saved_commands == commands + ["command3"] + + # Commands with the same text are not repeated in the history one by one + history.add_command(cbuf) + saved_commands = [buf.text for buf in history.saved_commands] + assert saved_commands == commands + ["command3"] + + # adding command in execution mode sets index at the beginning of the history + # and replace the last command buffer if it is empty or has the same text + cbuf = commander.CommandBuffer(tctx_master, "") + history.add_command(cbuf) + history.index = 0 + cbuf = commander.CommandBuffer(tctx_master, "command4") + history.add_command(cbuf, True) + assert history.index == history.last_index + saved_commands = [buf.text for buf in history.saved_commands] + assert saved_commands == ["command2", "command3", "command4"] + + def test_get_next(self): + commands = ["command1", "command2"] + history, tctx_master = self.fill_history(commands) + + history.index = -1 + expected_items = ["", "command1", "command2"] + for i in range(3): + assert history.get_next().text == expected_items[i] + # We are at the last item of the history + assert history.get_next() is None + + def test_get_prev(self): + commands = ["command1", "command2"] + history, tctx_master = self.fill_history(commands) + + expected_items = ["command2", "command1", ""] + history.index = history.last_index + 1 + for i in range(3): + assert history.get_prev().text == expected_items[i] + # We are at the first item of the history + assert history.get_prev() is None + + class TestCommandBuffer: def test_backspace(self): |